tumwater 0.4.2 → 0.4.3

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 (305) 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} +25 -12
  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 +1 -8
  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 +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -13,8 +13,8 @@ import { logEventBestEffort } from "../events/events.js";
13
13
  * full-suite check. The one vocabulary the three predicates that recognize a full-suite check
14
14
  * select on — isPassingFullCheck, isRedFullCheck, and the status polls' newest-merge-check
15
15
  * scan (status-polls.ts) — so they cannot drift on which scopes count. The deadline-killed
16
- * failure costing in time-spend.ts is a narrower pair (merge/batch only, with a status and a
17
- * timeout required), so it keeps its own check. `full` is the event's `full` field: false
16
+ * failure costing in failure-time-spend.ts is a narrower pair (merge/batch only, with a status
17
+ * and a timeout required), so it keeps its own check. `full` is the event's `full` field: false
18
18
  * (a check.mergeCommand run) is never a full-suite check. */
19
19
  export function isFullCheckScope(scope, full) {
20
20
  // A merge or batch run of check.mergeCommand carries `full: false` (buildCheckEvent): it ran
@@ -32,7 +32,7 @@ function resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad) {
32
32
  /** Run the resolved check under one permit, with the one clean retry for a verdict-less death —
33
33
  * the one-permit-two-attempt rule. Prices the first attempt as its own build_check event and
34
34
  * returns the final outcome and its run duration. */
35
- async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper) {
35
+ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper, signal) {
36
36
  // One permit covers both attempts (the killed-check retry re-runs under the permit it
37
37
  // holds); durationMs prices the run itself, not the wait for a permit.
38
38
  let durationMs = 0;
@@ -45,7 +45,7 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
45
45
  // outcome stands (runWithCleanRetry). The verdict-less first attempt is priced as its own
46
46
  // event (the feed must answer how long a check took), then the final event below records
47
47
  // the retry's classified outcome.
48
- const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur, cheaper)));
48
+ const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install, signal), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur, cheaper)));
49
49
  durationMs = d;
50
50
  return outcome;
