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
@@ -59,16 +59,11 @@ function screenOperableKey(root, key) {
59
59
  fail(fieldError);
60
60
  }
61
61
  else if (parsed.kind === "map") {
62
- // Only role-keyed maps check the role catalog. `maxConcurrentPerProvider.<provider>` is
63
- // keyed by provider, not a role id, and `config set` writes it without the role check —
64
- // so get/unset must agree, or set would succeed while get/unset refused the provider as
65
- // an "unknown role". A typo'd role id in the other two maps is refused the same way set's
62
+ // Role-keyed maps check the role catalog: a typo'd role id is refused the way set's
66
63
  // validator treats it.
67
- if (parsed.map !== "maxConcurrentPerProvider") {
68
- const ids = knownRoleIds(config);
69
- if (!ids.includes(parsed.role))
70
- fail(unknownRoleMessage(parsed.role, ids));
71
- }
64
+ const ids = knownRoleIds(config);
65
+ if (!ids.includes(parsed.role))
66
+ fail(unknownRoleMessage(parsed.role, ids));
72
67
  }
73
68
  return { parsed, config };
74
69
  }
@@ -89,72 +84,74 @@ function resolvedConfigValue(config, parsed, key) {
89
84
  ? record.roles?.[parsed.id]?.[parsed.field]
90
85
  : record[canonicalConfigKey(key)];
91
86
  }
