tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,87 +1,41 @@
1
- import { compareCodeUnits, increment } from "../collections.js";
2
- import { loopIds, loopPaused } from "../roles/loop-ids.js";
3
- import { fail, say } from "./cli-output.js";
4
- import { durationLabel, failOverDurationCap, failRivalShapes, flagValue, parseBranchFlag, parseDurationFlag, parseRoleFlag, } from "./cli-args.js";
5
- import { PAUSE_FOR_MAX_MS } from "../operator/operator-intent.js";
6
- import { parseInitArgs } from "./cli-command-args.js";
7
- import { cancelScheduledStop, isTeamPaused, pausedRoles, scheduledStopAt } from "../team/team-state.js";
1
+ import { baseRoleOf, runnerIds } from "../roles/runner-ids.js";
2
+ import { knownRoleIds } from "../config/config.js";
3
+ import { unknownRoleMessage } from "../roles/roles.js";
4
+ import { fail, say, sayJson } from "./cli-output.js";
5
+ import { durationLabel, failRivalShapes, parseBranchFlag, parsePortFlag, parseRoleListFlag, tokenValueProblem, } from "./cli-args.js";
6
+ import { cancelScheduledStop, scheduledStopAt } from "../team/team-state.js";
8
7
  import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
9
8
  import { runStartupCheck, runStartupProblem } from "../gates/startup-gate.js";
10
- import { initProject } from "../init/init.js";
11
- import { templateCatalog } from "../init/init-templates.js";
12
9
  import { repairInterruptedSwap } from "../build/build-stage.js";
13
10
  import { logEvent, subscribeEvents } from "../events/events.js";
14
11
  import { notifyTeamDown } from "../events/notify.js";
15
12
  import { formatEvent } from "../events/event-format.js";
16
13
  import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
17
14
  import { runStartupMigrations } from "../orchestrator/startup-migration-steps.js";
18
- import { loadLoopState, stateSkipReason } from "../loop/loop-state.js";
19
- import { plural } from "../text/phrases.js";
15
+ import { loadRunnerState } from "../runner/runner-state-load.js";
20
16
  import { shortSha } from "../text/format.js";
21
- import { readTickRowsSince, summarizeTickRows } from "../history/history-data.js";
22
- import { renderSummary } from "../history/history-render.js";
23
- import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
24
- /** `tumwater init`: seed a project directory from the operator's brief (src/init/init.ts does the
25
- * work; this prints the report). --dry-run prints the would-be actions without writing
26
- * anything; an already-initialized repo is reported as a no-op, never an error. */
27
- export async function cmdInit(root, args) {
28
- const { prompt, branch, template, listTemplates, adopt, dryRun } = parseInitArgs(args);
29
- if (listTemplates) {
30
- for (const { id, description } of templateCatalog())
31
- say(`${id} — ${description}`);
32
- return;
33
- }
34
- const result = await initProject(root, prompt, branch ?? undefined, {
35
- adopt,
36
- dryRun,
37
- template: template ?? undefined,
38
- });
39
- if (result.template !== "blank")
40
- say(`template: ${result.template}`);
41
- if (result.adopted) {
42
- say(`${result.dryRun ? "would adopt" : "adopting"} an existing repo: the project brief goes in TUMWATER.md and README.md is left untouched`);
43
- }
44
- if (result.dryRun) {
45
- if (result.repoInitialized && result.branch) {
46
- say(`dry run — would initialize a new git repository on branch ${result.branch}`);
47
- }
48
- const list = (names) => (names.length > 0 ? names.join(", ") : "nothing");
49
- say(`dry run — would create: ${list(result.created)}`);
50
- say(`would leave alone: ${list(result.leftAlone)}`);
51
- say("nothing written; re-run without --dry-run to apply");
52
- return;
53
- }
54
- if (result.created.length === 0) {
55
- say("already initialized; nothing to do");
56
- return;
57
- }
58
- if (result.repoInitialized && result.branch) {
59
- say(`initialized a new git repository on branch ${result.branch}`);
60
- }
61
- say(`created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`);
62
- if (result.bootstrap !== null) {
63
- say(`fresh project — maintenance roles wait until ${result.bootstrap} plans are done; remove "bootstrap" from tumwater.json to end that early`);
64
- }
65
- say("next: `tumwater start` in one terminal, `tumwater tui` in another");
66
- }
67
- /** `tumwater run`: boot the team and stream its event feed to this terminal until it stops.
17
+ import { readRunRowsSince, summarizeRunRows } from "../history/history-data.js";
18
+ import { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, } from "./cli-start-summary.js";
19
+ export { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, };
20
+ /** `tumwater start`: boot the team and stream its event feed to this terminal until it stops.
68
21
  * Three layers cooperate here: without SUPERVISED_ENV this process becomes the supervisor
69
- * (superviseRunCommand below), spawning the real orchestrator as a child generation; the child
22
+ * (superviseStartCommand below), spawning the real orchestrator as a child generation; the child
70
23
  * runs runOrchestrator to completion, handing the terminal back via RESTART_EXIT_CODE after a
71
24
  * self-redeploy so the supervisor respawns on the new build; --once collapses the whole thing
72
- * into one round of ticks and a summary line (onceSummary below), for cron-style invocations.
25
+ * into one round of runs and a summary line (onceSummary below), for cron-style invocations.
73
26
  * `--for <duration>` keeps the daemon shape (redeployer, LaunchServicesWatch, supervisor hand-off
74
27
  * all stay) but arms a deadline: at the deadline the same graceful stop a Ctrl+C runs fires, and
75
28
  * the process exits after printing the summary. A mid-run self-redeploy hands the same args
76
29
  * (including `--for`) to the supervisor's next child, and the supervisor hands every generation
77
30
  * the same window start (RUN_WINDOW_START_ENV), so the deadline and the summary span the whole
78
31
  * window; only the final exit prints the summary. */
