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
@@ -1,8 +1,9 @@
1
1
  /** The CLI layer of `tumwater questions` — the `tumwater questions` command body: list the open
2
2
  * questions in QUESTIONS.md numbered, or `answer <n> <decision>` one, which moves its entry from
3
3
  * `## Open` to `## Answered` with a dated operator answer. The markdown surgery itself — the
4
- * fence-aware `### ` block cut and the `## Answered` skeleton — lives in src/backlog/
5
- * question-answer.ts (the shared QUESTIONS.md core the TUI and dashboard also call); this module
4
+ * fence-aware `### ` block cut and the `## Answered` skeleton — lives in
5
+ * src/backlog/backlog-question-answer.ts (the shared QUESTIONS.md core the TUI and dashboard also
6
+ * call); this module
6
7
  * is the command's argument scan, say/fail/editor jacket and commit-and-wake tail over that core,
7
8
  * routed to by src/cli.ts.
8
9
  */
@@ -17,8 +18,14 @@ import { typoSuffix } from "../text/suggest.js";
17
18
  import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
18
19
  import { stampedSubject } from "../git/commit-message.js";
19
20
  import { wakeQuestionAsker } from "../operator/operator-intent.js";
20
- import { answeredQuestionAt, answeredQuestionCount, answeredQuestionList, answeredQuestionPayload, openQuestionAt, openQuestionList, questionListPayload, renderShownQuestion, UNCLOSED_FENCE_MESSAGE, tryAnswerQuestion } from "../backlog/question-answer.js";
21
+ import { answeredQuestionAt, answeredQuestionCount, answeredQuestionList, answeredQuestionPayload, openQuestionAt, openQuestionList, questionListPayload, renderShownQuestion, UNCLOSED_FENCE_MESSAGE, tryAnswerQuestion } from "../backlog/backlog-question-answer.js";
21
22
  import { bodyStructureProblem, normalizeEntryBody } from "../backlog/backlog-write.js";
23
+ /** `questions --answered -n`'s ceiling: bounds how many answered questions one list shows,
24
+ * so a typo'd huge count fails fast with the shared `must be between 1 and <max>` wording
25
+ * (the logs/history/report pattern) instead of silently meaning "everything". The list data
26
+ * is small (QUESTIONS.md), so the cap never meaningfully binds — it exists for the uniform
27
+ * count-flag contract and the clear over-range rebuke, not for memory. */
28
+ const QUESTIONS_MAX_LISTED = 100_000;
22
29
  /** The `tumwater questions answer` entry point: `tryAnswerQuestion` without a title check, its
23
30
  * refusals mapped to `fail` (exit 1) with the CLI's wording. Returns the answered question's
24
31
  * title plus the collapsed decision that landed in the file. */
@@ -84,6 +91,72 @@ function sayQuestionList(root, json, pattern) {
84
91
  }
85
92
  /** `questions answer`'s `--file <path>` flag: the decision comes from the file, lines kept. */
86
93
  const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