92
- /** The `tumwater config` command body: bare prints the resolved config; `get` and `unset`
93
- * screen their key through screenOperableKey, while `set` writes through setConfigKey. */
94
- export async function cmdConfig(root, args = []) {
95
- const [sub, key, ...rest] = args;
96
- if (sub === "get") {
97
- const { parsed, config } = screenOperableKey(root, key ?? "");
98
- const value = resolvedConfigValue(config, parsed, key ?? "");
99
- sayJsonLine(value === undefined ? null : value); // Absent optional key → JSON null.
100
- return;
87
+ /** The `config get` verb: screen the key through screenOperableKey, then print its resolved
88
+ * value (defaults merged in) as JSON, or JSON null when nothing sets it. */
89
+ async function configGet(root, key) {
90
+ const { parsed, config } = screenOperableKey(root, key ?? "");
91
+ const value = resolvedConfigValue(config, parsed, key ?? "");
92
+ sayJsonLine(value === undefined ? null : value); // Absent optional key → JSON null.
93
+ }
94
+ /** The `config unset` verb: screen the key, remove it, and confirm what, if anything, was
95
+ * removed (or what the key now resolves to). */
96
+ async function configUnset(root, key) {
97
+ // cli.ts's arity gate requires exactly two args (`unset <key>`) before cmdConfig runs;
98
+ // this is the defensive tail for a direct caller, matching the bare-config tail below.
99
+ if (key === undefined)
100
+ fail(CONFIG_USAGE);
101
+ const k = canonicalConfigKey(key);
102
+ // The same screens get runs first, so a typo'd top-level key, role id, or role field
103
+ // fails identically — unsetConfigKey's own parse/field checks are the mutation-side
104
+ // backstop, not the wording the operator sees.
105
+ const { parsed } = screenOperableKey(root, k);
106
+ const result = unsetConfigKey(root, k);
107
+ if (!result.ok)
108
+ fail(result.error);
109
+ // The reload only feeds the message, so a failed one leaves `now` undefined.
110
+ const reloaded = loadConfigSafe(root).config;
111
+ const now = reloaded === undefined ? undefined : resolvedConfigValue(reloaded, parsed, k);
112
+ const resolves = now === undefined ? "" : JSON.stringify(now);
113
+ if (result.oldValue === undefined) {
114
+ say(`${k} is not set in tumwater.json, so nothing was removed` +
115
+ (now === undefined ? "" : ` (it resolves to ${resolves})`));
101
116
  }
102
- if (sub === "unset") {
103
- // cli.ts's arity gate requires exactly two args (`unset <key>`) before cmdConfig runs;
104
- // this is the defensive tail for a direct caller, matching the bare-config tail below.
105
- if (key === undefined)
106
- fail(CONFIG_USAGE);
107
- const k = canonicalConfigKey(key);
108
- // The same screens get runs first, so a typo'd top-level key, role id, or role field
109
- // fails identically — unsetConfigKey's own parse/field checks are the mutation-side
110
- // backstop, not the wording the operator sees.
111
- const { parsed } = screenOperableKey(root, k);
112
- const result = unsetConfigKey(root, k);
113
- if (!result.ok)
114
- fail(result.error);
115
- // The reload only feeds the message, so a failed one leaves `now` undefined.
116
- const reloaded = loadConfigSafe(root).config;
117
- const now = reloaded === undefined ? undefined : resolvedConfigValue(reloaded, parsed, k);
118
- const resolves = now === undefined ? "" : JSON.stringify(now);
119
- if (result.oldValue === undefined) {
120
- say(`${k} is not set in tumwater.json, so nothing was removed` +
121
- (now === undefined ? "" : ` (it resolves to ${resolves})`));
122
- }
123
- else {
124
- const was = JSON.stringify(result.oldValue);
125
- say(`unset ${k} (was ${was}${now === undefined ? "" : `; now resolves to ${resolves}`})`);
126
- }
127
- return;
117
+ else {
118
+ const was = JSON.stringify(result.oldValue);
119
+ say(`unset ${k} (was ${was}${now === undefined ? "" : `; now resolves to ${resolves}`})`);
128
120
  }
129
- if (sub === "set") {
130
- // A flag-looking value is a dropped `--` in disguise: `config set model --json` would
131
- // otherwise take the literal string "--json" as the value (the JSON-parse fallback
132
- // swallows any token) and validateConfig's string keys accept it silently — the same
133
- // shape gui/gui-command.ts's --token gate refuses, where the value the next flag holds is never
134
- // what the operator meant. Refuse before setConfigKey reads it; a value that really
135
- // does begin with `--` has no legitimate shape among the config keys' values
136
- // (identifiers, durations, windows, JSON scalars and arrays), so nothing is lost.
137
- const raw = rest[0] ?? "";
138
- if (raw.startsWith("--"))
139
- fail(`config set got the flag-looking value ${JSON.stringify(raw)} instead of a value — is a flag missing its value, or did a token slip into the value's position? (e.g. \`tumwater config set model gpt-5\`)`);
140
- const result = setConfigKey(root, key ?? "", raw);
141
- if (!result.ok)
142
- fail(result.error);
143
- if (result.unchanged) {
144
- say(`${canonicalConfigKey(key ?? "")} is already ${JSON.stringify(result.value)}, so tumwater.json was not written`);
145
- return;
146
- }
147
- say(`set ${canonicalConfigKey(key ?? "")} to ${JSON.stringify(result.value)}`);
148
- const note = result.note;
149
- if (note)
150
- say(`note: ${note}`);
151
- // A bare `roles` set replaces the FILE's section, but the built-in catalog always merges
152
- // per entry at load, so the replacement cannot disable a role by omission. Say so here
153
- // rather than let the confirmation imply the role is gone.
154
- if (key === "roles")
155
- say("note: the built-in role catalog still merges per entry — disable a role with `tumwater config set roles.<id>.enabled false`, not by omitting it");
121
+ }
122
+ /** The `config set` verb: write one value through setConfigKey (refusing a flag-looking
123
+ * value first) and confirm the write or that it was already that value. */
124
+ async function configSet(root, key, rest) {
125
+ // A flag-looking value is a dropped `--` in disguise: `config set model --json` would
126
+ // otherwise take the literal string "--json" as the value (the JSON-parse fallback
127
+ // swallows any token) and validateConfig's string keys accept it silently — the same
128
+ // shape gui/gui-command.ts's --token gate refuses, where the value the next flag holds is never
129
+ // what the operator meant. Refuse before setConfigKey reads it; a value that really
130
+ // does begin with `--` has no legitimate shape among the config keys' values
131
+ // (identifiers, durations, windows, JSON scalars and arrays), so nothing is lost.
132
+ const raw = rest[0] ?? "";
133
+ if (raw.startsWith("--"))
134
+ fail(`config set got the flag-looking value ${JSON.stringify(raw)} instead of a value — is a flag missing its value, or did a token slip into the value's position? (e.g. \`tumwater config set model gpt-5\`)`);
135
+ const result = setConfigKey(root, key ?? "", raw);
136
+ if (!result.ok)
137
+ fail(result.error);
138
+ if (result.unchanged) {
139
+ say(`${canonicalConfigKey(key ?? "")} is already ${JSON.stringify(result.value)}, so tumwater.json was not written`);
156
140
  return;
157
141
  }
142
+ say(`set ${canonicalConfigKey(key ?? "")} to ${JSON.stringify(result.value)}`);
143
+ const note = result.note;
144
+ if (note)
145
+ say(`note: ${note}`);
146
+ // A bare `roles` set replaces the FILE's section, but the built-in catalog always merges
147
+ // per entry at load, so the replacement cannot disable a role by omission. Say so here
148
+ // rather than let the confirmation imply the role is gone.
149
+ if (key === "roles")
150
+ say("note: the built-in role catalog still merges per entry — disable a role with `tumwater config set roles.<id>.enabled false`, not by omitting it");
151
+ }
152
+ /** The bare-config tail: the whole resolved config as pretty JSON, refusing a surplus
153
+ * subcommand the arity gate somehow missed. */
154
+ async function configDump(root, sub) {
158
155
  // Bare config, or anything else: the whole-config dump is the default, and cli.ts's arity
159
156
  // check has already rejected a malformed get/set — this usage line is the defensive tail.
160
157
  if (sub !== undefined)
@@ -164,3 +161,17 @@ export async function cmdConfig(root, args = []) {
164
161
  fail(error); // validateConfig's message, via the standard fail()
165
162
  sayJson(config);
166
163
  }
164
+ /** The `tumwater config` command body: bare prints the resolved config; `get` and `unset`
165
+ * screen their key through screenOperableKey, while `set` writes through setConfigKey. Each
166
+ * verb's guards and confirmation wording live in one named helper — configGet, configUnset,
167
+ * configSet, configDump — that this dispatcher delegates to. */
168
+ export async function cmdConfig(root, args = []) {
169
+ const [sub, key, ...rest] = args;
170
+ if (sub === "get")
171
+ return configGet(root, key);
172
+ if (sub === "unset")
173
+ return configUnset(root, key);
174
+ if (sub === "set")
175
+ return configSet(root, key, rest);
176
+ return configDump(root, sub);
177
+ }
@@ -0,0 +1,103 @@
1
+ import { rejectUnknownArgs, JSON_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG, } from "./cli-flag-specs.js";
2
+ import { say, sayJsonOrRender } from "./cli-output.js";
3
+ import { parseDoctorChecks, peelPositionals } from "./cli-command-args.js";
4
+ import { loadConfigSafe } from "../config/config.js";
5
+ /** Run one doctor side-effect (`--kill-orphans`, `--commit-backlog`, `--resync-install`,
6
+ * `--drop-broken-pins`, `--retire-leftovers`) and return its report payload for the optional
7
+ * `--json` run — the shape every doctor*Case side-effect wrapper follows: import the effect's
8
+ * module and run it only when its flag is present, then (text form only) print the effect's
9
+ * own lines before the report with a separating blank line when there is anything to print;
10
+ * `--json` keeps stdout one document, carrying the result on the payload instead. The lazy
11
+ * per-effect `load` keeps each module imported only when its flag is asked for. */
12
+ async function doctorSideEffect(args, flag, load) {
13
+ if (!args.includes(flag))
14
+ return undefined;
15
+ const { run, lines } = await load();
16
+ const payload = await run();
17
+ // The text form prints the effect before the report; --json keeps stdout one document.
18
+ if (!args.includes("--json")) {
19
+ const out = lines(payload);
20
+ for (const line of out)
21
+ say(line);
22
+ if (out.length > 0)
23
+ say("");
24
+ }
25
+ return payload;
26
+ }
27
+ async function doctorKillOrphans(root, args) {
28
+ return doctorSideEffect(args, "--kill-orphans", async () => {
29
+ const { killOrphans, orphanKillLines } = await import("../doctor/doctor-orphans.js");
30
+ return { run: () => killOrphans(root), lines: orphanKillLines };
31
+ });
32
+ }
33
+ async function doctorCommitBacklog(root, args) {
34
+ return doctorSideEffect(args, "--commit-backlog", async () => {
35
+ const { commitUncommittedBacklog, backlogCommitLines } = await import("../doctor/doctor-backlog.js");
36
+ const { config } = loadConfigSafe(root); // the report's own config, so base resolution and the off-base skip match the `uncommitted backlog` row's
37
+ return { run: () => commitUncommittedBacklog(root, config), lines: backlogCommitLines };
38
+ });
39
+ }
40
+ async function doctorResyncInstall(root, args) {
41
+ return doctorSideEffect(args, "--resync-install", async () => {
42
+ const { resyncRootInstall, rootInstallResyncLines } = await import("../build/dep-install.js");
43
+ return { run: () => resyncRootInstall(root), lines: rootInstallResyncLines };
44
+ });
45
+ }
46
+ async function doctorDropBrokenPins(root, args) {
47
+ return doctorSideEffect(args, "--drop-broken-pins", async () => {
48
+ const { dropBrokenPins, brokenPinDropLines } = await import("../doctor/doctor-contention-checks.js");
49
+ return { run: () => dropBrokenPins(root), lines: brokenPinDropLines };
50
+ });
51
+ }
52
+ async function doctorRetireLeftovers(root, args) {
53
+ return doctorSideEffect(args, "--retire-leftovers", async () => {
54
+ const { retireLeftovers, leftoverRetireLines } = await import("../doctor/doctor-contention-checks.js");
55
+ const { config } = loadConfigSafe(root); // the row's own config, so the reap and the report agree on what is retirable
56
+ return { run: () => retireLeftovers(root, config), lines: leftoverRetireLines };
57
+ });
58
+ }
59
+ function doctorPayload(payload, parts) {
60
+ const out = { ...payload };
61
+ if (parts.leftoverRetire !== undefined)
62
+ out.leftoverRetire = parts.leftoverRetire;
63
+ if (parts.brokenPinDrop !== undefined)
64
+ out.brokenPinDrop = parts.brokenPinDrop;
65
+ if (parts.resyncInstall !== undefined)
66
+ out.resyncInstall = parts.resyncInstall;
67
+ if (parts.backlogCommit !== undefined)
68
+ out.backlogCommit = parts.backlogCommit;
69
+ if (parts.orphanKill !== undefined)
70
+ out.orphanKill = parts.orphanKill;
71
+ return out;
72
+ }
73
+ /** `tumwater doctor`: report what in the environment is keeping the team from running, and
74
+ * run the optional repair side-effects. No ready-repo gate: doctor's job is to report WHY the
75
+ * environment isn't ready, so it must run outside a git repo and print fail lines rather than
76
+ * throwing. */
77
+ export async function cmdDoctor(root, args) {
78
+ const { positionals: checkNames, rest: doctorFlags } = peelPositionals(args);
79
+ rejectUnknownArgs("doctor", doctorFlags, [JSON_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG]);
80
+ const only = parseDoctorChecks(checkNames);
81
+ const { runDoctor } = await import("../doctor/doctor.js");
82
+ const parts = {
83
+ orphanKill: await doctorKillOrphans(root, args),
84
+ backlogCommit: await doctorCommitBacklog(root, args),
85
+ resyncInstall: await doctorResyncInstall(root, args),
86
+ brokenPinDrop: await doctorDropBrokenPins(root, args),
87
+ leftoverRetire: await doctorRetireLeftovers(root, args),
88
+ };
89
+ const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("../doctor/doctor-render.js");
90
+ const report = await runDoctor(root, undefined, undefined, only);
91
+ // --json prints the collector's own payload (the DoctorReport object), not a re-parse of
92
+ // the render — the `report --json` precedent. The exit-code contract below holds in both
93
+ // forms: 1 when any check fails, 0 otherwise; warnings never fail the exit.
94
+ // --problems keeps the full report for the render (it counts the hidden ok rows) and
95
+ // hands --json the filtered one; the exit code below reads the full report either way.
96
+ // --kill-orphans adds the kill result to the JSON object as `orphanKill`.
97
+ if (args.includes("--problems"))
98
+ sayJsonOrRender(args, args.includes("--json") ? doctorPayload(problemsOnly(report), parts) : report, renderDoctorProblems);
99
+ else
100
+ sayJsonOrRender(args, args.includes("--json") ? doctorPayload(report, parts) : report, renderDoctor);
101
+ if (report.checks.some((c) => c.level === "fail"))
102
+ process.exitCode = 1; // Warnings never fail the exit.
103
+ }
@@ -465,35 +465,55 @@ export const GUI_FLAG_SPECS = [PORT_FLAG, ALL_INTERFACES_FLAG, TOKEN_FLAG];
465
465
  export function rejectUnknownArgs(command, args, specs) {
466
466
  if (args.length === 0)
467
467
  return;
468
- const claim = new Map();
469
- for (const spec of specs)
470
- for (const name of spec.names)
471
- claim.set(name, spec);
468
+ const claim = flagClaims(specs);
472
469
  const seen = new Set();
473
470
  const consumed = new Array(args.length).fill(false);
474
471
  for (let i = 0; i < args.length; i++) {
475
472
  if (consumed[i])
476
473
  continue;
477
474
  const arg = args[i] ?? ""; // Unreachable fallback: the loop bound guarantees a token here.
478
- const spec = claim.get(arg);
479
- if (spec === undefined) {
480
- // An equals-form token names a real flag; refuse it with its own message before the
481
- // generic "unknown argument" misreports `--role=feature` as a misspelling.
482
- rejectEqualsForm(arg, specs);
483
- if (specs.length === 0)
484
- fail(`tumwater ${command} takes no arguments${gotSuffix(arg)}`);
485
- failUnknownArgument(command, arg, specs);
486
- }
475
+ const spec = claim.get(arg) ?? rejectUnknownFlag(command, arg, specs);
487
476
  rejectRepeatFlag(seen.has(spec), spec.names[0]);
488
477
  seen.add(spec);
489
- if (spec.value && i === args.length - 1)
490
- fail(spec.missingValue ?? `${spec.names[0]} needs a value`);
491
- // The value exists (the trailing-no-value case failed above): name a malformed one now,
492
- // before the ready-repo gate or any environment check can mask it.
493
- if (spec.value)
494
- spec.validate?.(args[i + 1] ?? "");
495
- const n = spec.value ? 2 : 1;
496
- for (let j = 0; j < n && i + j < args.length; j++)
497
- consumed[i + j] = true;
478
+ consumeFlagOccurrence(spec, args, i, consumed);
498
479
  }
499
480
  }
481
+ /** The spelling-to-spec claim map rejectUnknownArgs keyed its loop on: every accepted flag
482
+ * spelling maps to the spec declaring it, alias spellings (-f and --follow) sharing one spec
483
+ * entry. Built once up front so a token look-up is a map get, not a scan. */
484
+ function flagClaims(specs) {
485
+ const claim = new Map();
486
+ for (const spec of specs)
487
+ for (const name of spec.names)
488
+ claim.set(name, spec);
489
+ return claim;
490
+ }
491
+ /** The one refusal for a token naming no known flag: an equals-form token first reroutes
492
+ * through rejectEqualsForm, which names a REAL flag's offending `--flag=value` spelling
493
+ * before the generic "unknown argument" misreports `--role=feature` as a misspelling; then
494
+ * a command with no flags at all fails with the takes-no-arguments wording; otherwise the
495
+ * shared unknown-argument wording (with its did-you-mean hint) fires. Returns never, so the
496
+ * caller's `claim.get(arg) ?? rejectUnknownFlag(...)` narrows `spec` to a known FlagSpec. */
497
+ function rejectUnknownFlag(command, arg, specs) {
498
+ // An equals-form token names a real flag; refuse it with its own message before the
499
+ // generic "unknown argument" misreports `--role=feature` as a misspelling.
500
+ rejectEqualsForm(arg, specs);
501
+ if (specs.length === 0)
502
+ fail(`tumwater ${command} takes no arguments${gotSuffix(arg)}`);
503
+ failUnknownArgument(command, arg, specs);
504
+ }
505
+ /** Consume one recognised flag occurrence: fail a valued flag left without its value, name a
506
+ * malformed value via the spec's validate call, and mark the one or two tokens (the flag and,
507
+ * for a valued flag, its value) as consumed so a later loop iteration skips them. The value
508
+ * is marked only after the value checks pass, so a failed value is never silently swallowed. */
509
+ function consumeFlagOccurrence(spec, args, i, consumed) {
510
+ if (spec.value && i === args.length - 1)
511
+ fail(spec.missingValue ?? `${spec.names[0]} needs a value`);
512
+ // The value exists (the trailing-no-value case failed above): name a malformed one now,
513
+ // before the ready-repo gate or any environment check can mask it.
514
+ if (spec.value)
515
+ spec.validate?.(args[i + 1] ?? "");
516
+ const n = spec.value ? 2 : 1;
517
+ for (let j = 0; j < n && i + j < args.length; j++)
518
+ consumed[i + j] = true;
519
+ }
@@ -1,7 +1,7 @@
1
1
  /** `tumwater init`'s implementation (src/init/init.ts does the work; this prints the report).
2
2
  * Split out of cli/cli-start.ts, whose name is for start alone: every command it dispatches
3
- * delegates to its own module (operator/operator-commands.ts, config-commands.ts, log-commands.ts,
4
- * doctor.ts, …), and init is no exception. cli.ts stays the pure dispatcher. */
3
+ * delegates to its own module (cli-config-commands.ts, cli-log-commands.ts, doctor.ts, …), and init
4
+ * is no exception. cli.ts stays the pure dispatcher. */
5
5
  import { say } from "./cli-output.js";
6
6
  import { parseInitArgs } from "./cli-command-args.js";
7
7
  import { initProject } from "../init/init.js";
@@ -12,8 +12,7 @@ import { templateCatalog } from "../init/init-templates.js";
12
12
  export async function cmdInit(root, args) {
13
13
  const { prompt, branch, template, listTemplates, adopt, dryRun } = parseInitArgs(args);
14
14
  if (listTemplates) {
15
- for (const { id, description } of templateCatalog())
16
- say(`${id} — ${description}`);
15
+ printTemplateCatalog();
17
16
  return;
18
17
  }
19
18
  const result = await initProject(root, prompt, branch ?? undefined, {
@@ -21,21 +20,44 @@ export async function cmdInit(root, args) {
21
20
  dryRun,
22
21
  template: template ?? undefined,
23
22
  });
23
+ printInitIntro(result);
24
+ if (result.dryRun) {
25
+ printDryRun(result);
26
+ return;
27
+ }
28
+ printInitReport(result);
29
+ }
30
+ /** Print the `tumwater init --list-templates` catalog: one line per template. */
31
+ function printTemplateCatalog() {
32
+ for (const { id, description } of templateCatalog())
33
+ say(`${id} — ${description}`);
34
+ }
35
+ /** Print the `template:`/adopt intro lines shared by the dry-run and completed paths. */
36
+ function printInitIntro(result) {
24
37
  if (result.template !== "blank")
25
38
  say(`template: ${result.template}`);
26
39
  if (result.adopted) {
27
40
  say(`${result.dryRun ? "would adopt" : "adopting"} an existing repo: the project brief goes in TUMWATER.md and README.md is left untouched`);
28
41
  }
29
- if (result.dryRun) {
30
- if (result.repoInitialized && result.branch) {
31
- say(`dry run — would initialize a new git repository on branch ${result.branch}`);
32
- }
33
- const list = (names) => (names.length > 0 ? names.join(", ") : "nothing");
34
- say(`dry run — would create: ${list(result.created)}`);
35
- say(`would leave alone: ${list(result.leftAlone)}`);
36
- say("nothing written; re-run without --dry-run to apply");
37
- return;
42
+ }
43
+ /** Print the dry-run preview: repo line, the would-create/would-leave-alone lists, and the
44
+ * "nothing written" line. */
45
+ function printDryRun(result) {
46
+ if (result.repoInitialized && result.branch) {
47
+ say(`dry run — would initialize a new git repository on branch ${result.branch}`);
38
48
  }
49
+ const list = (names) => (names.length > 0 ? names.join(", ") : "nothing");
50
+ say(`dry run — would create: ${list(result.created)}`);
51
+ say(`would leave alone: ${list(result.leftAlone)}`);
52
+ say("nothing written; re-run without --dry-run to apply");
53
+ }
54
+ /** Format the created-files line, appending `(committed)` when the repo was seeded. */
55
+ function createdLine(result) {
56
+ return `created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`;
57
+ }
58
+ /** Print the completed-init report: the already-initialized no-op, the repo line, the
59
+ * created-files line, the bootstrap note, and the "next:" line. */
60
+ function printInitReport(result) {
39
61
  if (result.created.length === 0) {
40
62
  say("already initialized; nothing to do");
41
63
  return;
@@ -43,7 +65,7 @@ export async function cmdInit(root, args) {
43
65
  if (result.repoInitialized && result.branch) {
44
66
  say(`initialized a new git repository on branch ${result.branch}`);
45
67
  }
46
- say(`created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`);
68
+ say(createdLine(result));
47
69
  if (result.bootstrap !== null) {
48
70
  say(`fresh project — maintenance roles wait until ${result.bootstrap} plans are done; remove "bootstrap" from tumwater.json to end that early`);
49
71
  }
@@ -103,6 +103,11 @@ export function followUntilParentGone(file, offset, onLines, seams) {
103
103
  });
104
104
  });
105
105
  }
106
+ /** The `-n` ceiling for the event-log dump (`tumwater logs -n <count>`): a terminal tail has
107
+ * no use for more than this many scanned events, and bounding it keeps an accidental
108
+ * `-n 999999999` from traversing the whole event file. Sister counts are bounded the same way
109
+ * (history's HISTORY_MAX_RUNS, report's REPORT_MAX_DAYS); the GUI clamps its logs counts too. */
110
+ const LOGS_MAX_EVENTS = 100_000;
106
111
  /** `tumwater logs [-f] [-n <count>] [--since <duration>] [--until <duration>]
107
112
  * [--role <id>[,<id>...]] [--transcript <id>] [--prompt]
108
113
  * [--grep <text>]`: follow or dump the harness event log, with `--since` a bounded
@@ -126,7 +131,7 @@ export async function cmdLogs(root, args, seams = {}) {
126
131
  return;
127
132
  }
128
133
  const nRaw = flagValue(feed.rest, "-n");
129
- const limit = nRaw !== null ? parseCountFlag("-n", nRaw) : 50;
134
+ const limit = nRaw !== null ? parseCountFlag("-n", nRaw, LOGS_MAX_EVENTS) : 50;
130
135
  // The event feed (no --transcript) never needs the config at all: parseRoleScope reads it only
131
136
  // when the flag is present.
132
137
  const role = parseRoleScope(root, feed.rest, "--transcript", LOGS_TRANSCRIPT_VALUE_ERROR);
@@ -278,39 +283,54 @@ function sayLogsWindowNoMatch(feed, windowPhrase) {
278
283
  else
279
284
  say(feed.roles !== null ? `${feed.roleEmpty} ${windowPhrase}` : `no events ${windowPhrase}`);
280
285
  }
281
- /** The `-n` tail view and its follow: `readEventsTailWithEnd`, the `shown` filter, the
282
- * `--count` branch and its empty-match prose, the per-event print, the `!follow` return, and
283
- * `followUntilParentGone` seeded from `tail.coveredEnd`. It owns the follow-loop locals. */
286
+ /** The `-n` tail view and its `-f` follow: read the filtered tail, print the `--count`
287
+ * tally or the per-event rows, and seed the follow; each job lives in a named helper. */
284
288
  async function sayLogsTail(root, feed, limit, seams) {
285
- // -n bounds the scan, not the print: the matched subset of the last `limit` events is what
286
- // appears, so `-n 200 --grep merge_failed` may print 3 rows from 200 scanned. The read is
287
- // the tail-with-end shape (not the per-poll cached readEvents) so the follow below can seed
288
- // from the byte end this very read covered instead of a later stat of the file.
289
- const tail = readEventsTailWithEnd(root, limit);
290
- const shown = feed.grepLower !== null || feed.type !== null || feed.roles !== null || feed.notable ? tail.events.filter(feed.filter) : tail.events;
291
- // `--count` over the -n view answers with the tally of exactly the filtered rows the plain
292
- // view would print, and carries over the plain view's empty-match prose below (a `--type`
293
- // or `--grep` that matches nothing names its value; an empty unfiltered -n view prints
294
- // nothing, as the plain view does). In JSON mode the empty output is the answer.
289
+ const { shown, coveredEnd } = readTailShown(root, feed, limit);
295
290
  if (feed.count) {
291
+ // `--count` over the -n view answers with the tally of exactly the filtered rows the plain
292
+ // view would print, and carries over the plain view's empty-match prose (a `--type` or
293
+ // `--grep` that matches nothing names its value; an empty unfiltered -n view prints
294
+ // nothing, as the plain view does). In JSON mode the empty output is the answer.
296
295
  if (shown.length !== 0)
297
296
  sayTally(shown, feed.json);
298
- else if (!feed.json && (feed.filtersGrepOrType || feed.roles !== null || feed.notable))
299
- sayLogsTailNoMatch(feed);
297
+ else
298
+ sayTailEmptyNote(shown, feed);
300
299
  return;
301
300
  }
302
301
  for (const e of shown)
303
302
  sayEventLine(e, feed.json);
304
303
  if (!feed.follow) {
305
- // The empty-match line is prose: in JSON mode the empty output is the answer. It fires
306
- // for either filter, naming the flag's value(s) whose match came up empty so the operator
307
- // knows what to loosen.
308
- if (shown.length === 0 && !feed.json && (feed.filtersGrepOrType || feed.roles !== null || feed.notable))
309
- sayLogsTailNoMatch(feed);
304
+ sayTailEmptyNote(shown, feed);
310
305
  return;
311
306
  }
307
+ await followLogsTail(root, feed, coveredEnd, seams);
308
+ }
309
+ /** How `-n` scans versus prints: `limit` bounds the scan, not the print — the matched
310
+ * subset of the last `limit` events is what appears, so `-n 200 --grep merge_failed` may
311
+ * print 3 rows from 200 scanned. The read is the tail-with-end shape (not the per-poll
312
+ * cached readEvents) so the follow can seed from the byte end this very read covered
313
+ * instead of a later stat of the file. It also owns the share of output to print: the
314
+ * `shown` rows in read order. */
315
+ function readTailShown(root, feed, limit) {
316
+ const tail = readEventsTailWithEnd(root, limit);
317
+ const shown = feed.grepLower !== null || feed.type !== null || feed.roles !== null || feed.notable ? tail.events.filter(feed.filter) : tail.events;
318
+ return { shown, coveredEnd: tail.coveredEnd };
319
+ }
320
+ /** The empty-match guard shared by the `--count` branch and the no-follow plain branch: the
321
+ * empty-match line is prose (in JSON mode the empty output is the answer) and fires for
322
+ * either filter, naming the flag's value(s) whose match came up empty so the operator knows
323
+ * what to loosen. */
324
+ function sayTailEmptyNote(shown, feed) {
325
+ if (shown.length === 0 && !feed.json && (feed.filtersGrepOrType || feed.roles !== null || feed.notable))
326
+ sayLogsTailNoMatch(feed);
327
+ }
328
+ /** The `-f` follow: resolve the event-log path and stream every newly landed event through
329
+ * the same `feed.filter` as the seeded window, seeded from `coveredEnd` (the byte end the
330
+ * initial read covered) and holding the filter across rotation. */
331
+ async function followLogsTail(root, feed, coveredEnd, seams) {
312
332
  const file = eventsLogPath(root);
313
- // Seed the follow from the byte end the initial read covered (tail.coveredEnd), mirroring
333
+ // Seed the follow from the byte end the initial read covered (coveredEnd), mirroring
314
334
  // cmdLogsTranscript's tail.end seed: an event appended between the read and the follow's
315
335
  // start lies past the printed window but at or above this offset, so the follow's first
316
336
  // poll delivers it — seeding from a fresh statOrNull(file).size instead would start past
@@ -318,7 +338,7 @@ async function sayLogsTail(root, feed, limit, seams) {
318
338
  // log scans to coveredEnd 0, and followFile tolerates a missing file (its poll re-stats
319
339
  // every interval), so a read-only command never leaves a harness state file behind — the
320
340
  // module contract above promises writes to stdout only.
321
- await followUntilParentGone(file, tail.coveredEnd, (lines) => {
341
+ await followUntilParentGone(file, coveredEnd, (lines) => {
322
342
  for (const line of lines.filter(Boolean)) {
323
343
  const e = parseEventLine(line);
324
344
  // The filter holds across rotation: every event the follow callback sees goes through
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { fail, say } from "./cli-output.js";
3
3
  import { loadConfig } from "../config/config.js";
4
4
  import { TUMWATER_SOURCE } from "../events/events.js";
5
- import { notifyCommandEnv, notifyExitGloss } from "../events/notify.js";
5
+ import { notifyCommandEnv, notifyExitGloss } from "../events/event-notify.js";
6
6
  /** The one representative allowlisted event `tumwater notify --test` fires: a failed merge
7
7
  * with source `tumwater`. Fixed and opinionated — the goal is to prove the hook runs, not to
8
8
  * replay a specific page, so there are no per-type or per-message flags. */
@@ -16,7 +16,7 @@ const NOTIFY_TEST_MESSAGE = "tumwater notify --test";
16
16
  * Generous enough not to misclassify a hook that is merely slow. */
17
17
  export const NOTIFY_TEST_TIMEOUT_MS = 10_000;
18
18
  /** Run one notify hook to completion, returning its exit code. Unlike the notifier's detached
19
- * fire-and-forget spawn (src/events/notify.ts), this waits with inherited stdio by
19
+ * fire-and-forget spawn (src/events/event-notify.ts), this waits with inherited stdio by
20
20
  * default (the dashboard passes "ignore"), so the
21
21
  * operator sees the hook's own output and its exit. A shell that cannot start resolves 127 —
22
22
  * the conventional "command not found" code. A hook that does not finish within `timeoutMs`
@@ -1,6 +1,6 @@
1
1
  /** The read-only query commands' bodies (status, diff, queue, backlog), split out of
2
2
  * cli.ts's dispatcher: main() routes to a cmd* here exactly as it routes to operator-commands's
3
- * cmdAbort or config-commands's cmdConfig, and cli.ts keeps only routing and the help/version
3
+ * cmdAbort or cli-config-commands's cmdConfig, and cli.ts keeps only routing and the help/version
4
4
  * surfaces. Each command owns its argument gate, ready-repo gating decision, and output shape,
5
5
  * with the wording and gating order moved verbatim from main()'s cases. */
6
6
  import { fail, say, sayJson, sayJsonOrRender } from "./cli-output.js";
@@ -25,7 +25,7 @@ import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.j
25
25
  import { mergeChanges } from "../merge/merge-slot.js";
26
26
  import { shortSha } from "../text/format.js";
27
27
  import { roleLabel } from "../roles/role-labels.js";
28
- import { orchestratorAlive } from "../team/orchestrator-info.js";
28
+ import { orchestratorAlive } from "../team/team-orchestrator-info.js";
29
29
  import { queueInFlight } from "../status/status-polls.js";
30
30
  import { roleInScope } from "../roles/runner-ids.js";
31
31
  /** `tumwater status [-f/--follow]`: the team snapshot as rendered text or as the JSON