tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -52,7 +52,18 @@ export async function followView(frameText, seams) {
52
52
  const timer = setInterval(() => {
53
53
  if (painting)
54
54
  return;
55
- const result = frameText();
55
+ let result;
56
+ try {
57
+ result = frameText();
58
+ }
59
+ catch {
60
+ // A sync frame (status, backlog, report) may throw the same transient failure as an
61
+ // async one — a snapshot lift, a momentarily unreadable file — and a throw out of a
62
+ // setInterval callback is an uncaught exception that kills the whole interactive
63
+ // follow, while the identical failure a tick later (async path) is skipped. Skip this
64
+ // repaint and retry on the next tick, the same policy the async path applies.
65
+ return;
66
+ }
56
67
  if (result instanceof Promise) {
57
68
  painting = true;
58
69
  void result
@@ -63,7 +74,14 @@ export async function followView(frameText, seams) {
63
74
  });
64
75
  return;
65
76
  }
66
- paint(result);
77
+ // A sync frame's paint is guarded like its read above: a paint that throws (a broken
78
+ // stdout mid-follow) is skipped and retried, never fatal.
79
+ try {
80
+ paint(result);
81
+ }
82
+ catch {
83
+ // Transient — retry on the next tick, like the async path.
84
+ }
67
85
  }, interval);