79
- export async function cmdRun(root, args) {
32
+ export async function cmdStart(root, args) {
80
33
  // The one function the self-redeploy asks before swapping onto a successor and the supervisor
81
34
  // asks when a generation dies, so the three cannot disagree about what boots. The supervisor
82
35
  // half runs it too, so a start that cannot boot fails before any child spawns. The flag
83
- // vocabulary is validated by the dispatcher (cli.ts, from the same RUN_FLAG_SPECS).
36
+ // vocabulary is validated by the dispatcher (cli.ts, from the same START_FLAG_SPECS).
84
37
  const once = args.includes("--once");
38
+ const json = args.includes("--json");
85
39
  // The TUI cannot be hosted by a run that is about to stop, like the dashboard: a --once round
86
40
  // drains and exits within the same invocation, so the two shapes are refused together before
87
41
  // any boot. --for stays allowed — the TUI runs for the window, like the dashboard.
@@ -92,22 +46,57 @@ export async function cmdRun(root, args) {
92
46
  // stays allowed — the dashboard serves for the window.
93
47
  if (once && args.includes("--gui"))
94
48
  failRivalShapes("--gui", "--once", "a one-round run cannot serve a dashboard it is about to kill");
49
+ // `--port` chooses the `--gui` dashboard's port; without `--gui` it names no port for this
50
+ // command, so refuse it (a valid value already passed the spec's shape gate) before any boot.
51
+ if (args.includes("--port") && !args.includes("--gui"))
52
+ fail("--port is only valid with --gui (e.g. `tumwater start --gui --port 8080`); the standalone dashboard is `tumwater gui --port <n>`");
53
+ // `--all-interfaces` and `--token` are dashboard-only flags too: without `--gui` each names
54
+ // nothing this command starts, so refuse them (valid values already passed the spec's shape
55
+ // gate) before any boot, with the same shape as the --port refusal above and fixing the
56
+ // copyable one-command form in the message.
57
+ if (args.includes("--all-interfaces") && !args.includes("--gui"))
58
+ fail("--all-interfaces is only valid with --gui (e.g. `tumwater start --gui --all-interfaces`); the standalone dashboard is `tumwater gui --all-interfaces`");
59
+ if (args.includes("--token") && !args.includes("--gui"))
60
+ fail("--token is only valid with --gui (e.g. `tumwater start --gui --token s3cret`); the standalone dashboard is `tumwater gui --token <secret>`");
61
+ // A malformed --token (valueless, empty, or flag-looking) must fail here before any boot, so
62
+ // a bad secret never leaves the team running behind a dashboard that exited. The same
63
+ // tokenValueProblem `tumwater gui` runs, named for the one-command form the operator typed.
64
+ const tokenProblem = tokenValueProblem(args, "tumwater start --gui");
65
+ if (tokenProblem)
66
+ fail(tokenProblem);
95
67
  const forMs = parseRunWindow(args, once);
96
68
  const branchArg = parseBranchFlag(args);
97
69
  const startup = await runStartupCheck(root, branchArg);
98
70
  if ("problem" in startup)
99
71
  fail(startup.problem);
100
72
  const { config, mainBranch } = startup;
101
- // A scoped once round: `--role <id>` names the one loop that runs. Parsed here, where the
102
- // live config is readable, and validated against the ENABLED ids — not the full catalog —
103
- // so an unknown id and a disabled one fail fast with the same wording (a scoped round that
104
- // booted a disabled role's runner would run nothing while claiming to serve the operator
105
- // who just queued that loop a prompt). Custom loops are valid targets: knownRoleIds
106
- // accepts them, and an enabled custom passes this check like any built-in. Daemon
107
- // `run --role` stays an error: scoping is a once-round concept.
108
- const roleFilter = parseRoleFlag(args, loopIds(config));
73
+ // A scoped once round: `--role <id>[,<id>...]` names the runners that run. Parsed here, where the
74
+ // live config is readable. The parser accepts every known id (custom roles included, enabled
75
+ // or not) so a typo still fails with the did-you-mean hint; an id that is known but has no
76
+ // runner is then named as disabled rather than read as unknown — a scoped round that booted
77
+ // a disabled role's runner would run nothing while claiming to serve the operator who just
78
+ // queued that role a prompt. Daemon `run --role` stays an error: scoping is a once-round concept.
79
+ const roleFilter = parseRoleListFlag(args, knownRoleIds(config));
109
80
  if (roleFilter !== null && !once)
110
81
  fail("--role is only valid with --once");
82
+ if (json && !once && forMs === null)
83
+ fail("--json is only valid with --once or --for");
84
+ // Both interactive modes write their own banner to the stdout a JSON document must own, so a
85
+ // --json round/window refuses them before any boot.
86
+ if (json && args.includes("--gui"))
87
+ failRivalShapes("--json", "--gui", "the dashboard writes its banner to the stdout a JSON document must own");
88
+ if (json && args.includes("--tui"))
89
+ failRivalShapes("--json", "--tui", "a TUI owns the terminal");
90
+ if (roleFilter !== null) {
91
+ const runners = runnerIds(config);
92
+ const missing = roleFilter.find((id) => !runners.includes(id));
93
+ if (missing !== undefined) {
94
+ const base = baseRoleOf(missing);
95
+ if (config.roles[base]?.enabled === false)
96
+ fail(`${missing} is disabled, so a --once round cannot run it — enable it with \`tumwater role ${base} --enable\``);
97
+ fail(unknownRoleMessage(missing, runners));
98
+ }
99
+ }
111
100
  // The refusal an operator hits by trying to start a second team in one repo: name the
112
101
  // running pid and the two ways to stop it, so the fix is in the message instead of a
113
102
  // separate `tumwater status`/help lookup. The info file is read once and reused for the
@@ -126,7 +115,7 @@ export async function cmdRun(root, args) {
126
115
  // targets the same branch (or whatever else the invocation named).
127
116
  // A stop marker left by a team that died before its deadline must not stop this run.
128
117
  cancelScheduledStop(root);
129
- await superviseRunCommand(root, args, branchArg, forMs);
118
+ await superviseStartCommand(root, args, branchArg, forMs);
130
119
  return;
131
120
  }
132
121
  const controller = new AbortController();
@@ -135,7 +124,11 @@ export async function cmdRun(root, args) {
135
124
  if (stopping)
136
125
  process.exit(130);
137
126
  stopping = true;
138
- say("\nstopping — finishing current runs (Ctrl+C again to force)");
127
+ // A --once --json round's stdout must hold exactly its one JSON document, so a signal,
128
+ // parent death, or scheduled stop quietens the stop prose too; the non-zero exit below
129
+ // is what a cron/CI caller sees instead of the words. Non-json runs keep the prose.
130
+ if (!json)
131
+ say("\nstopping — finishing current runs (Ctrl+C again to force)");
139
132
  controller.abort();
140
133
  };
141
134
  process.on("SIGINT", stop);
@@ -145,16 +138,19 @@ export async function cmdRun(root, args) {
145
138
  // retracts it when the run ends first (an early Ctrl+C), so no stray timer outlives the run.
146
139
  const windowStart = inheritedWindowStart(process.env[RUN_WINDOW_START_ENV], Date.now());
147
140
  const deadlineTimer = forMs !== null ? armDeadlineTimer(Math.max(0, windowStart + forMs - Date.now()), () => {
148
- say("deadline reached — stopping");
141
+ // A --for --json window's stdout must hold exactly its one JSON document, so the deadline
142
+ // prose is suppressed too; the document's non-zero-absent facts carry the window.
143
+ if (!json)
144
+ say("deadline reached — stopping");
149
145
  stop();
150
146
  }) : null;
151
147
  const scheduledStopWatch = armScheduledStopWatch(root, () => {
152
148
  if (!stopping)
153
149
  stop(); // A drain already under way must not be turned into a force-kill.
154
- });
150
+ }, json);
155
151
  // A supervisor that dies without forwarding — SIGKILL (the OOM killer, `kill -9`) cannot be
156
152
  // trapped or forwarded, and an uncaught supervisor crash forwards nothing either — leaves this
157
- // generation reparented and ticking the team unattended. The parent-death watch polls for the
153
+ // generation reparented and running the team unattended. The parent-death watch polls for the
158
154
  // reparent and runs the same graceful stop a forwarded SIGTERM would have run, so a killed
159
155
  // supervisor takes its team down with it (BUGS.md 2026-09-30). The spawner hands its pid
160
156
  // down (SUPERVISOR_PID_ENV), so a supervisor killed during this child's startup — before the
@@ -162,10 +158,10 @@ export async function cmdRun(root, args) {
162
158
  // the supervised generation: an unsupervised `run` became the supervisor above and returned.
163
159
  const expectedPpid = expectedSupervisorPid();
164
160
  startParentDeathWatch(stop, expectedPpid !== undefined ? { expectedPpid } : {});
165
- const enabled = loopIds(config);
166
- // A scoped round's loop list is exactly the filter; otherwise every enabled loop runs.
167
- const roles = roleFilter !== null ? [roleFilter] : enabled;
168
- // Main's green verdicts outlive this generation: the redeployer and the loops' red-main gate
161
+ const enabled = runnerIds(config);
162
+ // A scoped round's role list is exactly the filter; otherwise every enabled role runs.
163
+ const roles = roleFilter ?? enabled;
164
+ // Main's green verdicts outlive this generation: the redeployer and the roles' red-main gate
169
165
  // start from the ones the previous generation verified instead of re-running the suite.
170
166
  (await import("../baseline/main-baseline.js")).persistBaselineVerdicts(root);
171
167
  // A once round never self-redeploys: it runs one round on the build it booted and exits,
@@ -181,16 +177,22 @@ export async function cmdRun(root, args) {
181
177
  // from a subdirectory must see where .tumwater/ actually lives.
182
178
  const rootNote = root !== process.cwd() ? ` · root ${root}` : "";
183
179
  const forNote = forMs !== null ? ` · for ${durationLabel(forMs)}` : "";
184
- say(once
185
- ? `tumwater once on branch ${mainBranch}${rootNote} — one round, then exit`
186
- : `tumwater running on branch ${mainBranch}${forNote}${build}${rootNote} — Ctrl+C to stop`);
187
- say(`roles: ${roles.join(", ")}`);
188
- say("watch: `tumwater tui` or `tumwater logs -f` in another terminal; events stream below\n");
189
- const unsubscribe = subscribeEvents((e) => say(formatEvent(e)));
190
- // Snapshot each role's tick counter so the once summary can tell this round's ticks from
180
+ // A --once --json round's stdout must hold exactly its one JSON document, so its banner
181
+ // and event stream are suppressed: the caller parses the round's facts directly instead
182
+ // of the prose, and the orchestrator/merge pipeline logs events through logEvent (file
183
+ // plus subscription), not stdout, with nothing else printed on stdout for a once round.
184
+ if (!json) {
185
+ say(once
186
+ ? `tumwater once on branch ${mainBranch}${rootNote} — one round, then exit`
187
+ : `tumwater running on branch ${mainBranch}${forNote}${build}${rootNote} — Ctrl+C to stop`);
188
+ say(`roles: ${roles.join(", ")}`);
189
+ say("watch: `tumwater tui` or `tumwater logs -f` in another terminal; events stream below\n");
190
+ }
191
+ const unsubscribe = json ? () => { } : subscribeEvents((e) => say(formatEvent(e)));
192
+ // Snapshot each role's run counter so the once summary can tell this round's runs from
191
193
  // the persisted history (the state file accumulates across rounds).
192
194
  await runStartupMigrations(root); // so the snapshot reads a migrated state/<new>.json
193
- const ticksBefore = new Map(roles.map((role) => [role, loadLoopState(root, role).runs]));
195
+ const runsBefore = new Map(roles.map((role) => [role, loadRunnerState(root, role).runs]));
194
196
  let exit;
195
197
  try {
196
198
  exit = await (await import("../orchestrator/orchestrator.js")).runOrchestrator({
@@ -211,48 +213,42 @@ export async function cmdRun(root, args) {
211
213
  scheduledStopWatch.clear();
212
214
  unsubscribe();
213
215
  }
214
- if (once)
215
- say(onceSummary(root, roles, ticksBefore, exit.settled, exit.ticksRun));
216
+ if (once) {
217
+ if (json)
218
+ sayJson(onceReport(root, roles, runsBefore, exit.settled, exit.runsDone));
219
+ else
220
+ say(onceSummary(root, roles, runsBefore, exit.settled, exit.runsDone));
221
+ }
216
222
  else if (forMs !== null && !exit.restart) {
217
- const { rows, covered } = readTickRowsSince(root, Date.now() - windowStart, null);
218
- say(windowSummary(durationLabel(forMs), summarizeTickRows(rows), covered));
223
+ const { rows, covered } = readRunRowsSince(root, Date.now() - windowStart, null);
224
+ const summary = summarizeRunRows(rows);
225
+ if (json)
226
+ sayJson(windowReport(durationLabel(forMs), summary, covered));
227
+ else
228
+ say(windowSummary(durationLabel(forMs), summary, covered));
219
229
  }
220
230
  if (once) {
221
- const failures = onceFailures(root, roles, ticksBefore, exit.ticksRun);
231
+ const failures = onceFailures(root, roles, runsBefore, exit.runsDone);
222
232
  if (failures.length > 0) {
223
- say(`once: failed — ${failures.map((f) => `${f.role} ${f.result}`).join(", ")}`);
233
+ // In --json mode the failure facts already ride in the report's `failures`; the exit
234
+ // code is the contract a CI caller needs, so only the prose line is suppressed.
235
+ if (!json)
236
+ say(`once: failed — ${failures.map((f) => `${f.role} ${f.result}`).join(", ")}`);
224
237
  process.exitCode = ONCE_FAILED_EXIT_CODE;
225
238
  }
239
+ // A round a signal, parent death, or scheduled stop cut short before it completed must
240
+ // not read as success (0) to the CI caller parsing the JSON: the document still carries
241
+ // the round's partial facts, but the truncated round exits non-zero. Non-json once keeps
242
+ // its historical behavior (prose + 0 unless a role failed).
243
+ if (json && stopping)
244
+ process.exitCode = ONCE_FAILED_EXIT_CODE;
226
245
  }
227
246
  // A self-redeploy swapped the new build into dist/: hand the terminal back to the supervisor,
228
247
  // which respawns this same script — now the new code — as the next generation.
229
248
  if (exit.restart)
230
249
  process.exit(RESTART_EXIT_CODE);
231
250
  }
232
- /** The window start a generation adopts: the supervisor's RUN_WINDOW_START_ENV mark when it is a
233
- * positive integer no later than `now`, else `now` (absent, malformed, or in the future). */
234
- export function inheritedWindowStart(raw, now) {
235
- const n = Number(raw);
236
- return raw !== undefined && Number.isInteger(n) && n > 0 && n <= now ? n : now;
237
- }
238
- /** The `run --for` exit summary: a window line, then the `history --summary` table for the ticks
239
- * that ended since the window opened — every tick counted once by its outcome class. Only the
240
- * final exit prints it; a mid-window self-redeploy hands off silently and the next generation
241
- * inherits the window start, so the table spans every generation. */
242
- export function windowSummary(label, summary, covered) {
243
- const { total } = summary;
244
- const lines = [
245
- total.runs === 0
246
- ? `for ${label}: 0 runs — nothing ran`
247
- : `for ${label}: ${plural(total.runs, "run")} — ${total.changes} changes, ${total.none} none, ${total.refused} refused, ${total.problems} problems`,
248
- ];
249
- if (total.runs > 0)
250
- lines.push(renderSummary(summary));
251
- if (!covered)
252
- lines.push(SPARSE_WINDOW_NOTE);
253
- return lines.join("\n");
254
- }
255
- /** Arm the windowed run's deadline timer: the one home of the arming `cmdRun` does for
251
+ /** Arm the windowed run's deadline timer: the one home of the arming `cmdStart` does for
256
252
  * `run --for <duration>`. A duration over setTimeout's own 32-bit signed ceiling
257
253
  * (2_147_483_647 ms ≈ 24.8 days) is clamped by Node to 1ms with a TimeoutOverflowWarning, so
258
254
  * a raw setTimeout would fire the deadline the instant the run booted — and `run --for`
@@ -260,7 +256,7 @@ export function windowSummary(label, summary, covered) {
260
256
  * input, not an error. The timer is therefore armed in ≤32-bit chunks: each setTimeout covers
261
257
  * at most the ceiling, and a chunk that expires with time still left re-arms for the
262
258
  * remainder, so the callback runs exactly once, after the full duration, whatever its size.
263
- * Returns a clear() handle (the finally in cmdRun retracts the timer when the run ends first —
259
+ * Returns a clear() handle (the finally in cmdStart retracts the timer when the run ends first —
264
260
  * an early Ctrl+C — so no stray timer outlives the run); clear() is safe after the callback
265
261
  * has run. Exported for tests. */
266
262
  export function armDeadlineTimer(ms, onFired) {
@@ -280,133 +276,65 @@ export function armDeadlineTimer(ms, onFired) {
280
276
  }
281
277
  const SCHEDULED_STOP_POLL_MS = 5000;
282
278
  /** Poll for a `tumwater stop --in` marker: once its `until` has passed, consume the marker,
283
- * stop polling, and call `onStop` (cmdRun's Ctrl+C closure) exactly once. The interval is
279
+ * stop polling, and call `onStop` (cmdStart's Ctrl+C closure) exactly once. The interval is
284
280
  * unref'd; clear() retracts it. Exported for tests. */
285
- export function armScheduledStopWatch(root, onStop) {
281
+ export function armScheduledStopWatch(root, onStop, quiet = false) {
286
282
  const timer = setInterval(() => {
287
283
  const at = scheduledStopAt(root);
288
284
  if (at === undefined || at > Date.now())
289
285
  return;
290
286
  clearInterval(timer);
291
287
  cancelScheduledStop(root);
292
- say("scheduled stop reached — stopping");
288
+ // quiet suppresses the "scheduled stop reached" prose for a --once --json round so its
289
+ // stdout holds exactly the one JSON document; the non-zero exit carries the interruption.
290
+ if (!quiet)
291
+ say("scheduled stop reached — stopping");
293
292
  onStop();
294
293
  }, SCHEDULED_STOP_POLL_MS);
295
294
  timer.unref();
296
295
  return { clear: () => clearInterval(timer) };
297
296
  }
298
- /** Parse `run --for <duration>` (the windowed run): the parsed duration in milliseconds, or null
299
- * when the flag is absent. The two body-level rules fail fast, before any repo gate or boot: the
300
- * value must parse as a duration (parseDurationFlag's own wording, the same messages the
301
- * dispatcher's durationFlagSpec("run --for") gate already ran), and `--for` and `--once` are rivals
302
- * — a one-round run and a windowed run cannot both apply. The 90-day ceiling is `pause --for`'s
303
- * (PAUSE_FOR_MAX_MS), the same cap every scheduled window in the harness honors. Exported for
304
- * tests, exactly as onceSummary below is. */
305
- export function parseRunWindow(args, once) {
306
- const raw = flagValue(args, "--for");
307
- if (raw === null)
308
- return null;
309
- const ms = parseDurationFlag("start --for", raw);
310
- if (once)
311
- failRivalShapes("--for", "--once", "a one-round run and a windowed run cannot both apply");
312
- failOverDurationCap("--for", ms, PAUSE_FOR_MAX_MS);
313
- return ms;
314
- }
315
- /** Results that mean the harness or the project's check failed and the round's work did not
316
- * merge: a `--once` round with a role ending in one exits ONCE_FAILED_EXIT_CODE. `rejected` and
317
- * `refused` are the review gate and the agent working as designed; `aborted`, `quiet_killed`, and
318
- * `user_aborted` only reach a once round on a shutdown or operator abort, which exit
319
- * their own way. */
320
- export const ONCE_FAILURE_RESULTS = new Set([
321
- "error",
322
- "review_error",
323
- "merge_conflict",
324
- "merge_blocked",
325
- "main_red",
326
- ]);
327
- function ticksThisRound(s, role, ticksBefore, ticksRun) {
328
- return ticksRun ? (ticksRun.get(role) ?? 0) : s.runs - (ticksBefore.get(role) ?? 0);
329
- }
330
- /** Every role that could have ticked in this once round — `roles` in order plus any role that
331
- * appears only in `ticksRun` (enabled mid-round after `ticksBefore` was taken) — paired with
332
- * its loaded loop state and the number of ticks it ran this round. The same per-role setup
333
- * onceFailures and onceSummary both repeat, folded here so the two summary passes cannot
334
- * drift on which roles they visit or how they count a round's work. */
335
- function onceRoundRoles(root, roles, ticksBefore, ticksRun) {
336
- const extra = ticksRun ? [...ticksRun.keys()].filter((r) => !roles.includes(r)) : [];
337
- return [...roles, ...extra].map((role) => {
338
- const state = loadLoopState(root, role);
339
- return { role, state, ran: ticksThisRound(state, role, ticksBefore, ticksRun) };
340
- });
341
- }
342
- /** The roles that ticked this round and whose last result is in ONCE_FAILURE_RESULTS. */
343
- export function onceFailures(root, roles, ticksBefore, ticksRun) {
344
- const out = [];
345
- for (const { role, state, ran } of onceRoundRoles(root, roles, ticksBefore, ticksRun)) {
346
- if (ran > 0 && state.lastResult && ONCE_FAILURE_RESULTS.has(state.lastResult)) {
347
- out.push({ role, result: state.lastResult });
348
- }
349
- }
350
- return out;
351
- }
352
- /** The once round's one-line summary, read from the runners' persisted loop state plus the
353
- * orchestrator's own settle reasons — a cron job's log shows what the round did without parsing
354
- * events. Roles whose tick counter advanced bucket by their last completed result; the rest are
355
- * skipped with the reason the orchestrator settled them for (`paused`, `resume pending`,
356
- * `backoff`, `deferred`, `disabled`, `idle` — handed back on OrchestratorExit.settled, so a
357
- * deferred role reads as set aside, not as "nothing was due"); a role the map lacks (defensively
358
- * — the round only exits once every role is settled) falls back to deriving the reason from
359
- * state: a pause marker when one is held, a pending resume when one is flagged, backoff when
360
- * `backoffSeconds` is raised, otherwise idle — the same classification settleSkipped draws,
361
- * and keyed on the same signal: backoffSeconds is the backoff indicator, while nextRunAt is
362
- * shared with the scheduled clock a productive tick also writes (with backoffSeconds 0), so a
363
- * future nextRunAt alone reads as a not-yet-due role, not as backoff (BUGS.md 2026-09-25,
364
- * the same conflation settleSkipped itself carried).
365
- *
366
- * `ticksRun`, when the caller hands it back (the orchestrator's OnceRound snapshot deltas),
367
- * is the tick count's source of truth and also names roles the pre-round list never had —
368
- * a role enabled mid-round joins the runners array after `ticksBefore` was taken, so its
369
- * whole persisted history must not be counted as this round's work. Without it the old
370
- * `ticksBefore` delta path runs, for callers (and tests) without a round object. */
371
- export function onceSummary(root, roles, ticksBefore, settled, ticksRun) {
372
- let ticks = 0;
373
- const outcomes = new Map();
374
- const skipped = [];
375
- for (const { role, state, ran } of onceRoundRoles(root, roles, ticksBefore, ticksRun)) {
376
- if (ran > 0) {
377
- ticks += ran;
378
- const key = state.lastResult ?? "no_change";
379
- increment(outcomes, key);
380
- }
381
- else {
382
- const reason = settled?.get(role) ??
383
- (isTeamPaused(root) || loopPaused(new Set(pausedRoles(root)), role)
384
- ? "paused"
385
- : stateSkipReason(state));
386
- skipped.push(reason);
387
- }
388
- }
389
- // Sort by result key only. The comparator makes that explicit: a comparator-less sort
390
- // would order `[key, count]` tuples by their "key,count" string form, which happens to
391
- // agree only while each key is unique.
392
- const counts = [...outcomes.entries()]
393
- .sort(([a], [b]) => compareCodeUnits(a, b))
394
- .map(([k, n]) => `${n} ${k}`)
395
- .join(", ");
396
- const skipNote = skipped.length === 0 ? "" : `, ${skipped.length} skipped (${skipped.join(", ")})`;
397
- return `once: ${plural(ticks, "run")} — ${counts || "nothing ran"}${skipNote}`;
398
- }
399
297
  /** Split `run`'s args for `run --gui` and `run --tui`: whether the supervisor should also start
400
298
  * the dashboard and/or host the observer TUI, and the args the supervised orchestrator
401
- * generation gets with both stripped so the generation's own cmdRun never tries to supervise a
299
+ * generation gets with both stripped so the generation's own cmdStart never tries to supervise a
402
300
  * second dashboard or re-attach a TUI in a terminal the parent owns (a self-redeploy spawns a
403
301
  * fresh generation each time). Exported so the unit tests pin the stripping that removing the
404
302
  * spawn wiring would otherwise silently drop. */
405
303
  export function guiChildPlan(runArgs) {
304
+ const portIdx = runArgs.indexOf("--port");
305
+ const portRaw = portIdx >= 0 ? runArgs[portIdx + 1] : undefined;
306
+ const port = portRaw !== undefined ? parsePortFlag(portRaw) : null;
307
+ const tokenIdx = runArgs.indexOf("--token");
308
+ const tokenRaw = tokenIdx >= 0 ? runArgs[tokenIdx + 1] : undefined;
309
+ // Drop --port/--token and their value tokens from the supervized orchestrator generation
310
+ // along with --gui/--tui (and --all-interfaces by name): the child's own cmdStart would
311
+ // otherwise hit the dashboard-only-flag refusals (its args no longer carry --gui) or supervise
312
+ // a second dashboard, and a self-redeploy hand-off must never forward a dashboard flag it is
313
+ // not serving itself. --all-interfaces and --token have no value token to skip, so the name
314
+ // filter below drops them; the valued pair is skipped by index like --port's.
315
+ const skip = new Set();
316
+ if (portIdx >= 0) {
317
+ skip.add(portIdx);
318
+ if (portIdx + 1 < runArgs.length)
319
+ skip.add(portIdx + 1);
320
+ }
321
+ if (tokenIdx >= 0) {
322
+ skip.add(tokenIdx);
323
+ if (tokenIdx + 1 < runArgs.length)
324
+ skip.add(tokenIdx + 1);
325
+ }
326
+ const guiArgs = [];
327
+ if (port !== null)
328
+ guiArgs.push("--port", String(port));
329
+ if (runArgs.includes("--all-interfaces"))
330
+ guiArgs.push("--all-interfaces");
331
+ if (tokenRaw !== undefined)
332
+ guiArgs.push("--token", tokenRaw);
406
333
  return {
407
334
  spawnGui: runArgs.includes("--gui"),
408
335
  spawnTui: runArgs.includes("--tui"),
409
- orchestratorArgs: runArgs.filter((arg) => arg !== "--gui" && arg !== "--tui"),
336
+ guiArgs,
337
+ orchestratorArgs: runArgs.filter((arg, i) => arg !== "--gui" && arg !== "--tui" && arg !== "--all-interfaces" && !skip.has(i)),
410
338
  childStdio: runArgs.includes("--tui") ? ["ignore", "ignore", "ignore"] : "inherit",
411
339
  };
412
340
  }
@@ -416,30 +344,30 @@ export function guiChildPlan(runArgs) {
416
344
  * redeploy and exits on its own if this supervisor dies outright (SIGKILL, a crash) — a killed
417
345
  * supervisor never leaves a dashboard holding its port. Any other dashboard exit (a failed bind
418
346
  * prints its own message and exits) ends this loop and leaves the team running. */
419
- async function superviseDashboard(signal, stdio = "inherit") {
347
+ async function superviseDashboard(signal, stdio = "inherit", guiArgs = []) {
420
348
  const { DASHBOARD_CHILD_ENV } = await import("../redeploy/self-reload.js");
421
349
  const { spawnGuiChild } = await import("../process/supervisor.js");
422
350
  for (;;) {
423
- const exit = await spawnGuiChild(signal, { [DASHBOARD_CHILD_ENV]: String(process.pid) }, stdio);
351
+ const exit = await spawnGuiChild(signal, { [DASHBOARD_CHILD_ENV]: String(process.pid) }, stdio, guiArgs);
424
352
  if (signal.aborted || exit.code !== RESTART_EXIT_CODE)
425
353
  return;
426
354
  }
427
355
  }
428
- /** The supervisor half of `tumwater run` (src/process/supervisor.ts): spawn the orchestrator as a
356
+ /** The supervisor half of `tumwater start` (src/process/supervisor.ts): spawn the orchestrator as a
429
357
  * child generation and respawn it whenever it exits RESTART_EXIT_CODE after redeploying itself.
430
358
  * Ctrl+C reaches the child directly from the terminal, so only SIGTERM is forwarded; the
431
359
  * supervisor's own exit code is whatever the last generation's was. A team that goes down without
432
360
  * the operator asking leaves a `supervisor_exit` event behind (BUGS.md 2026-09-23): the dead
433
361
  * generation's stderr reached only this terminal, which nobody may be watching. */
434
- async function superviseRunCommand(root, runArgs, branchArg, forMs) {
435
- // The supervisor machinery loads lazily, like cmdRun's orchestrator import above: a command
362
+ async function superviseStartCommand(root, runArgs, branchArg, forMs) {
363
+ // The supervisor machinery loads lazily, like cmdStart's orchestrator import above: a command
436
364
  // that never boots the team never compiles it.
437
- const { RUN_WINDOW_START_ENV, teamDownEvent, spawnRunChild, superviseRun } = await import("../process/supervisor.js");
365
+ const { RUN_WINDOW_START_ENV, teamDownEvent, spawnStartChild, superviseStart } = await import("../process/supervisor.js");
438
366
  const plan = guiChildPlan(runArgs);
439
367
  // A TUI owns the terminal, so it can only be hosted when this supervisor's own stdin and
440
368
  // stdout are a TTY — the same guard runTui enforces. Check before booting a team a TUI
441
369
  // could never drive, surfacing the actionable tuiTerminalError instead of letting runTui's
442
- // rejection sit unhandled until superviseRun settles and then crash the supervisor after
370
+ // rejection sit unhandled until superviseStart settles and then crash the supervisor after
443
371
  // the team has (pointlessly) started.
444
372
  if (plan.spawnTui && (!process.stdin.isTTY || !process.stdout.isTTY)) {
445
373
  const { tuiTerminalError } = await import("../ui/tui/tui-input.js");
@@ -462,8 +390,8 @@ async function superviseRunCommand(root, runArgs, branchArg, forMs) {
462
390
  });
463
391
  // The TUI is hosted in the supervisor process, beside the orchestrator generations it must
464
392
  // outlive. Quitting it (q or Ctrl+C handled by ink's key handler) is the run's Ctrl+C:
465
- // runTui resolves and the .then sets `stopping` and aborts the controller, so superviseRun
466
- // drains in-flight ticks and the team shuts down exactly as Ctrl+C on a plain `run` does;
393
+ // runTui resolves and the .then sets `stopping` and aborts the controller, so superviseStart
394
+ // drains in-flight runs and the team shuts down exactly as Ctrl+C on a plain `run` does;
467
395
  // the team-down teardown (dashboard abort, onDone, terminal restore) is unchanged.
468
396
  // reloadDisabled keeps the hosted TUI off its self-reload watch: the team's own first
469
397
  // self-redeploy dist swap must not resolve runTui (which would read as a quit and abort
@@ -481,16 +409,16 @@ async function superviseRunCommand(root, runArgs, branchArg, forMs) {
481
409
  guiController.abort();
482
410
  })
483
411
  : Promise.resolve();
484
- const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.childStdio) : Promise.resolve();
485
- const code = await superviseRun({
486
- spawnChild: (signal) => spawnRunChild(signal, plan.orchestratorArgs, windowEnv, plan.childStdio),
412
+ const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.childStdio, plan.guiArgs) : Promise.resolve();
413
+ const code = await superviseStart({
414
+ spawnChild: (signal) => spawnStartChild(signal, plan.orchestratorArgs, windowEnv, plan.childStdio),
487
415
  stopping: () => stopping,
488
416
  onRespawn: (generation) => {
489
417
  // With a hosted TUI the supervisor owns no free stdout: its own say() would paint the
490
418
  // restart line over the ink full-screen render and corrupt it — the same interleaving
491
419
  // childStdio silences for the children. The TUI renders status from on-disk state, so
492
420
  // the redeploy is visible there as the team coming back.
493
- if (!plan.spawnTui)
421
+ if (!plan.spawnTui && !runArgs.includes("--json"))
494
422
  say(`\nrestarting onto the update (restart ${generation})\n`);
495
423
  },
496
424
  onCrashLoop: () => process.stderr.write("tumwater restarted itself too many times in a minute — giving up\n"),