94
+ /** The `--show`/`--answered`/`-n`/`--grep` arms — the flags only valid before the question's
95
+ * number positional. Each performs its read and writes its `scan.<field>` exactly as the
96
+ * inline scan did; returns `i` (the index of the last token consumed, after any `args[++i]`);
97
+ * every other token returns `undefined`. */
98
+ function readPreNumberFlag(scan, args, i) {
99
+ if (scan.numbered)
100
+ return undefined;
101
+ const arg = args[i];
102
+ if (arg === "--show") {
103
+ rejectRepeatFlag(scan.show !== undefined, "--show");
104
+ scan.show = parsePositionFlag("--show", args[++i], "--show 2");
105
+ return i;
106
+ }
107
+ if (arg === "--answered") {
108
+ rejectRepeatFlag(scan.answeredFlag, "--answered");
109
+ scan.answeredFlag = true;
110
+ return i;
111
+ }
112
+ if (arg === "-n") {
113
+ rejectRepeatFlag(scan.limit !== undefined, "-n");
114
+ scan.limit = parseCountFlag("-n", args[++i], QUESTIONS_MAX_LISTED);
115
+ return i;
116
+ }
117
+ if (arg === "--grep") {
118
+ rejectRepeatFlag(scan.grep !== undefined, "--grep");
119
+ const text = args[++i];
120
+ if (text === undefined || text === "")
121
+ fail("--grep needs text (usage: tumwater questions [--answered] --grep <text>)");
122
+ scan.grep = text;
123
+ return i;
124
+ }
125
+ return undefined;
126
+ }
127
+ /** The `--file` arm (valid anywhere), the `!numbered && --json` arm, and the unknown
128
+ * `--`-flag gate — in that order. Returns `i` when `--file`/`--json` consumed the token; the
129
+ * unknown-flag arm never returns (it `fail`s). */
130
+ function readAnywhereFlag(scan, args, i) {
131
+ const arg = args[i];
132
+ if (arg === "--file") {
133
+ rejectRepeatFlag(scan.file !== undefined, "--file");
134
+ scan.file = promptFileValue(args[++i]);
135
+ return i;
136
+ }
137
+ if (!scan.numbered && arg === "--json") {
138
+ rejectRepeatFlag(scan.jsonFlag, "--json");
139
+ scan.jsonFlag = true;
140
+ return i;
141
+ }
142
+ if (arg.startsWith("--") && arg !== "--json") {
143
+ rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
144
+ // The complete flag set (this gate only sees `--`-flags, but `-n` is a real flag the
145
+ // list should not omit) plus the same did-you-mean hint the sibling commands'
146
+ // unknown-argument gate (cli-flag-specs's failUnknownArgument) attaches, so a mistyped
147
+ // `--ansered` names `--answered` instead of only printing the valid list — the missing
148
+ // treatment the questions gate was the one unknown-argument caller without.
149
+ const spellings = ["--json", "--file", "--answered", "--show", "--grep", "-n"];
150
+ fail(`unknown argument: ${arg} (valid flags for tumwater questions: ${spellings.join(", ")})${typoSuffix(arg, spellings)}`);
151
+ }
152
+ return undefined;
153
+ }
154
+ /** The trailing `-n needs --answered` guard: `-n` limits the answered list, so without
155
+ * `--answered` it means nothing. */
156
+ function assertQuestionsFlags(scan) {
157
+ if (scan.limit !== undefined && !scan.answeredFlag)
158
+ fail("-n needs --answered (it limits the answered questions listed)");
159
+ }
87
160
  /** No requireReadyRepo gate, like backlog: the reader degrades to an empty list on a
88
161
  * missing QUESTIONS.md, so the command inspects any directory instead of refusing (a team
89
162
  * workspace still refuses).
@@ -102,72 +175,25 @@ const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
102
175
  * into JSON mode. Unknown `--`-prefixed tokens stay refused even there, so a misspelled
103
176
  * flag is still an error rather than silent decision text. */