68
86
  startParentDeathWatch(() => {
69
87
  clearInterval(timer);
@@ -4,6 +4,7 @@
4
4
  * <command>` from it by splitting the usage block on its ` tumwater <command>` lines, so a
5
5
  * topic can never drift out of sync with the full listing. */
6
6
  import { suggestClosest } from "../text/suggest.js";
7
+ import { distinctOrdered } from "../collections.js";
7
8
  /** The role-grouped summary's JSON keys, quoted verbatim in `history --summary --json` help
8
9
  * (its `--by model` twin is the hardcoded `{ models, total }` beside it). */
9
10
  const SUMMARY_JSON_KEYS = "{ roles, total }";
@@ -313,9 +314,8 @@ Usage:
313
314
  non-blank line is the symptom, the rest is the body kept
314
315
  line for line; wakes the bugfix role
315
316
  tumwater bug --release <n> [--json]
316
- Lift the Refused or a
317
- Needs-review note held past its threshold
318
- (one per run) from the Nth entry in BUGS.md ## Open (as
317
+ Lift the Refused or the
318
+ Needs review notes from the Nth entry in BUGS.md ## Open (as
319
319
  \`tumwater bug --list\` lists it), commit it on main, and wake
320
320
  the bugfix role; --json prints {file, title, lifted}
321
321
  tumwater bug --top <n> [--json]
@@ -370,9 +370,8 @@ Usage:
370
370
  the first non-blank line is the title, the rest is the body
371
371
  kept line for line; wakes the feature role
372
372
  tumwater plan --release <n> [--json]
373
- Lift the Refused or a
374
- Needs-review note held past its threshold
375
- (one per run) from the Nth entry in PLANS.md ## Planned
373
+ Lift the Refused or the
374
+ Needs review notes from the Nth entry in PLANS.md ## Planned
376
375
  (as \`tumwater plan --list\` lists it), commit it on main,
377
376
  and wake the feature role; --json prints {file, title, lifted}
378
377
  tumwater plan --top <n> [--json]
@@ -528,12 +527,6 @@ Usage:
528
527
  written to roles.<id>.instances for the base id
529
528
  tumwater role <id> --clear-instances
530
529
  Clear that role's instance count (back to 1)
531
- tumwater role <id> --review-after-merge
532
- Review that role's commits after they merge, written
533
- to review.afterMerge (the base id for an instance)
534
- tumwater role <id> --clear-review-after-merge
535
- Review it before merge again, removing it from
536
- review.afterMerge
537
530
  tumwater prompt <text...> Send a message to the director's inbox
538
531
  tumwater prompt --file <path> Send a message read from a file ("-" reads stdin)
539
532
  tumwater prompt --role <id> <text...>
@@ -648,7 +641,7 @@ sleep while there is nothing to do and wake when main moves. Everything is local
648
641
  * stanza; the distance mechanics are text/suggest.ts's suggestClosest, shared with the config-key
649
642
  * suggestion. */
650
643
  export function suggestCommand(input, help = HELP) {
651
- return suggestClosest(input, [...new Set(helpStanzas(help).map((s) => s.command))]);
644
+ return suggestClosest(input, distinctOrdered(helpStanzas(help).map((s) => s.command)));
652
645
  }
653
646
  /** Split HELP's usage block into per-command stanzas. A stanza starts at a two-space-indented
654
647
  * `tumwater <command>` line; any further indented line continues it, and a column-0 line (the
package/dist/src/cli.js CHANGED
@@ -13,16 +13,14 @@
13
13
  * ready-repo gate. An error escaping main() funnels through its catch to fail(errorMessage(err)),
14
14
  * the CLI's one error path. */
15
15
  import { parseCountFlag, parseGrepFlag, parseModelFlag, parseResultFlag } from "./cli/cli-args.js";
16
- import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, modelFlagSpec, byFlagSpec, LOGS_TRANSCRIPT_FLAG, ROLE_FLAG, START_FLAG_SPECS, sinceFlagSpec, untilFlagSpec, STAT_FLAG, PATCH_FLAG, PROMPT_FLAG, TRANSCRIPT_FLAG, SUMMARY_FLAG, COUNT_FLAG, NOTABLE_FLAG, typeFlagSpec, DRY_RUN_FLAG, FORCE_FLAG, FOLLOW_FLAG, STOP_CANCEL_FLAG, STOP_IN_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG, } from "./cli/cli-flag-specs.js";
17
- import { fail, say, sayJsonOrRender } from "./cli/cli-output.js";
18
- import { historyTrailForm, parseDoctorChecks, parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
16
+ import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, modelFlagSpec, byFlagSpec, LOGS_TRANSCRIPT_FLAG, ROLE_FLAG, START_FLAG_SPECS, sinceFlagSpec, untilFlagSpec, STAT_FLAG, PATCH_FLAG, PROMPT_FLAG, TRANSCRIPT_FLAG, SUMMARY_FLAG, COUNT_FLAG, NOTABLE_FLAG, typeFlagSpec, DRY_RUN_FLAG, FORCE_FLAG, FOLLOW_FLAG, STOP_CANCEL_FLAG, STOP_IN_FLAG, } from "./cli/cli-flag-specs.js";
17
+ import { fail, say } from "./cli/cli-output.js";
18
+ import { historyTrailForm, parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
19
19
  import { refuseTeamWorkspace, requireReadyRepo } from "./cli/cli-repo-gate.js";
20
- import { loadConfigSafe } from "./config/config.js";
21
20
  import { repoToplevel } from "./git/git.js";
22
21
  import { didYouMean } from "./text/suggest.js";
23
22
  import { errorMessage, gotSuffix, stripTerminalControls } from "./text/text.js";
24
23
  import { typoSuffix } from "./text/suggest.js";
25
- import { completionConfigKeys, completionModels, completionRoleIds, completionScript, configValueWords, SUPPORTED_SHELLS } from "./cli/completion.js";
26
24
  import { HELP, helpTopic, helpTopicForArgs, suggestCommand } from "./cli/help.js";
27
25
  import { PACKAGE_JSON, nodeFloorProblem, packageEnginesNode, packageVersion } from "./version.js";
28
26
  // The CLI's help text and its per-command topic parser live in help.ts — importing cli.ts
@@ -75,7 +73,7 @@ async function cmdGuiCase(root, args) {
75
73
  await (await import("./gui/gui-command.js")).cmdGui(root, args);
76
74
  }
77
75
  async function cmdConfigCase(root, args) {
78
- const { cmdConfig, CONFIG_USAGE } = await import("./cli/config-commands.js");
76
+ const { cmdConfig, CONFIG_USAGE } = await import("./cli/cli-config-commands.js");
79
77
  // Subcommand arity before the ready-repo gate, so a malformed get/set fails with its
80
78
  // usage no matter the directory: bare config takes nothing, `get`/`unset` exactly one
81
79
  // key, `set` exactly a key and a value. cmdConfig dispatches on the (now well-shaped)
@@ -126,98 +124,15 @@ async function cmdReportCase(root, args) {
126
124
  await (await import("./report/report.js")).cmdReport(root, args);
127
125
  }
128
126
  async function cmdDoctorCase(root, args) {
129
- // No requireReadyRepo gate: doctor's job is to report WHY the environment isn't ready,
130
- // so it must run outside a git repo and print fail lines rather than throwing.
131
- const { positionals: checkNames, rest: doctorFlags } = peelPositionals(args);
132
- rejectUnknownArgs("doctor", doctorFlags, [JSON_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG]);
133
- const only = parseDoctorChecks(checkNames);
134
- const { runDoctor } = await import("./doctor/doctor.js");
135
- let orphanKill;
136
- if (args.includes("--kill-orphans")) {
137
- const { killOrphans, orphanKillLines } = await import("./doctor/doctor-orphans.js");
138
- orphanKill = await killOrphans(root);
139
- // The text form prints the kill before the report; --json keeps stdout one document.
140
- if (!args.includes("--json")) {
141
- for (const line of orphanKillLines(orphanKill))
142
- say(line);
143
- say("");
144
- }
145
- }
146
- let backlogCommit;
147
- if (args.includes("--commit-backlog")) {
148
- const { commitUncommittedBacklog, backlogCommitLines } = await import("./doctor/doctor-backlog.js");
149
- const { config } = loadConfigSafe(root); // the report's own config, so base resolution and the off-base skip match the `uncommitted backlog` row's
150
- backlogCommit = await commitUncommittedBacklog(root, config);
151
- // The text form prints the commits before the report; --json keeps stdout one document.
152
- if (!args.includes("--json")) {
153
- for (const line of backlogCommitLines(backlogCommit))
154
- say(line);
155
- say("");
156
- }
157
- }
158
- let resyncInstall;
159
- if (args.includes("--resync-install")) {
160
- const { resyncRootInstall, rootInstallResyncLines } = await import("./build/dep-install.js");
161
- resyncInstall = await resyncRootInstall(root);
162
- // The text form prints the re-sync outcome before the report; --json keeps stdout one
163
- // document, carrying the result as `resyncInstall` on the payload.
164
- if (!args.includes("--json")) {
165
- for (const line of rootInstallResyncLines(resyncInstall))
166
- say(line);
167
- say("");
168
- }
169
- }
170
- let brokenPinDrop;
171
- if (args.includes("--drop-broken-pins")) {
172
- const { dropBrokenPins, brokenPinDropLines } = await import("./doctor/doctor-contention-checks.js");
173
- brokenPinDrop = await dropBrokenPins(root);
174
- // The text form prints the drop before the report; --json keeps stdout one document,
175
- // carrying the result as `brokenPinDrop` on the payload.
176
- if (!args.includes("--json")) {
177
- const lines = brokenPinDropLines(brokenPinDrop);
178
- for (const line of lines)
179
- say(line);
180
- if (lines.length > 0)
181
- say("");
182
- }
183
- }
184
- let leftoverRetire;
185
- if (args.includes("--retire-leftovers")) {
186
- const { retireLeftovers, leftoverRetireLines } = await import("./doctor/doctor-contention-checks.js");
187
- const { config } = loadConfigSafe(root); // the row's own config, so the reap and the report agree on what is retirable
188
- leftoverRetire = await retireLeftovers(root, config);
189
- // The text form prints the retire lines before the report; --json keeps stdout one
190
- // document, carrying the result as `leftoverRetire` on the payload.
191
- if (!args.includes("--json")) {
192
- const lines = leftoverRetireLines(leftoverRetire);
193
- for (const line of lines)
194
- say(line);
195
- if (lines.length > 0)
196
- say("");
197
- }
198
- }
199
- const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("./doctor/doctor-render.js");
200
- const report = await runDoctor(root, undefined, undefined, only);
201
- // --json prints the collector's own payload (the DoctorReport object), not a re-parse of
202
- // the render — the `report --json` precedent. The exit-code contract below holds in both
203
- // forms: 1 when any check fails, 0 otherwise; warnings never fail the exit.
204
- // --problems keeps the full report for the render (it counts the hidden ok rows) and
205
- // hands --json the filtered one; the exit code below reads the full report either way.
206
- // --kill-orphans adds the kill result to the JSON object as `orphanKill`.
207
- const withKill = (payload) => (orphanKill === undefined ? payload : { ...payload, orphanKill });
208
- const withBacklogCommit = (payload) => backlogCommit === undefined ? withKill(payload) : withKill({ ...payload, backlogCommit });
209
- const withResyncInstall = (payload) => resyncInstall === undefined ? withBacklogCommit(payload) : withBacklogCommit({ ...payload, resyncInstall });
210
- const withBrokenPinDrop = (payload) => brokenPinDrop === undefined ? withResyncInstall(payload) : withResyncInstall({ ...payload, brokenPinDrop });
211
- const withLeftoverRetire = (payload) => leftoverRetire === undefined ? withBrokenPinDrop(payload) : withBrokenPinDrop({ ...payload, leftoverRetire });
212
- if (args.includes("--problems"))
213
- sayJsonOrRender(args, args.includes("--json") ? withLeftoverRetire(problemsOnly(report)) : report, renderDoctorProblems);
214
- else
215
- sayJsonOrRender(args, args.includes("--json") ? withLeftoverRetire(report) : report, renderDoctor);
216
- if (report.checks.some((c) => c.level === "fail"))
217
- process.exitCode = 1; // Warnings never fail the exit.
127
+ // cmdDoctor lives in cli-doctor-command.ts (the doctor command body, its side-effect
128
+ // wrappers and payload shaping) and holds no ready-repo gate: doctor's job is to report
129
+ // WHY the environment isn't ready, so it must run outside a git repo and print fail
130
+ // lines rather than throwing. Imported lazily like every command body, so the doctor
131
+ // subgraph is only compiled when `tumwater doctor` is asked for.
132
+ await (await import("./cli/cli-doctor-command.js")).cmdDoctor(root, args);
218
133
  }
219
134
  async function cmdLogsCase(root, args) {
220
- const { cmdLogs, GREP_VALUE_ERROR, TYPE_VALUE_ERROR } = await import("./cli/log-commands.js");
135
+ const { cmdLogs, GREP_VALUE_ERROR, TYPE_VALUE_ERROR } = await import("./cli/cli-log-commands.js");
221
136
  rejectUnknownArgs("logs", args, [
222
137
  FOLLOW_FLAG,
223
138
  N_FLAG,
@@ -238,7 +153,7 @@ async function cmdLogsCase(root, args) {
238
153
  async function cmdNotifyCase(root, args) {
239
154
  rejectUnknownArgs("notify", args, [{ names: ["--test"] }]);
240
155
  await requireReadyRepo(root);
241
- await (await import("./cli/notify-command.js")).cmdNotify(root, args);
156
+ await (await import("./cli/cli-notify-command.js")).cmdNotify(root, args);
242
157
  }
243
158
  async function cmdHistoryCase(root, args) {
244
159
  const { cmdHistory, HISTORY_GREP_VALUE_ERROR, HISTORY_RESULT_VALUE_ERROR, HISTORY_MODEL_VALUE_ERROR, HISTORY_BY_VALUE_ERROR } = await import("./history/history.js");
@@ -267,69 +182,81 @@ async function cmdArchiveCase(root, args) {
267
182
  await requireReadyRepo(root);
268
183
  await (await import("./backlog/backlog-cli.js")).cmdArchive(root, args);
269
184
  }
270
- async function cmdBugCase(root, args) {
271
- const { OPERATOR_FILE_KINDS, bugWakeText, cancelBug, editBug, editBugFromText, fileBug, fileBugFromText } = await import("./backlog/backlog-write.js");
272
- const { moveEntryToTop, releaseEntry } = await import("./backlog/backlog-release.js");
273
- const { bugTitleOf, cancelAndAnnounce, releaseAndAnnounce, topAndAnnounce, editAndAnnounce, fileAndAnnounce } = await import("./backlog/backlog-cli.js");
185
+ /** The one dispatch `bug` and `plan` both run through (see BacklogCommandSpec): route on the
186
+ * leading flag token and hand off to the matching shared AndAnnounce shell, or file a text
187
+ * entry by default. */
188
+ async function runBacklogCommand(root, args, s) {
189
+ const { cancelAndAnnounce, editAndAnnounce, fileAndAnnounce, releaseAndAnnounce, topAndAnnounce } = await import("./backlog/backlog-cli.js");
274
190
  const { listAndAnnounce, showAndAnnounce } = await import("./backlog/backlog-cli-view.js");
275
- const usage = 'tumwater bug "<symptom>"';
276
191
  if (args.includes("--show")) {
277
- showAndAnnounce(root, args, "bug");
192
+ showAndAnnounce(root, args, s.command);
278
193
  return;
279
194
  }
280
195
  if (args.includes("--list")) {
281
- listAndAnnounce(root, args, "bug", "BUGS.md", "Open");
196
+ listAndAnnounce(root, args, s.command, s.file, s.section);
282
197
  return;
283
198
  }
284
199
  if (args.includes("--edit")) {
285
- await editAndAnnounce(root, args, "bug", (n, positionals) => editBug(root, n, positionals.join(" "), usage), (n, text) => editBugFromText(root, n, text, usage));
200
+ await editAndAnnounce(root, args, s.command, s.edit, s.editText);
286
201
  return;
287
202
  }
288
203
  if (args.includes("--cancel")) {
289
- await cancelAndAnnounce(root, args, "bug", (n) => cancelBug(root, n));
204
+ await cancelAndAnnounce(root, args, s.command, s.cancel);
290
205
  return;
291
206
  }
292
207
  if (args.includes("--top")) {
293
- await topAndAnnounce(root, args, "bug", (n) => moveEntryToTop(root, "bug", n));
208
+ await topAndAnnounce(root, args, s.command, s.top);
294
209
  return;
295
210
  }
296
211
  if (args.includes("--release")) {
297
- await releaseAndAnnounce(root, args, "bug", (n) => releaseEntry(root, "bug", n), OPERATOR_FILE_KINDS.bug.role);
212
+ await releaseAndAnnounce(root, args, s.command, s.release, s.releaseRole);
298
213
  return;
299
214
  }
300
- await fileAndAnnounce(root, args, "bug", (positionals) => fileBug(root, positionals.join(" "), usage), OPERATOR_FILE_KINDS.bug.role, bugWakeText, bugTitleOf, (text) => fileBugFromText(root, text, usage));
215
+ await fileAndAnnounce(root, args, s.command, s.fileOp, s.releaseRole, s.wakeText, s.titleOf, s.fileText);
216
+ }
217
+ async function cmdBugCase(root, args) {
218
+ const { OPERATOR_FILE_KINDS, bugWakeText, cancelBug, editBug, editBugFromText, fileBug, fileBugFromText } = await import("./backlog/backlog-write.js");
219
+ const { moveEntryToTop, releaseEntry } = await import("./backlog/backlog-release.js");
220
+ const { bugTitleOf } = await import("./backlog/backlog-cli.js");
221
+ const usage = 'tumwater bug "<symptom>"';
222
+ await runBacklogCommand(root, args, {
223
+ command: "bug",
224
+ usage,
225
+ file: "BUGS.md",
226
+ section: "Open",
227
+ edit: (n, positionals) => editBug(root, n, positionals.join(" "), usage),
228
+ editText: (n, text) => editBugFromText(root, n, text, usage),
229
+ cancel: (n) => cancelBug(root, n),
230
+ top: (n) => moveEntryToTop(root, "bug", n),
231
+ release: (n) => releaseEntry(root, "bug", n),
232
+ releaseRole: OPERATOR_FILE_KINDS.bug.role,
233
+ fileOp: (positionals) => fileBug(root, positionals.join(" "), usage),
234
+ fileText: (text) => fileBugFromText(root, text, usage),
235
+ wakeText: bugWakeText,
236
+ titleOf: bugTitleOf,
237
+ });
301
238
  }
302
239
  async function cmdPlanCase(root, args) {
303
240
  const { OPERATOR_FILE_KINDS, cancelPlan, editPlan, editPlanFromText, filePlan, filePlanFromText, planWakeText } = await import("./backlog/backlog-write.js");
304
241
  const { moveEntryToTop, releaseEntry } = await import("./backlog/backlog-release.js");
305
- const { cancelAndAnnounce, releaseAndAnnounce, topAndAnnounce, editAndAnnounce, fileAndAnnounce, planTitleOf } = await import("./backlog/backlog-cli.js");
306
- const { listAndAnnounce, showAndAnnounce } = await import("./backlog/backlog-cli-view.js");
242
+ const { planTitleOf } = await import("./backlog/backlog-cli.js");
307
243
  const usage = 'tumwater plan "<title>" [body...]';
308
- if (args.includes("--show")) {
309
- showAndAnnounce(root, args, "plan");
310
- return;
311
- }
312
- if (args.includes("--list")) {
313
- listAndAnnounce(root, args, "plan", "PLANS.md", "Planned");
314
- return;
315
- }
316
- if (args.includes("--edit")) {
317
- await editAndAnnounce(root, args, "plan", (n, positionals) => editPlan(root, n, positionals[0] ?? "", positionals.slice(1).join(" "), usage), (n, text) => editPlanFromText(root, n, text, usage));
318
- return;
319
- }
320
- if (args.includes("--cancel")) {
321
- await cancelAndAnnounce(root, args, "plan", (n) => cancelPlan(root, n));
322
- return;
323
- }
324
- if (args.includes("--top")) {
325
- await topAndAnnounce(root, args, "plan", (n) => moveEntryToTop(root, "plan", n));
326
- return;
327
- }
328
- if (args.includes("--release")) {
329
- await releaseAndAnnounce(root, args, "plan", (n) => releaseEntry(root, "plan", n), OPERATOR_FILE_KINDS.plan.role);
330
- return;
331
- }
332
- await fileAndAnnounce(root, args, "plan", (positionals) => filePlan(root, positionals[0] ?? "", positionals.slice(1).join(" "), usage), OPERATOR_FILE_KINDS.plan.role, planWakeText, planTitleOf, (text) => filePlanFromText(root, text, usage));
244
+ await runBacklogCommand(root, args, {
245
+ command: "plan",
246
+ usage,
247
+ file: "PLANS.md",
248
+ section: "Planned",
249
+ edit: (n, positionals) => editPlan(root, n, positionals[0] ?? "", positionals.slice(1).join(" "), usage),
250
+ editText: (n, text) => editPlanFromText(root, n, text, usage),
251
+ cancel: (n) => cancelPlan(root, n),
252
+ top: (n) => moveEntryToTop(root, "plan", n),
253
+ release: (n) => releaseEntry(root, "plan", n),
254
+ releaseRole: OPERATOR_FILE_KINDS.plan.role,
255
+ fileOp: (positionals) => filePlan(root, positionals[0] ?? "", positionals.slice(1).join(" "), usage),
256
+ fileText: (text) => filePlanFromText(root, text, usage),
257
+ wakeText: planWakeText,
258
+ titleOf: planTitleOf,
259
+ });
333
260
  }
334
261
  async function cmdPromptCase(root, args) {
335
262
  // Parse before the gate: prompt's positionals are free-form, so rejectUnknownArgs
@@ -364,43 +291,6 @@ async function cmdStopCase(root, args) {
364
291
  await requireReadyRepo(root);
365
292
  await (await import("./operator/operator-commands.js")).cmdStop(root, args);
366
293
  }
367
- async function cmdCompletionCase(root, args) {
368
- // Works outside a repo: no requireReadyRepo.
369
- if (args.length === 1 && args[0] === "--roles") {
370
- for (const id of completionRoleIds(root))
371
- say(id);
372
- return;
373
- }
374
- if (args.length === 1 && args[0] === "--config-keys") {
375
- for (const key of completionConfigKeys(root))
376
- say(key);
377
- return;
378
- }
379
- if (args.length === 2 && args[0] === "--config-value") {
380
- // The vocabulary `config set <key> <value>` completes for a discrete-valued key
381
- // (boolean/backend/model/thinking); free-form keys print nothing, so a `set` on one
382
- // completes nothing instead of offering a wrong vocabulary.
383
- for (const word of configValueWords(args[1]))
384
- say(word);
385
- return;
386
- }
387
- if (args.length === 1 && args[0] === "--models") {
388
- for (const m of completionModels(root))
389
- say(m);
390
- return;
391
- }
392
- const shell = args[0];
393
- // A wrong word count names the count, not a bad shell. A single near-miss shell word gets
394
- // the same typoSuffix `logs --type`/`history --result` give, so `completion zs` reads
395
- // "did you mean `zsh`?" while a far-off value (`tcsh`) stays hint-free.
396
- if (args.length === 0)
397
- fail("completion needs a shell: bash, zsh, or fish");
398
- if (args.length > 1)
399
- fail(`completion takes one shell: bash, zsh, or fish (got ${args.length} words: ${args.join(" ")})`);
400
- if (!SUPPORTED_SHELLS.includes(shell))
401
- fail(`completion takes one shell: bash, zsh, or fish${gotSuffix(shell)}${typoSuffix(shell ?? "", SUPPORTED_SHELLS)}`);
402
- say(completionScript(shell));
403
- }
404
294
  async function cmdVersionCase(args) {
405
295
  // A stray flag fails like every other command's: `tumwater version --json` (a plausible
406
296
  // slip from `status --json`) must not print a version as if it had answered the query.
@@ -535,10 +425,10 @@ async function main() {
535
425
  break;
536
426
  }
537
427
  case "questions":
538
- await (await import("./cli/question-commands.js")).cmdQuestions(root, args);
428
+ await (await import("./cli/cli-question-commands.js")).cmdQuestions(root, args);
539
429
  break;
540
430
  case "brief":
541
- await (await import("./cli/brief-command.js")).cmdBrief(root, args);
431
+ await (await import("./cli/cli-brief-command.js")).cmdBrief(root, args);
542
432
  break;
543
433
  case "role":
544
434
  await (await import("./cli/cli-role-command.js")).cmdRole(root, args);
@@ -574,7 +464,11 @@ async function main() {
574
464
  break;
575
465
  }
576
466
  case "completion": {
577
- await cmdCompletionCase(root, args);
467
+ // Works outside a repo (no requireReadyRepo): the body lives in
468
+ // cli-completion-command.ts, beside its data (src/cli/completion.ts), and is imported
469
+ // lazily like every command body so the dispatcher never compiles the completion
470
+ // subgraph until `tumwater completion` is asked for.
471
+ await (await import("./cli/cli-completion-command.js")).cmdCompletion(root, args);
578
472
  break;
579
473
  }
580
474
  case "version":
@@ -4,10 +4,10 @@
4
4
  * utilities module. */
5
5
  /** Group items by a key, preserving first-seen key order and input order within each group.
6
6
  * The single home of the "append to a Map of arrays" step, shared by the event bucketers
7
- * (history-queued.ts's bucketMergeEvents, failure/time-spend.ts's rejected-by-role pass) and
8
- * the team-hold poll's observations-by-provider grouping (team-polls.ts's pollTeamHold), so
9
- * those sites cannot drift on how a missing key is seeded. Returns a fresh Map; the input is
10
- * never mutated. */
7
+ * (history-queued.ts's bucketMergeEvents and failure/failure-time-spend.ts's rejected-by-role
8
+ * pass) and the team-hold poll's observations-by-provider grouping (team-polls.ts's
9
+ * pollTeamHold), so those sites cannot drift on how a missing key is seeded. Returns a fresh
10
+ * Map; the input is never mutated. */
11
11
  export function groupBy(items, key) {
12
12
  const groups = new Map();
13
13
  for (const item of items) {
@@ -15,11 +15,31 @@ export function groupBy(items, key) {
15
15
  }
16
16
  return groups;
17
17
  }
18
+ /** The distinct items of an iterable, in first-seen order — the single home of the
19
+ * `seen.includes -> push` dedupe, shared by the pending pane's three scope lists
20
+ * (mergeQueueRoles, postReviewRoles, overlapHolders in ui/tui/tui-pane-pending.ts), the
21
+ * session pane's scope cycle (ui/tui/tui-pane-session.ts's scopeCycle), the report
22
+ * scope fold (report/report-fold.ts's scopeBases), and the rest of the harness's
23
+ * `[...new Set(...)]` dedupes (completion.ts's commands/config keys, operator-commands.ts's
24
+ * role lists, text.ts's splitCommaListUnique/pathTokens, startup-gates.ts's backendsInUse,
25
+ * failure-data.ts's delta roles, doctor-backlog.ts's watched set, doctor-orphans.ts's pid
26
+ * list, main-watch.ts's knownMainFailures, help.ts's suggestCommand candidates, and
27
+ * tui-pane-activity.ts's present types), so those sites cannot drift on how a repeat is
28
+ * skipped. Returns a fresh array; the input is never mutated. (The dashboard's
29
+ * role-scope builder in ui/gui/gui-client-report.ts keeps its own copy by design: it runs
30
+ * in the browser, where harness modules cannot be imported.) */
31
+ export function distinctOrdered(items) {
32
+ const seen = [];
33
+ for (const item of items)
34
+ if (!seen.includes(item))
35
+ seen.push(item);
36
+ return seen;
37
+ }
18
38
  /** Return `map`'s value for `key`, creating and storing `make()`'s result first when the key is
19
39
  * absent — the single home of the `map.get(key) ?? fresh; …; map.set(key, value)` step, shared
20
40
  * by the failure digest's per-role stats, outcome, and prompt-token accumulators (failure-data.ts),
21
- * its time-and-spend per-role row (time-spend.ts), the team hold's provider+kind grouping
22
- * (team-hold.ts), the error-storm reducer's roles-by-key set (src/team/error-storm.ts), and
41
+ * its time-and-spend per-role row (failure-time-spend.ts), the team hold's provider+kind grouping
42
+ * (team-hold.ts), the error-storm reducer's roles-by-key set (src/team/team-error-storm.ts), and
23
43
  * `groupBy` above, so those sites cannot drift on how a missing key is seeded. `make` runs only
24
44
  * when the key is absent. A stored value of `undefined` reads as absent, matching the `??` idiom
25
45
  * this replaces; the callers above never store one. */
@@ -50,9 +70,9 @@ export function filterKeepPositions(items, keep) {
50
70
  }
51
71
  /** Increment a Map counter, seeding a missing key at 1 — the single home of the
52
72
  * `map.set(k, (map.get(k) ?? 0) + 1)` step, shared by the failure-spread kind tally
53
- * (src/team/failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
73
+ * (src/team/team-failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
54
74
  * section-title tally (backlog/backlog-structure.ts), the logs' per-type tally
55
- * (cli/log-commands.ts), the flaky-check tally
75
+ * (cli/cli-log-commands.ts), the flaky-check tally
56
76
  * (doctor/doctor-project-checks.ts), and the multi-runner base tally
57
77
  * (orchestrator/orchestrator-scheduling.ts) — the exit-summary round's outcome tally
58
78
  * (cli/cli-start-summary.ts) seeds its own copy of this step inline — so those sites cannot
@@ -125,11 +145,11 @@ export function trimToLast(arr, max) {
125
145
  * order the same way. These two helpers are the single home of that rule; every surface that
126
146
  * ranks counters by count calls one of them rather than re-implementing the comparator —
127
147
  * `rankCountEntries` serves the `[key, count]` entry shape (src/report/report-render.ts's
128
- * rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick) and `rankByCount` serves
129
- * callers whose counts are fields or computations on other shapes
130
- * (src/failure/failure-cluster.ts's clusters, src/team/error-storm.ts's strongest-cause pick,
131
- * failure-data.ts's outcome table, and time-spend.ts's time-and-spend table). `sortByValueDesc`
132
- * is the deliberate sibling for the same value-descending shape that wants a code-unit tiebreak
148
+ * rankedRoleMap, src/team/team-failure-spread.ts's strongest-kind pick) and `rankByCount` serves
149
+ * callers whose counts are fields or computations on other shapes (src/failure/failure-cluster.ts's
150
+ * clusters and src/team/team-error-storm.ts's strongest-cause pick; failure-data.ts's outcome
151
+ * table and failure-time-spend.ts's time-and-spend table). `sortByValueDesc` is the deliberate
152
+ * sibling for the same value-descending shape that wants a code-unit tiebreak
133
153
  * instead of localeCompare, and unlike these two it sorts its input array in place. The one
134
154
  * deliberate exception to both orderings is ui/gui/gui-client-report-chart.ts's reportRoleOrder,
135
155
  * which keeps its own copy by design: it runs in the browser, where harness modules cannot be
@@ -143,9 +163,9 @@ export function rankByCount(items, count, key) {
143
163
  * aliases `items` when it already fits; `hidden` is `max(0, items.length - top)`, which the
144
164
  * slice-length arithmetic yields without the explicit clamp (a slice never returns more than
145
165
  * `top` even when the list is shorter). Shared by the merge-intent and conflict-hunk caps
146
- * (merge-git.ts), the failure digest's loss-cause and message-cluster tops
147
- * (time-spend.ts, failure-cluster.ts), and the answered-questions cap (question-answer.ts),
148
- * so those cuts cannot drift on how the remainder is counted. */
166
+ * (merge-git.ts), the failure digest's loss-cause and message-cluster tops (failure-time-spend.ts
167
+ * and failure-cluster.ts), and the answered-questions cap (backlog-question-answer.ts), so those
168
+ * cuts cannot drift on how the remainder is counted. */
149
169
  export function topAndHidden(items, top) {
150
170
  const visible = items.slice(0, top);
151
171
  return { visible, hidden: items.length - visible.length };
@@ -159,17 +179,9 @@ export function lowerMedian(values) {
159
179
  const sorted = [...values].sort((a, b) => a - b);
160
180
  return sorted[Math.floor((sorted.length - 1) / 2)];
161
181
  }
162
- /** Most-days-waited-first comparator, ties in ascending index (file order) — `b.days - a.days ||
163
- * a.index - b.index`, the single home of that one-line ordering. Shared by the stale-questions
164
- * feed (src/ui/alert-feeds.ts's staleOpenQuestions), which builds `{index, days}`
165
- * rows off the same backlog scan, so its tie order cannot drift. The generic holds
166
- * any row type carrying both fields; a row without a natural index passes its first-seen order. */
167
- export function byDaysThenIndex(a, b) {
168
- return b.days - a.days || a.index - b.index;
169
- }
170
182
  /** Ascending string comparator by UTF-16 code unit — the locale-independent order a bare `.sort()`
171
183
  * uses, unlike `localeCompare` (which `rankByCount` uses for its tiebreak). Used directly by
172
- * src/cli/cli-start-summary.ts, src/cli/completion.ts, src/cli/log-commands.ts,
184
+ * src/cli/cli-start-summary.ts, src/cli/completion.ts, src/cli/cli-log-commands.ts,
173
185
  * src/history/history-data.ts, src/review/post-merge-review.ts, src/review/quarantine-render.ts,
174
186
  * src/budget/budget.ts and src/change/change-data.ts, and by `sortByValueDesc` for its tiebreak. */
175
187
  export function compareCodeUnits(a, b) {
@@ -178,7 +190,7 @@ export function compareCodeUnits(a, b) {
178
190
  /** Rank `[key, value, …]` pairs by value descending, breaking ties in code-unit order
179
191
  * (compareCodeUnits — the locale-independent order, unlike rankByCount's localeCompare), so
180
192
  * equal values always order the same way. The single home of that comparator, which
181
- * src/cli/log-commands.ts's per-type event-count lines and src/report/report-render.ts's
193
+ * src/cli/cli-log-commands.ts's per-type event-count lines and src/report/report-render.ts's
182
194
  * mergeDone sort hand-rolled before; `…rest` lets a site carry extra trailing fields
183
195
  * (report-render's third column) through the sort untouched. Mutates and returns the input
184
196
  * array, matching the `.sort()` the call sites replaced. */
@@ -200,7 +212,7 @@ export function sortEntriesByKey(entries) {
200
212
  return [...entries].sort(([a], [b]) => (a ?? "").localeCompare(b ?? ""));
201
213
  }
202
214
  /** rankByCount for `[key, count]` entry pairs (count first): src/report/report-render.ts's
203
- * rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick, and
215
+ * rankedRoleMap, src/team/team-failure-spread.ts's strongest-kind pick, and
204
216
  * src/doctor/doctor-contention-checks.ts's check-wait and provider-wait rankings. */
205
217
  export function rankCountEntries(entries) {
206
218
  return rankByCount(entries, (e) => e[1], (e) => e[0]);
@@ -0,0 +1,70 @@
1
+ import path from "node:path";
2
+ import { classifyLock, releaseOwnedLockDir, tryClaimLockDir } from "./lock.js";
3
+ import { ensureParentDir, isDirectory, removeTree, walkUp } from "../files/files.js";
4
+ /** The host-wide suite-slot ledger that bounds how many project check scripts (an agent's bare
5
+ * `npm run test:gate` and the harness's own spawned check alike) run their suite step at once,
6
+ * sized to the same `config.maxConcurrentChecks` the in-process check permit enforces. Where
7
+ * withCheckPermit is one process-wide semaphore (every suite spawned by THIS process shares it),
8
+ * the ledger is files on disk so suites started by DIFFERENT processes — the harness and the pi
9
+ * agent's bash tools, or two agents at once — still contend for the same cap. It exists to close
10
+ * the gap where an agent's own suite run takes no team check permit at all (BUGS.md 2026-10-28),
11
+ * so the two mechanisms coexist: withCheckPermit keeps its in-process tier queue, reentrancy and
12
+ * callers exactly as they are, and the npm check scripts additionally claim a slot here.
13
+ *
14
+ * A slot is a lock dir under `.tumwater/state/check-permits/slot-<n>` using lock.ts's directory-
15
+ * and-pid protocol unchanged (tryClaimLockDir / releaseOwnedLockDir / classifyLock), so a killed
16
+ * suite's slot is reclaimed by lock.ts's stale rules (dead pid, no-pid grace, then age) on a later
17
+ * claim — no separate sweep. A waiter gives up after a bounded wait and runs the suite anyway with
18
+ * a warning rather than hanging past the check deadline or the bash tool's 600 s cap: the ledger
19
+ * is a scheduling throttle, not a correctness mutex, so a full ledger must degrade to "run
20
+ * anyway", never to "hang forever". */
21
+ /** How long acquireCheckSlot keeps trying for a free slot before giving up and returning null (the
22
+ * callers then run the suite unpermitted with a warning). Bounded well under the bash tool's 600 s
23
+ * cap so a full ledger never prices a run's own timeout. */
24
+ const CHECK_SLOT_WAIT_TIMEOUT_MS = 60_000;
25
+ /** The retry cadence between full passes over a full ledger — a slot frees quickly (a suite's
26
+ * release is the moment its check process exits), so a 200 ms repoll finds it almost immediately
27
+ * without hammering the filesystem. */
28
+ const CHECK_SLOT_RETRY_MS = 200;
29
+ /** The ancestor of `startDir` that holds the team's `.tumwater/state`, or null when none within the
30
+ * walk's bound does. The slot ledger lives under that state dir, so `scripts/check-permit.mjs`
31
+ * locates the root the same way the harness's own root lookup does (shared walkUp climb), and a
32
+ * suite run outside any team root has no ledger to contend for. Exported for tests. */
33
+ export function findTeamRoot(startDir = process.cwd()) {
34
+ return walkUp(startDir, 63, (dir) => (isDirectory(path.join(dir, ".tumwater", "state")) ? dir : null));
35
+ }
36
+ /** The slot directory for `index` of a ledger rooted at `root` (under `root/.tumwater/state`). One
37
+ * function owns the path so the acquirer, the releaser and the reclaim all name the same dir. */
38
+ export function checkSlotDir(root, index) {
39
+ return path.join(root, ".tumwater", "state", "check-permits", `slot-${index}`);
40
+ }
41
+ /** Claim one of the `cap` slots of `root`'s ledger, waiting up to `waitMs` for one to free. Walks
42
+ * slots `0..cap-1`, breaking any it finds lock.ts's classifyLock calls "stale" (a dead holder, a
43
+ * leftover no-pid dir past its grace, or an old dir), then trying to claim each by atomic publish;
44
+ * when none is free it sleeps and retries until the budget is spent, returning null. A stale slot
45
+ * is broken as encountered because a dead holder's dir would otherwise block that index forever. */
46
+ export async function acquireCheckSlot(root, cap, waitMs = CHECK_SLOT_WAIT_TIMEOUT_MS) {
47
+ // The ledger's parent must exist before any slot's atomic publish can create it: lock.ts's
48
+ // publishLockDir never makes its own parent (withStateLock is where ensureParentDir runs), so a
49
+ // first claim on a root with no check-permits dir would fail its mkdirSync and never acquire.
50
+ ensureParentDir(checkSlotDir(root, 0));
51
+ const startedAt = Date.now();
52
+ for (;;) {
53
+ for (let index = 0; index < cap; index++) {
54
+ const dir = checkSlotDir(root, index);
55
+ if (classifyLock(dir) === "stale")
56
+ removeTree(dir);
57
+ if (tryClaimLockDir(dir))
58
+ return dir;
59
+ }
60
+ if (Date.now() - startedAt >= waitMs)
61
+ return null;
62
+ await new Promise((resolve) => setTimeout(resolve, CHECK_SLOT_RETRY_MS));
63
+ }
64
+ }
65
+ /** Return a slot claimed by THIS process. Deleting a dir that another pid now owns must not
66
+ * happen (releaseOwnedLockDir checks the pid first); a slot stolen from under us stays for its
67
+ * new owner or for the stale rules. */
68
+ export function releaseCheckSlot(dir) {
69
+ releaseOwnedLockDir(dir);
70
+ }