51
51
  }, {
@@ -59,6 +59,23 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
59
59
  * unverified reject; anything else passes through. Never logs — the caller warns (see
60
60
  * warnScopedCheck). Returns the unverified reason only when a rule fired. */
61
61
  function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs) {
62
+ const sleepFailed = sleptThroughRun(raw);
63
+ if (!unverifiedSkipFor(scope, raw, sleepFailed))
64
+ return { outcome: raw, unverifiedReason: null };
65
+ const unverifiedReason = unverifiedReasonFor(scope, raw, effectiveMs, load, durationMs, sleepFailed);
66
+ return { outcome: unverifiedFailOutcome(check, raw, unverifiedReason), unverifiedReason };
67
+ }
68
+ /** True when the failed run slept past the tolerance — the slept-through rule. */
69
+ function sleptThroughRun(raw) {
70
+ // A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
71
+ // attributed to the change, not cached as a red baseline — rather than read as a second
72
+ // deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
73
+ // under the project's own check, even if the host napped through parts of it.
74
+ return raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
75
+ }
76
+ /** Whether a merge scope classifies this raw outcome as a deterministic unverified reject — the
77
+ * merge-scope skip rule. */
78
+ function unverifiedSkipFor(scope, raw, sleepFailed) {
62
79
  // A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
63
80
  // genuinely hung or oversized suite), made no verdict about the tree — and this is the check
64
81
  // whose whole job is to catch a semantic conflict before it merges, so it rejects
@@ -67,20 +84,19 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
67
84
  // slept, so a second verdict-less attempt is recorded as unverified rather than read again as
68
85
  // weather (BUGS.md 2026-09-28). no-npm and a broken toolchain still say nothing about the tree,
69
86
  // and the gate scope still proceeds to the model reviewer, which the merge path's own check backs
70
- // up. A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
71
- // attributed to the change, not cached as a red baseline — rather than read as a second
72
- // deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
73
- // under the project's own check, even if the host napped through parts of it.
74
- const sleepFailed = raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
87
+ // up.
75
88
  // A failed dependency install is the same at a merge scope: nothing ran against the tree as its
76
89
  // lockfile pins it, so it must not merge (BUGS.md 2026-10-01).
77
- const unverifiedSkip = MERGE_SCOPES.has(scope) &&
78
- (((raw.status === "skipped" &&
90
+ return (MERGE_SCOPES.has(scope) &&
91
+ ((raw.status === "skipped" &&
79
92
  (raw.skipReason === "timeout" || raw.skipReason === "killed" || raw.skipReason === "install")) ||
80
93
  sleepFailed));
81
- if (!unverifiedSkip)
82
- return { outcome: raw, unverifiedReason: null };
83
- const unverifiedReason = raw.skipReason === "install"
94
+ }
95
+ /** The unverified reason phrase for the rule's four arms — the reason chain, with the optional
96
+ * starved-host tail. The reason names the starved host only when the load says the team was the
97
+ * check's competition (a late-deadline wording already names its own cause). */
98
+ function unverifiedReasonFor(scope, raw, effectiveMs, load, durationMs, sleepFailed) {
99
+ return raw.skipReason === "install"
84
100
  ? installFailedPhrase(SCOPE_WORDS[scope].label, raw.install, UNVERIFIED_TREE)
85
101
  : raw.skipReason === "killed"
86
102
  ? killedPhrase(SCOPE_WORDS[scope].label, raw.killedBy ? { signal: raw.killedBy, durationMs } : undefined, UNVERIFIED_TREE)
@@ -90,7 +106,11 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
90
106
  // An on-time kill (the late-deadline wording already names its own cause) names the
91
107
  // starved host when the load average says the team was the check's competition.
92
108
  (raw.run?.deadlineLateMs ?? 0) > DEADLINE_LATE_TOLERANCE_MS ? "" : loadedHostPhrase(load)}`;
93
- const outcome = {
109
+ }
110
+ /** Assemble the deterministic `failed` outcome that carries the unverified reason — the
111
+ * failed-outcome assembly. */
112
+ function unverifiedFailOutcome(check, raw, unverifiedReason) {
113
+ return {
94
114
  status: "failed",
95
115
  script: checkScriptName(check),
96
116
  outputTail: [unverifiedReason],
@@ -98,12 +118,16 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
98
118
  unverified: true,
99
119
  ...(raw.install ? { install: raw.install } : {}),
100
120
  };
101
- return { outcome, unverifiedReason };
102
121
  }
103
122
  /** Warn for the two verdict-less roads: a skipped check (buildCheckSkipWarning) or an unverified
104
123
  * merge reject. Reads the scope word table and the killed signal. */
105
124
  function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, unverifiedReason) {
106
125
  if (outcome.status === "skipped") {
126
+ // A check the harness stopped on an abort signal is neither weather nor a skip to warn
127
+ // about: the caller (the gate pre-check) fails closed as aborted on its way out, so no
128
+ // "proceeding to model review" warning may suggest the check still stands.
129
+ if (outcome.skipReason === "aborted")
130
+ return;
107
131
  // Environmental — deliberately NOT fail-closed, so a hung build script cannot wedge every
108
132
  // code run into the 3-strike discard (gate) or a merge behind the merge lock.
109
133
  const w = SCOPE_WORDS[scope];
@@ -142,11 +166,11 @@ function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, un
142
166
  * merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
143
167
  * detects on its own, keeps running check.command. `permitWait` hears
144
168
  * about a wait for the check permit (the merge cell's "waiting for a check slot"). Never throws. */
145
- export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
169
+ export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad, signal) {
146
170
  const r = resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad);
147
171
  if (!r)
148
172
  return null;
149
- const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper);
173
+ const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper, signal);
150
174
  const { outcome, unverifiedReason } = unverifiedOutcomeFor(scope, r.check, raw, r.effectiveMs, r.load, durationMs);
151
175
  logEventBestEffort(root, buildCheckEvent(role, scope, outcome, durationMs, r.cheaper));
152
176
  warnScopedCheck(root, role, scope, outcome, r.effectiveMs, durationMs, unverifiedReason);
@@ -72,7 +72,7 @@ export function checkScriptName(check) {
72
72
  * branch state — which is what this check exists for; build-check.test.ts pins the
73
73
  * no-node_modules-worktree resolution. A configured command's cwd is resolved by
74
74
  * detectBuildCheck against the worktree the detection started from. */
75
- export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_MS, killGraceMs = KILL_GRACE_MS, sampleSleep = sampleSleepClock, install = npmInstall) {
75
+ export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_MS, killGraceMs = KILL_GRACE_MS, sampleSleep = sampleSleepClock, install = npmInstall, signal) {
76
76
  const script = checkScriptName(check);
77
77
  const effectiveMs = checkTimeoutMs(check, timeoutMs);
78
78
  // Environmental probe first: a toolchain broken below the project (git exiting 69 on an
@@ -105,13 +105,13 @@ export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_M
105
105
  if (r)
106
106
  installed = { packages: r.packages, durationMs: r.durationMs };
107
107
  }
108
- const outcome = await runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep);
108
+ const outcome = await runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep, signal);
109
109
  return installed ? { ...outcome, install: installed } : outcome;
110
110
  }
111
111
  /** runBuildCheck's run-and-classify step, once the probe and any owed install are done: spawn
112
- * the declared check as a process group under its deadline, bracketed by sleep samples, and
113
- * classify what it did. */
114
- async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep) {
112
+ * the declared check as a process group under its deadline — or, when `signal` fires, stopped
113
+ * by the harness as its own abort — bracketed by sleep samples, and classify what it did. */
114
+ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep, signal) {
115
115
  // Sleep evidence brackets the spawn: the opening sample is taken while the host is provably
116
116
  // awake (this code is running), so any sleep the closing sample can attribute began inside
117
117
  // the run's window. A platform with no readable clock leaves run.sleptMs unset — no evidence
@@ -123,12 +123,14 @@ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sam
123
123
  timeoutMs: effectiveMs,
124
124
  killGraceMs,
125
125
  maxBuffer: EXEC_MAX_BUFFER,
126
+ signal,
126
127
  })
127
128
  : await runScriptGroup("npm", ["run", check.script], {
128
129
  cwd: wt,
129
130
  timeoutMs: effectiveMs,
130
131
  killGraceMs,
131
132
  maxBuffer: EXEC_MAX_BUFFER,
133
+ signal,
132
134
  });
133
135
  // Spawn failed before anything ran — the runner is missing from PATH.
134
136
  if (r.spawnError)
@@ -143,6 +145,12 @@ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sam
143
145
  // deadline actually fired, which the warning reports when it was late.
144
146
  if (r.timedOut)
145
147
  return { status: "skipped", script, skipReason: "timeout", run };
148
+ // The harness stopped the tree on an abort signal (a user abort or a shutdown) — its own
149
+ // stop, not weather and not a verdict about the tree: classify it separately from a timeout
150
+ // and from an external killed signal so runScopedBuildCheck neither retries it nor warns it
151
+ // as a skip.
152
+ if (r.aborted)
153
+ return { status: "skipped", script, skipReason: "aborted", run };
146
154
  // The tree died on a signal the harness did NOT send (runScriptGroup resolves its own
147
155
  // timeout with timedOut already set, so a signal reaching this line is external — another
148
156
  // run's `pkill`, an operator's cleanup): the outcome says nothing about the tree, but it is
@@ -72,6 +72,15 @@ function failStrayArg(args, reason, ...claimed) {
72
72
  if (extra !== undefined)
73
73
  fail(`unexpected argument ${JSON.stringify(extra)} — ${reason}`);
74
74
  }
75
+ /** The two positions a valued flag claims for a free-form-prompt parser's stray-argument
76
+ * accounting: the flag token and its value — the flag and its value are never prompt content.
77
+ * Empty when the flag is absent, so a negative index never over-claims a real token. The one
78
+ * shape every valued-flag pair claims for the parsePromptFlags/parsePromptArgs stray-argument
79
+ * accounting — `--role`, `--grep`, `--at` in parsePromptFlags and `--file` in editPromptArgs
80
+ * ("claimed like --role's": each site is this same pair) — so no site can drift from the others. */
81
+ function pairClaim(flagIndex) {
82
+ return flagIndex >= 0 ? [flagIndex, flagIndex + 1] : [];
83
+ }
75
84
  /** Read a `--file <path>` prompt file: fail naming the file when it cannot be read. A raw
76
85
  * ENOENT/EISDIR names the path but not its role, so the message says this was the --file
77
86
  * prompt. Shared by the init and prompt parsers' path branches; the prompt parser's stdin
@@ -141,25 +150,22 @@ function rejectDuplicateFlags(args, flags) {
141
150
  for (const flag of flags)
142
151
  rejectRepeatFlag(args.filter((a) => a === flag).length > 1, flag);
143
152
  }
144
- /** `tumwater init` argument handling. Init's positionals are free-form prompt text, so
145
- * rejectUnknownArgs (which rejects ANY unconsumed token) can't be used wholesale. The rules
146
- * instead: a double-dash token must be a known flag from INIT_FLAG_SPECS (`--file`,
147
- * `--branch`, `--template`, `--adopt`, `--dry-run` or `--list-templates`), each given at most
148
- * once; with `--file` present nothing but the other flags may accompany it; a `--branch
149
- * <name>` pair and the valueless flags are never prompt content; single-dash positionals are
150
- * prompt content, not flags. Without these checks a misspelled --file would be baked into the
151
- * brief text — injected into every run of every role until someone edits the project
153
+ /** The flag-vocabulary gate for `tumwater init`: every double-dash token must be a known
154
+ * INIT_FLAG_SPECS flag, each given at most once. Without it a misspelled --file would be baked
155
+ * into the brief text — injected into every run of every role until someone edits the project
152
156
  * brief. */
153
- export function parseInitArgs(args) {
157
+ function assertInitFlags(args) {
154
158
  const known = INIT_FLAG_SPECS.flatMap((s) => s.names);
155
159
  // The unknown-double-dash gate reads the specs, so the list its error names admits exactly
156
160
  // what the duplicate-flag check below accepts — one vocabulary, one message renderer.
157
161
  rejectUnknownDoubleDash("init", args, INIT_FLAG_SPECS);
158
162
  rejectDuplicateFlags(args, known);
159
- const branch = parseBranchFlag(args);
160
- const adopt = args.includes("--adopt");
161
- const dryRun = args.includes("--dry-run");
162
- const listTemplates = args.includes("--list-templates");
163
+ }
164
+ /** Resolve `--template <id>` from the argument list, refusing an empty id, an id unknown to
165
+ * the catalog, and a `--template` given alongside `--list-templates`. Validated here so an
166
+ * unknown id fails before initProject runs any side effect — before a half-seeded repo can
167
+ * result from a typo'd id. */
168
+ function resolveInitTemplate(args, listTemplates) {
163
169
  // Validate the template id here, so an unknown one fails before initProject runs any side
164
170
  // effect — and before a half-seeded repo can result from a typo'd id.
165
171
  const templateFlag = args.indexOf("--template");
@@ -175,29 +181,40 @@ export function parseInitArgs(args) {
175
181
  if (listTemplates && templateFlag >= 0) {
176
182
  fail("--list-templates takes no --template — it lists the catalog, it does not use one");
177
183
  }
184
+ return template;
185
+ }
186
+ /** The `--file` branch of `parseInitArgs`: when the flag is present the prompt comes from the
187
+ * file, so every other positional is claimed (refused as stray) and the file's exact contents
188
+ * are read as the prompt — unless `--list-templates` conflicts, which takes no prompt at all.
189
+ * Returns null when no `--file` flag is present. */
190
+ function resolveInitFileArg(args, base) {
178
191
  const fileFlag = args.indexOf("--file");
179
- if (fileFlag >= 0) {
180
- const file = promptFileValue(args[fileFlag + 1]);
181
- const claimed = [fileFlag, fileFlag + 1];
182
- const branchFlag = args.indexOf("--branch");
183
- if (branchFlag >= 0)
184
- claimed.push(branchFlag, branchFlag + 1);
185
- if (templateFlag >= 0)
186
- claimed.push(templateFlag, templateFlag + 1);
187
- for (const flag of INIT_BOOLEAN_FLAGS) {
188
- if (args.includes(flag))
189
- claimed.push(args.indexOf(flag));
190
- }
191
- failStrayArg(args, "with --file the prompt comes from the file", ...claimed);
192
- const contents = readPromptFileChecked(file);
193
- if (listTemplates) {
194
- fail(LIST_TEMPLATES_PROMPT_ERROR);
195
- }
196
- return { prompt: contents, branch, template, listTemplates, adopt, dryRun };
192
+ if (fileFlag < 0)
193
+ return null;
194
+ const file = promptFileValue(args[fileFlag + 1]);
195
+ const claimed = [fileFlag, fileFlag + 1];
196
+ const branchFlag = args.indexOf("--branch");
197
+ if (branchFlag >= 0)
198
+ claimed.push(branchFlag, branchFlag + 1);
199
+ const templateFlag = args.indexOf("--template");
200
+ if (templateFlag >= 0)
201
+ claimed.push(templateFlag, templateFlag + 1);
202
+ for (const flag of INIT_BOOLEAN_FLAGS) {
203
+ if (args.includes(flag))
204
+ claimed.push(args.indexOf(flag));
205
+ }
206
+ failStrayArg(args, "with --file the prompt comes from the file", ...claimed);
207
+ const contents = readPromptFileChecked(file);
208
+ if (base.listTemplates) {
209
+ fail(LIST_TEMPLATES_PROMPT_ERROR);
197
210
  }
198
- // Everything except the --branch/--template pairs and the booleans is prompt text; the join
199
- // keeps single-dash tokens as content, exactly as before — with no flag present this is the
200
- // old args.join(" ").
211
+ return { ...base, prompt: contents };
212
+ }
213
+ /** Reconstruct the positional prompt text for `tumwater init`: everything except the
214
+ * `--branch`/`--template` pairs and the valueless flags is prompt text. The join keeps
215
+ * single-dash tokens as content, exactly as before — with no flag present this is the old
216
+ * args.join(" "). */
217
+ function joinInitPrompt(args) {
201
218
  const promptTokens = [];
202
219
  for (let i = 0; i < args.length; i++) {
203
220
  const arg = args[i];
@@ -206,11 +223,29 @@ export function parseInitArgs(args) {
206
223
  else if (!INIT_BOOLEAN_FLAGS.includes(arg))
207
224
  promptTokens.push(arg);
208
225
  }
209
- const prompt = promptTokens.join(" ");
210
- if (listTemplates && prompt.trim() !== "") {
226
+ return promptTokens.join(" ");
227
+ }
228
+ /** `tumwater init` argument handling. Init's positionals are free-form prompt text, so
229
+ * rejectUnknownArgs (which rejects ANY unconsumed token) can't be used wholesale. The rules
230
+ * instead: a double-dash token must be a known flag from INIT_FLAG_SPECS, each given at most
231
+ * once; with `--file` present nothing but the other flags may accompany it; a `--branch
232
+ * <name>` pair and the valueless flags are never prompt content; single-dash positionals are
233
+ * prompt content, not flags. */
234
+ export function parseInitArgs(args) {
235
+ assertInitFlags(args);
236
+ const branch = parseBranchFlag(args);
237
+ const adopt = args.includes("--adopt");
238
+ const dryRun = args.includes("--dry-run");
239
+ const listTemplates = args.includes("--list-templates");
240
+ const template = resolveInitTemplate(args, listTemplates);
241
+ const base = { branch, template, listTemplates, adopt, dryRun };
242
+ const fromFile = resolveInitFileArg(args, base);
243
+ if (fromFile !== null)
244
+ return fromFile;
245
+ const prompt = joinInitPrompt(args);
246
+ if (listTemplates && prompt.trim() !== "")
211
247
  fail(LIST_TEMPLATES_PROMPT_ERROR);
212
- }
213
- return { prompt, branch, template, listTemplates, adopt, dryRun };
248
+ return { ...base, prompt };
214
249
  }
215
250
  /** The stdin prompt read once per process: cli.ts pre-parses prompt's args and cmdPrompt
216
251
  * re-parses them, and a second readFileSync(0) on a drained pipe would see an empty prompt. */
@@ -306,7 +341,7 @@ function parsePromptRole(args, roleFlag) {
306
341
  const role = roleRaw ?? null;
307
342
  // The flag and its value are never prompt content: --role is a scope, not text. Only claimed
308
343
  // when actually present — negative indexes would over-claim real tokens.
309
- const roleClaim = roleFlag >= 0 ? [roleFlag, roleFlag + 1] : [];
344
+ const roleClaim = pairClaim(roleFlag);
310
345
  return { role, roleClaim };
311
346
  }
312
347
  /** The `--json` list-only refusal: in enqueue or cancel mode it must never silently ride
@@ -325,7 +360,7 @@ function parsePromptGrep(args, scan) {
325
360
  if (scan.grepFlag >= 0 && (!grepRaw || grepRaw.startsWith("--"))) {
326
361
  fail("--grep needs text (usage: tumwater prompt --list --grep <text>)");
327
362
  }
328
- const grepClaim = scan.grepFlag >= 0 ? [scan.grepFlag, scan.grepFlag + 1] : [];
363
+ const grepClaim = pairClaim(scan.grepFlag);
329
364
  return { grepRaw, grepClaim };
330
365
  }
331
366
  /** The queue-only-mode refusals: --file and --at have nothing to mean except in the enqueue
@@ -365,7 +400,7 @@ function parsePromptDeferral(args, atFlag) {
365
400
  // An edit keeps the target prompt's existing deferral exactly as it was (the marker is
366
401
  // plumbing the edit carries over), so a --at alongside it has nothing to mean.
367
402
  // The pair is claimed like --role's: the flag and its value are never prompt content.
368
- const atClaim = atFlag >= 0 ? [atFlag, atFlag + 1] : [];
403
+ const atClaim = pairClaim(atFlag);
369
404
  let atDelayMs = null;
370
405
  if (atFlag >= 0) {
371
406
  atDelayMs = parseDurationFlag("--at", args[atFlag + 1]);
@@ -415,13 +450,19 @@ function listPromptArgs(args, f) {
415
450
  failStrayArg(args, "with --list there is no prompt text", f.listFlag, ...f.jsonFlags, ...f.roleClaim, ...f.grepClaim);
416
451
  return { mode: "list", role: f.role, json: f.jsonFlags.length > 0, grep: f.grepRaw ?? null };
417
452
  }
453
+ /** The shared `--cancel <n>` / `--top <n>` mode: each takes exactly one 1-based position
454
+ * and refuses any other token — these modes have no prompt text. Shared so the two cannot
455
+ * drift; `mode` drives both the refusal wording and the returned mode. */
456
+ function positionOnlyPromptArgs(args, f, mode, flag, flagIndex) {
457
+ const n = parsePositionFlag(flag, args[flagIndex + 1], `${flag} 2`);
458
+ // The position is the only token this mode may carry; anything else alongside it would be
459
+ // prompt text, and this mode has none.
460
+ failStrayArg(args, `with ${flag} there is no prompt text`, flagIndex, flagIndex + 1, ...f.roleClaim);
461
+ return { mode, role: f.role, position: n };
462
+ }
418
463
  /** `--cancel <n>`: remove one entry by its 1-based queue position. */
419
464
  function cancelPromptArgs(args, f) {
420
- const n = parsePositionFlag("--cancel", args[f.cancelFlag + 1], "--cancel 2");
421
- // The position is the only token --cancel may carry; anything else alongside it would be
422
- // prompt text, and this mode has none.
423
- failStrayArg(args, "with --cancel there is no prompt text", f.cancelFlag, f.cancelFlag + 1, ...f.roleClaim);
424
- return { mode: "cancel", role: f.role, position: n };
465
+ return positionOnlyPromptArgs(args, f, "cancel", "--cancel", f.cancelFlag);
425
466
  }
426
467
  /** `--edit <n>`: rewrite one entry's text in place — the replacement is the --file contents,
427
468
  * the tokens after the position, or (with neither) an editor open in the caller. */
@@ -432,7 +473,7 @@ function editPromptArgs(args, f) {
432
473
  // a mistyped command would edit prompt N with only the trailing fragment — so they are
433
474
  // refused here by the same stray-argument wording the sibling modes use; tokens after the
434
475
  // position are the replacement text, minus any claimed attach pair (none: edit in $EDITOR).
435
- const fileClaim = f.fileFlag >= 0 ? [f.fileFlag, f.fileFlag + 1] : [];
476
+ const fileClaim = pairClaim(f.fileFlag);
436
477
  for (let i = 0; i < f.editFlag; i++) {
437
478
  if (f.roleClaim.includes(i) || f.attachClaims.includes(i) || fileClaim.includes(i))
438
479
  continue;
@@ -457,11 +498,7 @@ function editPromptArgs(args, f) {
457
498
  }
458
499
  /** `--top <n>`: move one entry to the head of its role's queue. */
459
500
  function topPromptArgs(args, f) {
460
- const n = parsePositionFlag("--top", args[f.topFlag + 1], "--top 2");
461
- // The position is the only token --top may carry; anything else alongside it would be
462
- // prompt text, and this mode has none.
463
- failStrayArg(args, "with --top there is no prompt text", f.topFlag, f.topFlag + 1, ...f.roleClaim);
464
- return { mode: "top", role: f.role, position: n };
501
+ return positionOnlyPromptArgs(args, f, "top", "--top", f.topFlag);
465
502
  }
466
503
  /** `--file <path>`: enqueue the file's (or stdin's) whole contents as the prompt. */
467
504
  function filePromptArgs(args, f) {
@@ -465,35 +465,55 @@ export const GUI_FLAG_SPECS = [PORT_FLAG, ALL_INTERFACES_FLAG, TOKEN_FLAG];
465
465
  export function rejectUnknownArgs(command, args, specs) {
466
466
  if (args.length === 0)
467
467
  return;
468
- const claim = new Map();
469
- for (const spec of specs)
470
- for (const name of spec.names)
471
- claim.set(name, spec);
468
+ const claim = flagClaims(specs);
472
469
  const seen = new Set();
473
470
  const consumed = new Array(args.length).fill(false);
474
471
  for (let i = 0; i < args.length; i++) {
475
472
  if (consumed[i])
476
473
  continue;
477
474
  const arg = args[i] ?? ""; // Unreachable fallback: the loop bound guarantees a token here.
478
- const spec = claim.get(arg);
479
- if (spec === undefined) {
480
- // An equals-form token names a real flag; refuse it with its own message before the
481
- // generic "unknown argument" misreports `--role=feature` as a misspelling.
482
- rejectEqualsForm(arg, specs);
483
- if (specs.length === 0)
484
- fail(`tumwater ${command} takes no arguments${gotSuffix(arg)}`);
485
- failUnknownArgument(command, arg, specs);
486
- }
475
+ const spec = claim.get(arg) ?? rejectUnknownFlag(command, arg, specs);
487
476
  rejectRepeatFlag(seen.has(spec), spec.names[0]);
488
477
  seen.add(spec);
489
- if (spec.value && i === args.length - 1)
490
- fail(spec.missingValue ?? `${spec.names[0]} needs a value`);
491
- // The value exists (the trailing-no-value case failed above): name a malformed one now,
492
- // before the ready-repo gate or any environment check can mask it.
493
- if (spec.value)
494
- spec.validate?.(args[i + 1] ?? "");
495
- const n = spec.value ? 2 : 1;
496
- for (let j = 0; j < n && i + j < args.length; j++)
497
- consumed[i + j] = true;
478
+ consumeFlagOccurrence(spec, args, i, consumed);
498
479
  }
499
480
  }
481
+ /** The spelling-to-spec claim map rejectUnknownArgs keyed its loop on: every accepted flag
482
+ * spelling maps to the spec declaring it, alias spellings (-f and --follow) sharing one spec
483
+ * entry. Built once up front so a token look-up is a map get, not a scan. */
484
+ function flagClaims(specs) {
485
+ const claim = new Map();
486
+ for (const spec of specs)
487
+ for (const name of spec.names)
488
+ claim.set(name, spec);
489
+ return claim;
490
+ }
491
+ /** The one refusal for a token naming no known flag: an equals-form token first reroutes
492
+ * through rejectEqualsForm, which names a REAL flag's offending `--flag=value` spelling
493
+ * before the generic "unknown argument" misreports `--role=feature` as a misspelling; then
494
+ * a command with no flags at all fails with the takes-no-arguments wording; otherwise the
495
+ * shared unknown-argument wording (with its did-you-mean hint) fires. Returns never, so the
496
+ * caller's `claim.get(arg) ?? rejectUnknownFlag(...)` narrows `spec` to a known FlagSpec. */
497
+ function rejectUnknownFlag(command, arg, specs) {
498
+ // An equals-form token names a real flag; refuse it with its own message before the
499
+ // generic "unknown argument" misreports `--role=feature` as a misspelling.
500
+ rejectEqualsForm(arg, specs);
501
+ if (specs.length === 0)
502
+ fail(`tumwater ${command} takes no arguments${gotSuffix(arg)}`);
503
+ failUnknownArgument(command, arg, specs);
504
+ }
505
+ /** Consume one recognised flag occurrence: fail a valued flag left without its value, name a
506
+ * malformed value via the spec's validate call, and mark the one or two tokens (the flag and,
507
+ * for a valued flag, its value) as consumed so a later loop iteration skips them. The value
508
+ * is marked only after the value checks pass, so a failed value is never silently swallowed. */
509
+ function consumeFlagOccurrence(spec, args, i, consumed) {
510
+ if (spec.value && i === args.length - 1)
511
+ fail(spec.missingValue ?? `${spec.names[0]} needs a value`);
512
+ // The value exists (the trailing-no-value case failed above): name a malformed one now,
513
+ // before the ready-repo gate or any environment check can mask it.
514
+ if (spec.value)
515
+ spec.validate?.(args[i + 1] ?? "");
516
+ const n = spec.value ? 2 : 1;
517
+ for (let j = 0; j < n && i + j < args.length; j++)
518
+ consumed[i + j] = true;
519
+ }
@@ -12,8 +12,7 @@ import { templateCatalog } from "../init/init-templates.js";
12
12
  export async function cmdInit(root, args) {
13
13
  const { prompt, branch, template, listTemplates, adopt, dryRun } = parseInitArgs(args);
14
14
  if (listTemplates) {
15
- for (const { id, description } of templateCatalog())
16
- say(`${id} — ${description}`);
15
+ printTemplateCatalog();
17
16
  return;
18
17
  }
19
18
  const result = await initProject(root, prompt, branch ?? undefined, {
@@ -21,21 +20,44 @@ export async function cmdInit(root, args) {
21
20
  dryRun,
22
21
  template: template ?? undefined,
23
22
  });
23
+ printInitIntro(result);
24
+ if (result.dryRun) {
25
+ printDryRun(result);
26
+ return;
27
+ }
28
+ printInitReport(result);
29
+ }
30
+ /** Print the `tumwater init --list-templates` catalog: one line per template. */
31
+ function printTemplateCatalog() {
32
+ for (const { id, description } of templateCatalog())
33
+ say(`${id} — ${description}`);
34
+ }
35
+ /** Print the `template:`/adopt intro lines shared by the dry-run and completed paths. */
36
+ function printInitIntro(result) {
24
37
  if (result.template !== "blank")
25
38
  say(`template: ${result.template}`);
26
39
  if (result.adopted) {
27
40
  say(`${result.dryRun ? "would adopt" : "adopting"} an existing repo: the project brief goes in TUMWATER.md and README.md is left untouched`);
28
41
  }
29
- if (result.dryRun) {
30
- if (result.repoInitialized && result.branch) {
31
- say(`dry run — would initialize a new git repository on branch ${result.branch}`);
32
- }
33
- const list = (names) => (names.length > 0 ? names.join(", ") : "nothing");
34
- say(`dry run — would create: ${list(result.created)}`);
35
- say(`would leave alone: ${list(result.leftAlone)}`);
36
- say("nothing written; re-run without --dry-run to apply");
37
- return;
42
+ }
43
+ /** Print the dry-run preview: repo line, the would-create/would-leave-alone lists, and the
44
+ * "nothing written" line. */
45
+ function printDryRun(result) {
46
+ if (result.repoInitialized && result.branch) {
47
+ say(`dry run — would initialize a new git repository on branch ${result.branch}`);
38
48
  }
49
+ const list = (names) => (names.length > 0 ? names.join(", ") : "nothing");
50
+ say(`dry run — would create: ${list(result.created)}`);
51
+ say(`would leave alone: ${list(result.leftAlone)}`);
52
+ say("nothing written; re-run without --dry-run to apply");
53
+ }
54
+ /** Format the created-files line, appending `(committed)` when the repo was seeded. */
55
+ function createdLine(result) {
56
+ return `created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`;
57
+ }
58
+ /** Print the completed-init report: the already-initialized no-op, the repo line, the
59
+ * created-files line, the bootstrap note, and the "next:" line. */
60
+ function printInitReport(result) {
39
61
  if (result.created.length === 0) {
40
62
  say("already initialized; nothing to do");
41
63
  return;
@@ -43,7 +65,7 @@ export async function cmdInit(root, args) {
43
65
  if (result.repoInitialized && result.branch) {
44
66
  say(`initialized a new git repository on branch ${result.branch}`);
45
67
  }
46
- say(`created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`);
68
+ say(createdLine(result));
47
69
  if (result.bootstrap !== null) {
48
70
  say(`fresh project — maintenance roles wait until ${result.bootstrap} plans are done; remove "bootstrap" from tumwater.json to end that early`);
49
71
  }
@@ -25,7 +25,7 @@ import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.j
25
25
  import { mergeChanges } from "../merge/merge-slot.js";
26
26
  import { shortSha } from "../text/format.js";
27
27
  import { roleLabel } from "../roles/role-labels.js";
28
- import { orchestratorAlive } from "../team/orchestrator-info.js";
28
+ import { orchestratorAlive } from "../team/team-orchestrator-info.js";
29
29
  import { queueInFlight } from "../status/status-polls.js";
30
30
  import { roleInScope } from "../roles/runner-ids.js";
31
31
  /** `tumwater status [-f/--follow]`: the team snapshot as rendered text or as the JSON
@@ -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. */