104
177
  function parseQuestionsInvocation(args) {
105
- const words = [];
106
- let jsonFlag = false;
107
- let numbered = false;
108
- let file;
109
- let answeredFlag = false;
110
- let limit;
111
- let grep;
112
- let show;
178
+ const scan = { words: [], jsonFlag: false, numbered: false, file: undefined, answeredFlag: false, limit: undefined, grep: undefined, show: undefined };
113
179
  for (let i = 0; i < args.length; i++) {
114
- const arg = args[i];
115
- if (!numbered && arg === "--show") {
116
- rejectRepeatFlag(show !== undefined, "--show");
117
- show = parsePositionFlag("--show", args[++i], "--show 2");
180
+ const consumed = readPreNumberFlag(scan, args, i) ?? readAnywhereFlag(scan, args, i);
181
+ if (consumed !== undefined) {
182
+ i = consumed;
118
183
  continue;
119
184
  }
120
- if (!numbered && arg === "--answered") {
121
- rejectRepeatFlag(answeredFlag, "--answered");
122
- answeredFlag = true;
123
- continue;
124
- }
125
- if (!numbered && arg === "-n") {
126
- rejectRepeatFlag(limit !== undefined, "-n");
127
- limit = parseCountFlag("-n", args[++i]);
128
- continue;
129
- }
130
- if (!numbered && arg === "--grep") {
131
- rejectRepeatFlag(grep !== undefined, "--grep");
132
- const text = args[++i];
133
- if (text === undefined || text === "")
134
- fail("--grep needs text (usage: tumwater questions [--answered] --grep <text>)");
135
- grep = text;
136
- continue;
137
- }
138
- if (arg === "--file") {
139
- rejectRepeatFlag(file !== undefined, "--file");
140
- file = promptFileValue(args[++i]);
141
- continue;
142
- }
143
- if (!numbered && arg === "--json") {
144
- rejectRepeatFlag(jsonFlag, "--json");
145
- jsonFlag = true;
146
- continue;
147
- }
148
- if (arg.startsWith("--") && arg !== "--json") {
149
- rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
150
- // The complete flag set (this gate only sees `--`-flags, but `-n` is a real flag the
151
- // list should not omit) plus the same did-you-mean hint the sibling commands'
152
- // unknown-argument gate (cli-flag-specs's failUnknownArgument) attaches, so a mistyped
153
- // `--ansered` names `--answered` instead of only printing the valid list — the missing
154
- // treatment the questions gate was the one unknown-argument caller without.
155
- const spellings = ["--json", "--file", "--answered", "--show", "--grep", "-n"];
156
- fail(`unknown argument: ${arg} (valid flags for tumwater questions: ${spellings.join(", ")})${typoSuffix(arg, spellings)}`);
157
- }
158
- if (!numbered) {
185
+ if (!scan.numbered) {
159
186
  // The position is a plain-decimal positive integer (parsePositiveInt's rule, the same
160
187
  // one --cancel's position honors): bare Number() admitted hex ("0x2"), exponent
161
188
  // ("1e2"), and whitespace-padded spellings as a valid question number, answering a
162
189
  // question the operator never named.
163
- if (parsePositiveInt(arg) !== null)
164
- numbered = true;
190
+ if (parsePositiveInt(args[i]) !== null)
191
+ scan.numbered = true;
165
192
  }
166
- words.push(arg);
193
+ scan.words.push(args[i]);
167
194
  }
168
- if (limit !== undefined && !answeredFlag)
169
- fail("-n needs --answered (it limits the answered questions listed)");
170
- return { words, jsonFlag, file, answeredFlag, limit, grep, show };
195
+ assertQuestionsFlags(scan);
196
+ return { words: scan.words, jsonFlag: scan.jsonFlag, file: scan.file, answeredFlag: scan.answeredFlag, limit: scan.limit, grep: scan.grep, show: scan.show };
171
197
  }
172
198
  /** The `--show` output branch: one open (or, with `--answered`, answered) question in full. */
173
199
  function sayShownQuestion(root, inv) {
@@ -1,12 +1,12 @@
1
1
  /** The repo-readiness gate every repo-bound command runs before its body reads state: fail
2
- * fast on the first unmet precondition (startup-gate.ts's repoNotReady — the repo half of
2
+ * fast on the first unmet precondition (startup-gates.ts's repoNotReady — the repo half of
3
3
  * `tumwater start`'s startup gate). One named home beside the argument gate (cli-flag-specs.ts):
4
4
  * the dispatcher (cli.ts), the marker commands (cli-marker-commands.ts), the write commands
5
5
  * (backlog/backlog-cli.ts), and the read-only command bodies (cli-query-commands.ts) all
6
6
  * import it, so a command layer never reaches into a sibling command module for a gate. */
7
7
  import { fail } from "./cli-output.js";
8
8
  import { teamWorkspaceMessage } from "../gates/readiness.js";
9
- import { repoNotReady } from "../gates/startup-gate.js";
9
+ import { repoNotReady } from "../gates/startup-gates.js";
10
10
  import { owningTeamRoot } from "../paths.js";
11
11
  /** Fail (exit 1) on the first unmet repo precondition, naming the fix; return once the repo is
12
12
  * ready. */
@@ -1,7 +1,7 @@
1
1
  /** `tumwater role <id>`'s command body, split out of cli-query-commands.ts so that module
2
2
  * keeps only the read-only query commands (status, diff, queue, backlog). cli.ts's dispatcher
3
3
  * routes to cmdRole here exactly as it routes cmdStatus to cli-query-commands or cmdConfig to
4
- * config-commands. cmdRole is the one query-named command that also writes: it renders a
4
+ * cli-config-commands. cmdRole is the one query-named command that also writes: it renders a
5
5
  * role's standing and next-run prompts and carries eleven per-role write flags (notes,
6
6
  * instructions, quiet-hours, daily cap, min-run-interval, instances, enable/disable, and the
7
7
  * provider/model/thinking/backend), each through the same setConfigKey/unsetConfigKey core
@@ -56,10 +56,6 @@ const INSTANCES_SPEC = { names: ["--instances"], value: true, valueName: "<n>" }
56
56
  * roles.<base>.enabled. */
57
57
  const ENABLE_SPEC = { names: ["--enable"] };
58
58
  const DISABLE_SPEC = { names: ["--disable"] };
59
- /** `role`'s review-after-merge write flags: add the role to, or remove it from,
60
- * review.afterMerge (written for the base id). */
61
- const REVIEW_AFTER_MERGE_SPEC = { names: ["--review-after-merge"] };
62
- const CLEAR_REVIEW_AFTER_MERGE_SPEC = { names: ["--clear-review-after-merge"] };
63
59
  /** `role`'s model-triple write flags: clear the field, or set it, written to
64
60
  * roles.<base>.provider/.model/.thinking. model and provider take an id; thinking takes a
65
61
  * pi `--thinking` level (off|minimal|low|medium|high|xhigh|max). */
@@ -175,7 +171,7 @@ export async function cmdRole(root, args) {
175
171
  if (next?.startsWith("--"))
176
172
  fail(`${flag} needs a value (the next token ${JSON.stringify(next)} is a flag, not a value)`);
177
173
  }
178
- rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC, CLEAR_BACKEND_SPEC, BACKEND_SPEC, REVIEW_AFTER_MERGE_SPEC, CLEAR_REVIEW_AFTER_MERGE_SPEC]);
174
+ rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC, CLEAR_BACKEND_SPEC, BACKEND_SPEC]);
179
175
  refuseTeamWorkspace(root);
180
176
  // The id is required: positional (`tumwater role <id>`) or --role <id> (the flag
181
177
  // spelling every other role-targeting command shares). Both at once is a mistake, not
@@ -211,9 +207,6 @@ export async function cmdRole(root, args) {
211
207
  const backendWrite = clearBackend || rest.includes("--backend");
212
208
  const enable = rest.includes("--enable");
213
209
  const disable = rest.includes("--disable");
214
- const reviewAfterMerge = rest.includes("--review-after-merge");
215
- const clearReviewAfterMerge = rest.includes("--clear-review-after-merge");
216
- const reviewAfterMergeWrite = reviewAfterMerge || clearReviewAfterMerge;
217
210
  const writes = [];
218
211
  if (notesWrite)
219
212
  writes.push("notes");
@@ -239,8 +232,6 @@ export async function cmdRole(root, args) {
239
232
  writes.push("enable");
240
233
  if (disable)
241
234
  writes.push("disable");
242
- if (reviewAfterMergeWrite)
243
- writes.push("review-after-merge");
244
235
  if (writes.length > 1)
245
236
  fail(`${writes.join(" and ")} writes cannot share one invocation`);
246
237
  if (notesWrite) {
@@ -443,36 +434,6 @@ export async function cmdRole(root, args) {
443
434
  say(`${base}: ${state}`);
444
435
  return;
445
436
  }
446
- // The review-after-merge write: the one command the post-merge churn row's remedy
447
- // needs — add the role to, or remove it from, review.afterMerge for the base id, the
448
- // membership test reviewedAfterMerge shares. The whole-list value lands through the same
449
- // setConfigKey core `config set review.afterMerge <json>` uses, so validateConfig owns the
450
- // string-array shape, the known-role-id check and the `review.afterMerge must not be
451
- // "director"` refusal, and the write is atomic. A role already in the asked-for state is
452
- // reported as such and the file is left alone (the section branch compares the value
453
- // against the resolved list, so a no-op reports unchanged and writes nothing).
454
- if (reviewAfterMergeWrite) {
455
- if (reviewAfterMerge && clearReviewAfterMerge)
456
- fail("give --review-after-merge or --clear-review-after-merge, not both");
457
- if (rest.includes("--json"))
458
- fail("--json does not apply to a review-after-merge write");
459
- requireKnownRole(root, id);
460
- const base = baseRoleOf(id);
461
- const list = loadConfigSafe(root).config?.review.afterMerge ?? [];
462
- const has = list.includes(base);
463
- const next = has === reviewAfterMerge ? list : reviewAfterMerge ? [...list, base] : list.filter((r) => r !== base);
464
- const result = setConfigKey(root, "review.afterMerge", JSON.stringify(next));
465
- if (!result.ok)
466
- fail(result.error);
467
- say(result.unchanged
468
- ? reviewAfterMerge
469
- ? `${base}: already in review.afterMerge, so tumwater.json was not written`
470
- : `${base}: not in review.afterMerge, so nothing was cleared`
471
- : reviewAfterMerge
472
- ? `${base}: added to review.afterMerge`
473
- : `${base}: removed from review.afterMerge`);
474
- return;
475
- }
476
437
  // The unknown-role answer (exit 1, unknownRoleMessage's wording) lives in the
477
438
  // collector; here the id just has to be non-null for the thunk's types.
478
439
  sayJsonOrRender(rest, () => rolePayload(root, id), renderRoleMarkdown);
@@ -1,9 +1,10 @@
1
1
  /** `tumwater start`'s exit-summary half: the machinery that turns a finished round or window
2
2
  * into its summary — the `--once` one-line report and its `--json` document, and the `--for`
3
3
  * window summary table and report. `src/cli/cli-start.ts` keeps `cmdStart` (the boot and the
4
- * run loop), the window's deadline and scheduled-stop watchdogs, the `--gui` child plan, and
5
- * the supervisor half; this module holds only the reporting that a `start`'s exit prints, so
6
- * the largest command module is no longer also the home of every way a run's tally is shaped.
4
+ * run loop), the window's deadline and scheduled-stop watchdogs, and the invocation guards;
5
+ * the supervisor half lives in `src/cli/cli-start-supervisor.ts`; this module holds only the
6
+ * reporting that a `start`'s exit prints, so the largest command module is no longer also the
7
+ * home of every way a run's tally is shaped.
7
8
  * `parseRunWindow` keeps `--for`'s shape and ceiling rules together with the window summary
8
9
  * they feed. */
9
10
  import { compareCodeUnits } from "../collections.js";
@@ -0,0 +1,190 @@
1
+ import { fail, say } from "./cli-output.js";
2
+ import { parsePortFlag } from "./cli-args.js";
3
+ import { runStartupProblem } from "../gates/startup-gates.js";
4
+ import { repairInterruptedSwap } from "../build/build-stage.js";
5
+ import { logEvent } from "../events/events.js";
6
+ import { notifyTeamDown } from "../events/event-notify.js";
7
+ import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
8
+ /** The env var that carries the dashboard gate token between a supervisor and its dashboard
9
+ * child (and across a re-exec'd dashboard generation), for the `run --gui`/`start --gui`
10
+ * supervisor's own use. Defined here rather than imported from gui-command.js so this module
11
+ * (and every `start`/`run` invocation) never pulls the GUI server graph onto the page; it
12
+ * must stay the same literal as gui-command's GUI_TOKEN_ENV, and a merge that renames one
13
+ * renames the other. */
14
+ export const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
15
+ /** Split `run`'s args for `run --gui` and `run --tui`: whether the supervisor should also start
16
+ * the dashboard and/or host the observer TUI, and the args the supervised orchestrator
17
+ * generation gets with both stripped so the generation's own cmdStart never tries to supervise a
18
+ * second dashboard or re-attach a TUI in a terminal the parent owns (a self-redeploy spawns a
19
+ * fresh generation each time). Exported so the unit tests pin the stripping that removing the
20
+ * spawn wiring would otherwise silently drop. */
21
+ export function guiChildPlan(runArgs) {
22
+ const portIdx = runArgs.indexOf("--port");
23
+ const portRaw = portIdx >= 0 ? runArgs[portIdx + 1] : undefined;
24
+ const port = portRaw !== undefined ? parsePortFlag(portRaw) : null;
25
+ const tokenIdx = runArgs.indexOf("--token");
26
+ const tokenRaw = tokenIdx >= 0 ? runArgs[tokenIdx + 1] : undefined;
27
+ // Drop --port/--token and their value tokens from the supervized orchestrator generation
28
+ // along with --gui/--tui (and --all-interfaces by name): the child's own cmdStart would
29
+ // otherwise hit the dashboard-only-flag refusals (its args no longer carry --gui) or supervise
30
+ // a second dashboard, and a self-redeploy hand-off must never forward a dashboard flag it is
31
+ // not serving itself. --all-interfaces and --token have no value token to skip, so the name
32
+ // filter below drops them; the valued pair is skipped by index like --port's.
33
+ const skip = new Set();
34
+ if (portIdx >= 0) {
35
+ skip.add(portIdx);
36
+ if (portIdx + 1 < runArgs.length)
37
+ skip.add(portIdx + 1);
38
+ }
39
+ if (tokenIdx >= 0) {
40
+ skip.add(tokenIdx);
41
+ if (tokenIdx + 1 < runArgs.length)
42
+ skip.add(tokenIdx + 1);
43
+ }
44
+ const guiArgs = [];
45
+ if (port !== null)
46
+ guiArgs.push("--port", String(port));
47
+ if (runArgs.includes("--all-interfaces"))
48
+ guiArgs.push("--all-interfaces");
49
+ // The token is returned separately (guiToken) and reaches the child through its env, never
50
+ // guiArgs/argv: a secret on the command line is world-readable (/proc/<pid>/cmdline) and is
51
+ // re-run verbatim by a self-reload onto each new generation. The skip set still drops the
52
+ // operator's --token pair from the orchestrator generation (its cmdStart would refuse a
53
+ // dashboard flag without --gui, and a self-redeploy hand-off must never forward it).
54
+ return {
55
+ spawnGui: runArgs.includes("--gui"),
56
+ spawnTui: runArgs.includes("--tui"),
57
+ guiArgs,
58
+ guiToken: tokenRaw,
59
+ orchestratorArgs: runArgs.filter((arg, i) => arg !== "--gui" && arg !== "--tui" && arg !== "--all-interfaces" && !skip.has(i)),
60
+ childStdio: runArgs.includes("--tui") ? ["ignore", "ignore", "ignore"] : "inherit",
61
+ };
62
+ }
63
+ /** The env a supervised dashboard child is spawned with: it marks the child as owned by this
64
+ * supervisor (`childMarkerEnv`, DASHBOARD_CHILD_ENV, names the host pid so the child's reload
65
+ * watch at src/redeploy/self-reload.ts exits RESTART_EXIT_CODE / dies with the supervisor) and,
66
+ * when the operator gave a gate token, carries it under GUI_TOKEN_ENV. The token deliberately
67
+ * never reaches the child's argv: /proc/<pid>/cmdline is world-readable, and the child's own
68
+ * self-reload (reexecSelf, which propagates env via childEnv) would re-run a command-line secret
69
+ * onto each fresh generation. Kept out of superviseDashboard so this env wiring is unit-testable
70
+ * without spawning a live dashboard child (BUGS-FIXED.md's dashboard gate-token fix). */
71
+ export function dashboardChildEnv(childMarkerEnv, guiToken, hostPid) {
72
+ const env = { [childMarkerEnv]: String(hostPid) };
73
+ if (guiToken !== undefined)
74
+ env[GUI_TOKEN_ENV] = guiToken;
75
+ return env;
76
+ }
77
+ /** The dashboard half of `run --gui`: keep a `tumwater gui` child alive across its own
78
+ * self-reloads. The child is marked with DASHBOARD_CHILD_ENV naming this supervisor, so its
79
+ * reload watch (src/redeploy/self-reload.ts) exits RESTART_EXIT_CODE for a fresh sibling on a
80
+ * redeploy and exits on its own if this supervisor dies outright (SIGKILL, a crash) — a killed
81
+ * supervisor never leaves a dashboard holding its port. Any other dashboard exit (a failed bind
82
+ * prints its own message and exits) ends this loop and leaves the team running. */
83
+ async function superviseDashboard(signal, guiToken, stdio = "inherit", guiArgs = []) {
84
+ const { DASHBOARD_CHILD_ENV } = await import("../redeploy/self-reload.js");
85
+ const { spawnGuiChild } = await import("../process/supervisor.js");
86
+ const env = dashboardChildEnv(DASHBOARD_CHILD_ENV, guiToken, process.pid);
87
+ for (;;) {
88
+ const exit = await spawnGuiChild(signal, env, stdio, guiArgs);
89
+ if (signal.aborted || exit.code !== RESTART_EXIT_CODE)
90
+ return;
91
+ }
92
+ }
93
+ /** The supervisor half of `tumwater start` (src/process/supervisor.ts): spawn the orchestrator as a
94
+ * child generation and respawn it whenever it exits RESTART_EXIT_CODE after redeploying itself.
95
+ * Ctrl+C reaches the child directly from the terminal, so only SIGTERM is forwarded; the
96
+ * supervisor's own exit code is whatever the last generation's was. A team that goes down without
97
+ * the operator asking leaves a `supervisor_exit` event behind (BUGS.md 2026-09-23): the dead
98
+ * generation's stderr reached only this terminal, which nobody may be watching. */
99
+ export async function superviseStartCommand(root, runArgs, branchArg, forMs) {
100
+ // The supervisor machinery loads lazily, like cmdStart's orchestrator import above: a command
101
+ // that never boots the team never compiles it.
102
+ const { RUN_WINDOW_START_ENV, teamDownEvent, spawnStartChild, superviseStart } = await import("../process/supervisor.js");
103
+ const plan = guiChildPlan(runArgs);
104
+ // A TUI owns the terminal, so it can only be hosted when this supervisor's own stdin and
105
+ // stdout are a TTY — the same guard runTui enforces. Check before booting a team a TUI
106
+ // could never drive, surfacing the actionable tuiTerminalError instead of letting runTui's
107
+ // rejection sit unhandled until superviseStart settles and then crash the supervisor after
108
+ // the team has (pointlessly) started.
109
+ if (plan.spawnTui && (!process.stdin.isTTY || !process.stdout.isTTY)) {
110
+ const { tuiTerminalError } = await import("../ui/tui/tui-input.js");
111
+ fail(tuiTerminalError(Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY)));
112
+ }
113
+ const windowEnv = forMs !== null ? { [RUN_WINDOW_START_ENV]: String(Date.now()) } : {};
114
+ const controller = new AbortController();
115
+ // The dashboard lives beside the orchestrator generations rather than inside one: it must
116
+ // outlive every generation a redeploy swaps out. Its own abort stops the current dashboard
117
+ // child, which spawnGuiChild's abort wiring SIGTERMs.
118
+ const guiController = new AbortController();
119
+ let stopping = false;
120
+ process.on("SIGINT", () => {
121
+ stopping = true; // The child got the same SIGINT from the terminal and stops on its own.
122
+ });
123
+ process.on("SIGTERM", () => {
124
+ stopping = true;
125
+ controller.abort(); // Not delivered to the child by the kernel — forward it.
126
+ guiController.abort();
127
+ });
128
+ // The TUI is hosted in the supervisor process, beside the orchestrator generations it must
129
+ // outlive. Quitting it (q or Ctrl+C handled by ink's key handler) is the run's Ctrl+C:
130
+ // runTui resolves and the .then sets `stopping` and aborts the controller, so superviseStart
131
+ // drains in-flight runs and the team shuts down exactly as Ctrl+C on a plain `run` does;
132
+ // the team-down teardown (dashboard abort, onDone, terminal restore) is unchanged.
133
+ // reloadDisabled keeps the hosted TUI off its self-reload watch: the team's own first
134
+ // self-redeploy dist swap must not resolve runTui (which would read as a quit and abort
135
+ // this run's controller) or spawn a re-exec replacement; the host supervisor survives a
136
+ // redeploy by respawning its team children, and the hosted TUI — like the supervisor
137
+ // itself — stays on the build it started, getting fresh code on the next run. The abort
138
+ // signal is how this process ends the TUI when the run ends on its own (a `--for` window
139
+ // closes, or the team stops): runTui resolves on abort and takes its quit teardown, so the
140
+ // `await tui` below cannot hang on a TUI nothing else would ever close.
141
+ const tuiController = new AbortController();
142
+ const tui = plan.spawnTui
143
+ ? (await import("../ui/tui/tui.js")).runTui(root, { abortSignal: tuiController.signal, reloadDisabled: true }).then(() => {
144
+ stopping = true; // the TUI quit = the run's Ctrl+C
145
+ controller.abort();
146
+ guiController.abort();
147
+ })
148
+ : Promise.resolve();
149
+ const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.guiToken, plan.childStdio, plan.guiArgs) : Promise.resolve();
150
+ const code = await superviseStart({
151
+ spawnChild: (signal) => spawnStartChild(signal, plan.orchestratorArgs, windowEnv, plan.childStdio),
152
+ stopping: () => stopping,
153
+ onRespawn: (generation) => {
154
+ // With a hosted TUI the supervisor owns no free stdout: its own say() would paint the
155
+ // restart line over the ink full-screen render and corrupt it — the same interleaving
156
+ // childStdio silences for the children. The TUI renders status from on-disk state, so
157
+ // the redeploy is visible there as the team coming back.
158
+ if (!plan.spawnTui && !runArgs.includes("--json"))
159
+ say(`\nrestarting onto the update (restart ${generation})\n`);
160
+ },
161
+ onCrashLoop: () => process.stderr.write("tumwater restarted itself too many times in a minute — giving up\n"),
162
+ // Re-ask the startup gate for the reason: a generation that dies right after a respawn
163
+ // most likely met an environment that no longer boots (the 2026-09-22 child found
164
+ // tumwater.json gone). A crash loop's generations all asked for restarts, so the gate
165
+ // has nothing to say about it.
166
+ onTeamDown: async (down) => {
167
+ const event = logEvent(root, teamDownEvent(down, down.crashLoop ? null : await runStartupProblem(root, branchArg)));
168
+ notifyTeamDown(root, event);
169
+ },
170
+ // The end-of-supervision build repair (see build-stage.repairInterruptedSwap): a swap
171
+ // killed between its two renames leaves dist/ missing, and this process is the only one
172
+ // still able to rename the preserved old build back before it exits — the next `tumwater
173
+ // run` would otherwise fail to load its own dist/src/cli.js. repairInterruptedSwap is a
174
+ // static import, so its code is in memory before any swap can move dist/ away: an import
175
+ // at call time would fail ERR_MODULE_NOT_FOUND exactly when it is needed. The repair
176
+ // reports its own outcome, so it can never change the exit code.
177
+ onDone: () => repairInterruptedSwap(root),
178
+ }, controller.signal);
179
+ // The orchestrator is gone; take the dashboard and the TUI with it so a stopped team never
180
+ // leaves a dashboard holding its port or a TUI holding the terminal. abort() SIGTERMs the
181
+ // current gui child; await lets the spawn loop observe the abort before this process exits.
182
+ // On a plain run the user's own Ctrl+C already resolved the TUI; on a windowed (--for) run
183
+ // the window just closed with the TUI still up, so it is aborted here and its teardown /
184
+ // terminal restore settle before this process exits.
185
+ tuiController.abort();
186
+ await tui; // let the TUI's promise settle / terminal restore
187
+ guiController.abort();
188
+ await gui;
189
+ process.exit(code);
190
+ }