tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -3,7 +3,7 @@ import { betweenWindow, durationLabel, failRivalShapes, failUntilShorterThanSinc
3
3
  import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, HISTORY_SUMMARY_GROUPS, historyClass, parseHistoryResultClass, readRunRows, readRunRowsSince, runRows, summarizeRunRows, summarizeRunRowsByModel, runRowsMatchingGrep, runRowsOnModels } from "./history-data.js";
4
4
  import { renderModelSummary, renderRow, renderSummary, widenWidths, widestWidths } from "./history-render.js";
5
5
  import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
6
- import { followUntilParentGone } from "../cli/log-commands.js";
6
+ import { followUntilParentGone } from "../cli/cli-log-commands.js";
7
7
  import { hasFollowFlag } from "../cli/cli-flag-specs.js";
8
8
  import { parseEventLine, readEventsTailWithEnd } from "../events/event-read.js";
9
9
  import { eventsLogPath } from "../paths.js";
@@ -23,7 +23,7 @@ export function parseHistoryModelList(raw) {
23
23
  return pieces;
24
24
  }
25
25
  /** `tumwater history [--role <id>[,<id>...]] [-n N]`: one row per completed run, newest first. The
26
- * observing half beside cmdLogs (log-commands.ts): read-only over the event log, stdout only —
26
+ * observing half beside cmdLogs (cli-log-commands.ts): read-only over the event log, stdout only —
27
27
  * every datum rides the `run_end` events src/run/run-finalize.ts already writes, so this
28
28
  * is a rendering of the existing record, not a new one. The rows themselves are collected
29
29
  * by history-data.ts (beside src/report/report-data.ts and src/failure/failure-data.ts), and
@@ -31,7 +31,7 @@ export function parseHistoryModelList(raw) {
31
31
  * src/report/report-render.ts); this module parses the args, drives the CLI, and prints. */
32
32
  /** The missing-pattern error cmdHistory prints for a valueless or empty `--grep`, exported so
33
33
  * cli.ts's rejectUnknownArgs spec for --grep can fail a trailing `history --grep` with the same
34
- * wording. Kept local to this module — log-commands.ts holds its own GREP_VALUE_ERROR — so the
34
+ * wording. Kept local to this module — cli-log-commands.ts holds its own GREP_VALUE_ERROR — so the
35
35
  * two observing views' wordings each name the command that printed them. */
36
36
  export const HISTORY_GREP_VALUE_ERROR = "history --grep needs a pattern";
37
37
  /** The missing-value error cmdHistory prints for a valueless or empty `--result`, exported so
@@ -166,11 +166,9 @@ function applyHistoryModelFilter(rows, rest) {
166
166
  }
167
167
  return { rows, models };
168
168
  }
169
- /** Render the collected-and-filtered rows: the `--by`/`--summary` summary grouping, the `--json`
170
- * document, the empty-state wording (blaming the filter that emptied the set, else the log or
171
- * window), the aligned table, and the sparse-window note. A bad `--by` value fails here, after
172
- * row collection and filtering and before `--json`. */
173
- function sayHistory(rows, q, covered, filtered) {
169
+ /** Validate `--by`'s value and its `--summary` requirement; return whether the grouping is by
170
+ * model (false for `role`). One home for "can this `--by` proceed, and does it group by model?" */
171
+ function historySummaryGroup(q) {
174
172
  // `--by role|model` picks how --summary groups its lines (role by default).
175
173
  const byRaw = flagValue(q.rest, "--by");
176
174
  let byModel = false;
@@ -184,6 +182,52 @@ function sayHistory(rows, q, covered, filtered) {
184
182
  fail("history --by needs --summary (it picks how the summary groups runs)");
185
183
  byModel = byRaw === "model";
186
184
  }
185
+ return byModel;
186
+ }
187
+ /** The empty-set prose: blame the filter that emptied the set (model, then result, then grep),
188
+ * else the log or window. Never the `no runs yet`/`no runs in <window>` prose when rows were
189
+ * scanned and filtered out — the runs are there, none match. */
190
+ function sayHistoryEmpty(q, filtered) {
191
+ // A grep that matched nothing must not borrow the empty-log prose: `no runs yet` and
192
+ // `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
193
+ // rows were scanned and filtered out — the operator would read a quiet team where the
194
+ // truth is `the runs are there, none match`. The logs --grep rule (its `no events
195
+ // matching "<pattern>"`, src/cli/cli-log-commands.ts) names the pattern instead; --json stays
196
+ // a bare {"rows":[]} document above, never prose in either form.
197
+ //
198
+ // The result class is named only when it emptied a non-empty set (`afterGrep > 0`): an
199
+ // empty log or window scanned no rows, so the result filter removed nothing and the
200
+ // empty-log/window prose is the true one — the dashboard's History view draws the same
201
+ // line. A grep that admitted nothing keeps its own wording regardless of --result.
202
+ say(filtered.models !== null && filtered.afterResult > 0
203
+ ? `no runs on model "${filtered.models.join(",")}"`
204
+ : filtered.classes !== null && filtered.afterGrep > 0
205
+ ? `no runs with result "${filtered.classes.join(",")}"`
206
+ : q.grepPattern !== null
207
+ ? `no runs matching "${q.grepPattern}"`
208
+ : q.sinceMs === null
209
+ ? "no runs yet"
210
+ : q.untilMs === null
211
+ ? `no runs in ${durationLabel(q.sinceMs)}`
212
+ : `no runs ${betweenWindow(q.sinceMs, q.untilMs)}`);
213
+ }
214
+ /** Render the non-empty rows: the summary (model or role) when `--summary`, else the aligned
215
+ * table. Called only with `rows.length > 0`.
216
+ */
217
+ function sayHistoryRows(rows, summary, byModel) {
218
+ if (summary)
219
+ say(byModel ? renderModelSummary(summarizeRunRowsByModel(rows)) : renderSummary(summarizeRunRows(rows)));
220
+ else {
221
+ const widths = widestWidths(rows);
222
+ say(rows.map((r) => renderRow(r, widths)).join("\n"));
223
+ }
224
+ }
225
+ /** Render the collected-and-filtered rows: the `--by`/`--summary` summary grouping, the `--json`
226
+ * document, the empty-state wording (blaming the filter that emptied the set, else the log or
227
+ * window), the aligned table, and the sparse-window note. A bad `--by` value fails here, after
228
+ * row collection and filtering and before `--json`. */
229
+ function sayHistory(rows, q, covered, filtered) {
230
+ const byModel = historySummaryGroup(q);
187
231
  const summary = q.rest.includes("--summary") && rows.length > 0;
188
232
  // --json swaps the renderer for the collector's own payload, exactly as status --json and
189
233
  // report --json: the same rows the table prints, each with ts/tokens/costUsd kept raw. A
@@ -194,36 +238,10 @@ function sayHistory(rows, q, covered, filtered) {
194
238
  return;
195
239
  }
196
240
  if (rows.length === 0) {
197
- // A grep that matched nothing must not borrow the empty-log prose: `no runs yet` and
198
- // `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
199
- // rows were scanned and filtered out — the operator would read a quiet team where the
200
- // truth is `the runs are there, none match`. The logs --grep rule (its `no events
201
- // matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
202
- // a bare {"rows":[]} document above, never prose in either form.
203
- //
204
- // The result class is named only when it emptied a non-empty set (`afterGrep > 0`): an
205
- // empty log or window scanned no rows, so the result filter removed nothing and the
206
- // empty-log/window prose is the true one — the dashboard's History view draws the same
207
- // line. A grep that admitted nothing keeps its own wording regardless of --result.
208
- say(filtered.models !== null && filtered.afterResult > 0
209
- ? `no runs on model "${filtered.models.join(",")}"`
210
- : filtered.classes !== null && filtered.afterGrep > 0
211
- ? `no runs with result "${filtered.classes.join(",")}"`
212
- : q.grepPattern !== null
213
- ? `no runs matching "${q.grepPattern}"`
214
- : q.sinceMs === null
215
- ? "no runs yet"
216
- : q.untilMs === null
217
- ? `no runs in ${durationLabel(q.sinceMs)}`
218
- : `no runs ${betweenWindow(q.sinceMs, q.untilMs)}`);
241
+ sayHistoryEmpty(q, filtered);
219
242
  return;
220
243
  }
221
- if (summary)
222
- say(byModel ? renderModelSummary(summarizeRunRowsByModel(rows)) : renderSummary(summarizeRunRows(rows)));
223
- else {
224
- const widths = widestWidths(rows);
225
- say(rows.map((r) => renderRow(r, widths)).join("\n"));
226
- }
244
+ sayHistoryRows(rows, summary, byModel);
227
245
  // A sparse window is never mistaken for a quiet team — but the note only ever rides rows
228
246
  // (an empty window returned above), it never touches --json output, and it is the shared
229
247
  // SPARSE_WINDOW_NOTE wording: the oldest retained event being inside the window is exactly
@@ -179,10 +179,10 @@ function runPromptImageGate(root, target) {
179
179
  if (guidance.notice)
180
180
  say(guidance.notice);
181
181
  }
182
- /** `--edit <n>` mode — replace a queued prompt's body by position: the attachment gate, then
183
- * the editor for a bare position or the typed/`--file` text, list-wide or `--role`-scoped. One
184
- * helper so `--edit`'s rules are found by name. */
185
- function runPromptEdit(root, position, text, attachPaths, role) {
182
+ /** The `--edit` attachment phase: read the `--attach` image list (undefined when there is
183
+ * none) and run the image-input gate under the role the edit targets — the position's
184
+ * resolved role when no `--role` narrows it. */
185
+ function readEditAttachments(root, position, attachPaths, role) {
186
186
  // The same broken-config policy and list-wide resolution as cancel, one verb over: the
187
187
  // file's new content rides the same queue file (position, enqueue stamp, and a pending
188
188
  // --at deferral untouched), and a concurrent dequeue reports gone and exits clean. An
@@ -200,48 +200,66 @@ function runPromptEdit(root, position, text, attachPaths, role) {
200
200
  const targetRole = role ?? resolveListedRole(root, position);
201
201
  runPromptImageGate(root, targetRole);
202
202
  }
203
- const typed = text;
204
- if (typed === null) {
205
- // Bare `--edit <n>`: open the prompt's body in the editor. The role is resolved first
206
- // and the write goes by file name, so a dequeue while the editor is open reports gone
207
- // instead of editing the neighbour that shifted into the position.
208
- const targetRole = role ?? resolveListedRole(root, position);
209
- let name;
210
- let body;
211
- try {
212
- name = path.basename(queuedFileAtPosition(root, targetRole, position));
213
- body = readQueuedPromptBody(root, targetRole, name);
214
- }
215
- catch (err) {
216
- return fail(errorMessage(err));
217
- }
218
- if (body === null) {
219
- sayPromptOutcome(position, targetRole, { status: "gone" }, role === null);
220
- return;
221
- }
222
- const after = editInEditor(body, "prompt.md");
223
- // An emptied buffer is an abort, not a blank prompt: the queue would otherwise hold a
224
- // prompt with no text, which the enqueue forms refuse.
225
- if (after.trim() === "" && body.trim() !== "")
226
- fail("the edited prompt is empty; nothing changed (use --cancel <n> to drop it)");
227
- if (after.trim() === body.trim() && images === undefined) {
228
- say(`prompt ${position} unchanged`);
229
- return;
230
- }
231
- try {
232
- sayPromptOutcome(position, targetRole, editQueuedFile(root, targetRole, name, after, images), role === null);
233
- }
234
- catch (err) {
235
- fail(errorMessage(err));
236
- }
203
+ return images;
204
+ }
205
+ /** The bare `--edit <n>` arm: resolve the role, read the queued body by file name (so a
206
+ * concurrent dequeue reports gone), open it in the editor and write the result back —
207
+ * unchanged, emptied or edited — with the carried `images`. */
208
+ function editBareQueuedPrompt(root, position, role, images) {
209
+ // Bare `--edit <n>`: open the prompt's body in the editor. The role is resolved first
210
+ // and the write goes by file name, so a dequeue while the editor is open reports gone
211
+ // instead of editing the neighbour that shifted into the position.
212
+ const targetRole = role ?? resolveListedRole(root, position);
213
+ let name;
214
+ let body;
215
+ try {
216
+ name = path.basename(queuedFileAtPosition(root, targetRole, position));
217
+ body = readQueuedPromptBody(root, targetRole, name);
218
+ }
219
+ catch (err) {
220
+ return fail(errorMessage(err));
221
+ }
222
+ if (body === null) {
223
+ sayPromptOutcome(position, targetRole, { status: "gone" }, role === null);
237
224
  return;
238
225
  }
226
+ const after = editInEditor(body, "prompt.md");
227
+ // An emptied buffer is an abort, not a blank prompt: the queue would otherwise hold a
228
+ // prompt with no text, which the enqueue forms refuse.
229
+ if (after.trim() === "" && body.trim() !== "")
230
+ fail("the edited prompt is empty; nothing changed (use --cancel <n> to drop it)");
231
+ if (after.trim() === body.trim() && images === undefined) {
232
+ say(`prompt ${position} unchanged`);
233
+ return;
234
+ }
235
+ try {
236
+ sayPromptOutcome(position, targetRole, editQueuedFile(root, targetRole, name, after, images), role === null);
237
+ }
238
+ catch (err) {
239
+ fail(errorMessage(err));
240
+ }
241
+ }
242
+ /** The typed `--edit <n> <text>` / `--file` arm: the list-wide `editListedPrompt` path when
243
+ * no `--role`, else the role-scoped `editRolePrompt` command, with the carried `images`. */
244
+ function editTypedQueuedPrompt(root, position, text, role, images) {
239
245
  if (role === null) {
240
- sayListedOutcome(editListedPrompt(root, listWideScope(root), position, typed, images), position);
246
+ sayListedOutcome(editListedPrompt(root, listWideScope(root), position, text, images), position);
241
247
  return;
242
248
  }
243
249
  const target = role ?? DIRECTOR_ROLE;
244
- sayPromptOutcome(position, target, runRolePromptCommand(root, target, position, (r, role, position) => editRolePrompt(r, role, position, typed, images)), false);
250
+ sayPromptOutcome(position, target, runRolePromptCommand(root, target, position, (r, role, position) => editRolePrompt(r, role, position, text, images)), false);
251
+ }
252
+ /** `--edit <n>` mode — replace a queued prompt's body by position: read the `--attach` image
253
+ * list through the gate, then the editor for a bare position or the typed/`--file` text,
254
+ * list-wide or `--role`-scoped. The three phases live in `readEditAttachments`,
255
+ * `editBareQueuedPrompt` and `editTypedQueuedPrompt`, so each `--edit` job is found by name. */
256
+ function runPromptEdit(root, position, text, attachPaths, role) {
257
+ const images = readEditAttachments(root, position, attachPaths, role);
258
+ if (text === null) {
259
+ editBareQueuedPrompt(root, position, role, images);
260
+ return;
261
+ }
262
+ editTypedQueuedPrompt(root, position, text, role, images);
245
263
  }
246
264
  /** `--top <n>` mode — move a queued prompt to the head of its queue by position, list-wide or
247
265
  * `--role`-scoped. One helper so `--top`'s rules are found by name. */
@@ -283,8 +301,7 @@ function runPromptSubmit(root, text, atDelayMs, attachPaths, role) {
283
301
  // `--attach <path>`: read and validate through the shared helper (a bad path fails by name
284
302
  // before anything is queued). An attachment the target role cannot read is refused here by
285
303
  // name — the same gate submitRolePrompt enforces, surfaced early so the CLI exits non-zero
286
- // with the fix before anything is written or queued; a role that reroutes a blind read is
287
- // not refused, but the operator is told where the prompt will actually run.
304
+ // with the fix before anything is written or queued.
288
305
  const images = attachPaths.length > 0 ? readPromptImages(attachPaths) : undefined;
289
306
  if (images && images.length > 0) {
290
307
  runPromptImageGate(root, target);
@@ -234,26 +234,8 @@ async function resolveInitPlan(root, briefText, branch, opts) {
234
234
  const preferredBranch = await resolveInitPreferredBranch(root, branch, wasRepo);
235
235
  return { dryRun, tpl, combinedPrompt, brief, readmeExists, adopted, wasRepo, preferredBranch };
236
236
  }
237
- /** Seed `root` from a resolved `InitPlan`: git init (a new repo only), every absent project
238
- * file and starter directory, the config, and the commit. Holds the `created`/`leftAlone`
239
- * bookkeeping locally; performs no validation and never refuses. Returns everything the
240
- * composed `InitResult` needs except `adopted`, `dryRun` and `template`, which `initProject`
241
- * reads off `plan`. */
242
- async function seedProject(root, plan) {
243
- const { dryRun, tpl, combinedPrompt, brief, readmeExists, adopted, wasRepo, preferredBranch } = plan;
244
- let repoInitialized = false;
245
- let createdBranch;
246
- if (!wasRepo) {
247
- if (!dryRun)
248
- await git(root, "init", "-b", preferredBranch);
249
- repoInitialized = true;
250
- createdBranch = preferredBranch;
251
- }
252
- // Decided before any project file is written (init only seeds bootstrap on a fresh project).
253
- // A repo the `git init` above just created (or a dry run would) has no commits, and init
254
- // itself commits nothing before the end, so this one answer also picks the commit message.
255
- const withCommits = wasRepo && (await hasCommits(root));
256
- const fresh = await isFreshProject(root, withCommits);
237
+ /** The `created`/`leftAlone` arrays and the `claim`/`write` closures above them. */
238
+ function seedWriters(root, dryRun) {
257
239
  const created = [];
258
240
  const leftAlone = [];
259
241
  /** Record a template path this run either created (via `make`, under a real run only) or
@@ -275,33 +257,64 @@ async function seedProject(root, plan) {
275
257
  // because claim's existsSync would then treat the torn file as complete on a re-run and
276
258
  // never repair it.
277
259
  const write = (name, content) => claim(name, path.join(root, name), () => writeTextAtomic(path.join(root, name), content));
260
+ return { created, leftAlone, claim, write };
261
+ }
262
+ /** The new-repo `git init` job: seed the preferred branch unless the run is a dry run. */
263
+ async function initSeedRepo(root, plan) {
264
+ let repoInitialized = false;
265
+ let createdBranch;
266
+ if (!plan.wasRepo) {
267
+ if (!plan.dryRun)
268
+ await git(root, "init", "-b", plan.preferredBranch);
269
+ repoInitialized = true;
270
+ createdBranch = plan.preferredBranch;
271
+ }
272
+ return { repoInitialized, createdBranch };
273
+ }
274
+ /** Whether `root` is a fresh project and the repo has commits — decided before any project
275
+ * file is written (init only seeds bootstrap on a fresh project). A repo the `git init`
276
+ * above just created (or a dry run would) has no commits, and init itself commits nothing
277
+ * before the end, so this one answer also picks the commit message. */
278
+ async function resolveSeedFreshness(root, plan) {
279
+ const withCommits = plan.wasRepo && (await hasCommits(root));
280
+ const fresh = await isFreshProject(root, withCommits);
281
+ return { withCommits, fresh };
282
+ }
283
+ /** Where the brief goes: the existing brief file, TUMWATER.md (adopted), or README.md. */
284
+ function seedBriefFile(root, plan, writers) {
278
285
  // README.md is the brief only when none exists yet and there is no README to adopt (a fresh
279
286
  // repo): with TUMWATER.md owning the managed sections, a created README.md would carry a
280
287
  // duplicate prompt + status block that readBriefText never reaches and the readme role
281
288
  // never maintains.
282
289
  const name = projectName(root);
283
- if (brief !== null)
284
- leftAlone.push(brief);
285
- else if (adopted) {
286
- if (readmeExists)
287
- leftAlone.push("README.md");
288
- write("TUMWATER.md", briefTemplate(name, combinedPrompt));
290
+ if (plan.brief !== null)
291
+ writers.leftAlone.push(plan.brief);
292
+ else if (plan.adopted) {
293
+ if (plan.readmeExists)
294
+ writers.leftAlone.push("README.md");
295
+ writers.write("TUMWATER.md", briefTemplate(name, plan.combinedPrompt));
289
296
  }
290
297
  else
291
- write("README.md", readmeTemplate(name, combinedPrompt));
298
+ writers.write("README.md", readmeTemplate(name, plan.combinedPrompt));
299
+ }
300
+ /** The template backlog files: PLANS/BUGS/QUESTIONS/PRINCIPLES, with starter-plans subst. */
301
+ function seedBacklogFiles(tpl, writers) {
292
302
  // A template with starter plans seeds the backlog with them, rendered as `### ` entries —
293
303
  // the primary entry format every backlog reader parses — so the team's first runs land on
294
304
  // real work. blank keeps the `_None yet._` placeholder byte-identical to today.
295
305
  const plansContent = tpl.starterPlans.length > 0
296
306
  ? PLANS_TEMPLATE.replace("_None yet._", tpl.starterPlans.map((p) => `### ${p}`).join("\n\n"))
297
307
  : PLANS_TEMPLATE;
298
- write("PLANS.md", plansContent);
299
- write("BUGS.md", BUGS_TEMPLATE);
300
- write("QUESTIONS.md", QUESTIONS_TEMPLATE);
301
- write("PRINCIPLES.md", PRINCIPLES_TEMPLATE);
308
+ writers.write("PLANS.md", plansContent);
309
+ writers.write("BUGS.md", BUGS_TEMPLATE);
310
+ writers.write("QUESTIONS.md", QUESTIONS_TEMPLATE);
311
+ writers.write("PRINCIPLES.md", PRINCIPLES_TEMPLATE);
312
+ }
313
+ /** The config seed and its fresh-project bootstrap; returns the seeded bootstrap count. */
314
+ function seedConfigFile(root, fresh, dryRun, writers) {
302
315
  let bootstrap = null;
303
316
  if (fs.existsSync(configPath(root)))
304
- leftAlone.push(CONFIG_BASENAME);
317
+ writers.leftAlone.push(CONFIG_BASENAME);
305
318
  else {
306
319
  const seed = seedConfig(root);
307
320
  if (fresh) {
@@ -310,26 +323,36 @@ async function seedProject(root, plan) {
310
323
  }
311
324
  if (!dryRun)
312
325
  saveConfig(root, seed);
313
- created.push(CONFIG_BASENAME);
326
+ writers.created.push(CONFIG_BASENAME);
314
327
  }
328
+ return bootstrap;
329
+ }
330
+ /** The .gitignore edit: gain the missing lines, or record it as left alone. */
331
+ function seedGitignore(root, dryRun, writers) {
315
332
  if (ensureGitignore(root, dryRun))
316
- created.push(".gitignore");
333
+ writers.created.push(".gitignore");
317
334
  else if (fs.existsSync(path.join(root, ".gitignore")))
318
- leftAlone.push(".gitignore");
335
+ writers.leftAlone.push(".gitignore");
336
+ }
337
+ /** The template's starter directories — empty ones only, before the first run. */
338
+ function seedStarterDirs(root, tpl, writers) {
319
339
  // The template's starter directories: empty ones only, before the first run — the team's
320
340
  // own runs write any code. Existing directories are left alone, like existing files above.
321
341
  for (const dir of tpl.starterDirs) {
322
- claim(dir, path.join(root, dir), () => ensureDir(path.join(root, dir)));
342
+ writers.claim(dir, path.join(root, dir), () => ensureDir(path.join(root, dir)));
323
343
  }
344
+ }
345
+ /** What gets committed: the committable filter and the `git add` / staged-check / commit. */
346
+ async function commitSeeded(root, plan, withCommits, writers) {
324
347
  // The config stays out of the commit pathspec: `git add -- tumwater.json` fails on a path
325
348
  // the just-written .gitignore ignores (plans/portability.md §4a/7). It still heads the
326
349
  // `created …` line the CLI prints — the file WAS created, it just must not be tracked. When
327
350
  // that leaves the pathspec empty (`git add --` with no pathspec exits 1), there is nothing
328
351
  // to commit: a repo that only gained a config reports it and stays uncommitted.
329
- const dirSet = new Set(tpl.starterDirs);
330
- const committable = created.filter((name) => name !== CONFIG_BASENAME && !dirSet.has(name));
352
+ const dirSet = new Set(plan.tpl.starterDirs);
353
+ const committable = writers.created.filter((name) => name !== CONFIG_BASENAME && !dirSet.has(name));
331
354
  let committed = false;
332
- if (!dryRun && committable.length > 0) {
355
+ if (!plan.dryRun && committable.length > 0) {
333
356
  await git(root, "add", "--", ...committable);
334
357
  const staged = await gitTry(root, "diff", "--cached", "--quiet");
335
358
  if (staged === null) {
@@ -339,7 +362,32 @@ async function seedProject(root, plan) {
339
362
  committed = true;
340
363
  }
341
364
  }
342
- return { created, leftAlone, committed, repoInitialized, branch: createdBranch, bootstrap };
365
+ return committed;
366
+ }
367
+ /** Seed `root` from a resolved `InitPlan`: git init (a new repo only), every absent project
368
+ * file and starter directory, the config, and the commit. Holds the `created`/`leftAlone`
369
+ * bookkeeping locally; performs no validation and never refuses. Returns everything the
370
+ * composed `InitResult` needs except `adopted`, `dryRun` and `template`, which `initProject`
371
+ * reads off `plan`. */
372
+ async function seedProject(root, plan) {
373
+ const { dryRun, tpl } = plan;
374
+ const { repoInitialized, createdBranch } = await initSeedRepo(root, plan);
375
+ const { withCommits, fresh } = await resolveSeedFreshness(root, plan);
376
+ const writers = seedWriters(root, dryRun);
377
+ seedBriefFile(root, plan, writers);
378
+ seedBacklogFiles(tpl, writers);
379
+ const bootstrap = seedConfigFile(root, fresh, dryRun, writers);
380
+ seedGitignore(root, dryRun, writers);
381
+ seedStarterDirs(root, tpl, writers);
382
+ const committed = await commitSeeded(root, plan, withCommits, writers);
383
+ return {
384
+ created: writers.created,
385
+ leftAlone: writers.leftAlone,
386
+ committed,
387
+ repoInitialized,
388
+ branch: createdBranch,
389
+ bootstrap,
390
+ };
343
391
  }
344
392
  /** Initialize a repo for tumwater: README (with prompt + status) when no project brief exists
345
393
  * yet, PLANS, BUGS, QUESTIONS, PRINCIPLES, tumwater.json, .gitignore — then commit whatever was
@@ -314,7 +314,7 @@ export async function resolveBacklogInsertConflicts(wt, files) {
314
314
  }
315
315
  // Atomic (tmp + rename): a merge process killed mid-write (SIGKILL, the timeout
316
316
  // group-kill) must leave either the conflicted file or the fully resolved one, never a
317
- // truncated backlog — the CLI's backlog writers (backlog-write.ts, question-answer.ts)
317
+ // truncated backlog — the CLI's backlog writers (backlog-write.ts, backlog-question-answer.ts)
318
318
  // already write these unbounded-history files through this helper.
319
319
  writeTextAtomic(p, resolved);
320
320
  await git(wt, "add", "--", file);
@@ -3,7 +3,7 @@ import { mergeRefName } from "../paths.js";
3
3
  import { recordReview } from "../run/run-apply.js";
4
4
  import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
5
5
  import { unverifiedTreeOutcome } from "../build/build-check-events.js";
6
- import { checkFailureReasons, describeCheck, failureHeadline } from "../build/build-check-report.js";
6
+ import { checkFailureReasons, redCheckHeadline } from "../build/build-check-report.js";
7
7
  import { onlyKnownMainFailures } from "../baseline/main-watch.js";
8
8
  import { mainTipVerdict } from "../baseline/main-red.js";
9
9
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
@@ -126,7 +126,7 @@ export async function mergeCheckRed(ctx, role, wt, red) {
126
126
  if (result === "merge_blocked" && !attributed) {
127
127
  warnEventBestEffort(ctx.root, role, `merge check failed (kept for one retry): ${reason}`);
128
128
  if (ctx.state.mergeCheckFailures) {
129
- ctx.state.mergeCheckFailures.headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
129
+ ctx.state.mergeCheckFailures.headline = redCheckHeadline(red.check, red.outcome.outputTail);
130
130
  }
131
131
  }
132
132
  return result;
@@ -143,15 +143,18 @@ export function noteMergeRetryPassed(ctx, role, pinPatch) {
143
143
  if (headline)
144
144
  warnEventBestEffort(ctx.root, role, MERGE_FLAKY_WARNING_PREFIX + headline);
145
145
  }
146
- /** A merge blocked by a deterministic cross-check (onMergeBlocked's fix-claim or
147
- * backlog-structure reason — a property of the tree ahead of main, not a flaky run). The
148
- * block is counted like a red merge check, keyed by the same patch-id: under
149
- * MERGE_CHECK_FAILURE_LIMIT the pin is kept for one recovery re-merge — main may have moved
150
- * under the rebase, and a differently-rebased tree can pass the cross-check — but at the
146
+ /** A merge blocked by a deterministic cross-check — a property of the tree ahead of main,
147
+ * not a flaky run. The block is counted like a red merge check, keyed by the same patch-id:
148
+ * under MERGE_CHECK_FAILURE_LIMIT the pin is kept for one recovery re-merge — main may have
149
+ * moved under the rebase, and a differently-rebased tree can pass the cross-check — but at the
151
150
  * limit the block is attributed to the change: rejected deterministically with the block
152
151
  * reason, the pin deleted, no model run. Without this a resolution that trips the same
153
152
  * cross-check on every re-resolve pays a fresh conflict-resolver run each run, forever
154
- * (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). */
153
+ * (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). Since part 3m/3
154
+ * the merge path no longer produces cross-check blocks (a fix claim and a broken backlog
155
+ * structure are repairable and land for the main watch), so nothing in the in-lock path calls
156
+ * this today; it stays as the strike-counting sink, still pinned by INV-BF2 and retired with
157
+ * the queue by 3f/3. */
155
158
  export async function mergeBlocked(ctx, role, wt, blocked) {
156
159
  return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`merge blocked: ${blocked}`], ctx.state));
157
160
  }
@@ -185,7 +188,7 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
185
188
  const kept = keepForKnownFailures(ctx, role, label, red, state);
186
189
  if (kept)
187
190
  return kept;
188
- const headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
191
+ const headline = redCheckHeadline(red.check, red.outcome.outputTail);
189
192
  const key = normalizeClusterKey(headline);
190
193
  if (isKnownFlake(ctx.root, headline) && state.mergeFlakeExcused !== key) {
191
194
  state.mergeFlakeExcused = key;
@@ -6,12 +6,11 @@ import { ensureDetachedWorktree } from "../git/worktree.js";
6
6
  import { useWorktree } from "../git/worktree-use.js";
7
7
  import { mergeToMain } from "./merge-to-main.js";
8
8
  import { rebaseOntoMain } from "./merge-git.js";
9
- import { mergeCheckRed, mergeBlocked, noteMergeRetryPassed } from "./merge-check-failures.js";
9
+ import { mergeCheckRed, noteMergeRetryPassed } from "./merge-check-failures.js";
10
10
  import { reviewAheadOfMain } from "../review/review.js";
11
- import { reviewRunConfig, modelSelectorField } from "../config/config-views.js";
12
- import { warnEventBestEffort, logEventBestEffort } from "../events/events.js";
11
+ import { logEventBestEffort } from "../events/events.js";
13
12
  import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
14
- import { REVISION_FINAL_ON_REPEAT, REVISION_BACKSTOP, holdEntryAfterExhaustion } from "../runner/revision.js";
13
+ import { REVISION_FINAL_ON_REPEAT, REVISION_BACKSTOP, holdEntryAfterExhaustion } from "../runner/runner-revision.js";
15
14
  import { priorObjectionNumber } from "../review/review-verdict.js";
16
15
  import { DIRECTOR_ROLE } from "../roles/roles.js";
17
16
  import { shortSha } from "../text/format.js";
@@ -89,16 +88,14 @@ function reviewContextFor(ctx, req, wt, foldUsage) {
89
88
  * merge (a restart hand-off past its deadline, BUGS.md 2026-09-23) would otherwise sail
90
89
  * through its gate into the check. */
91
90
  export async function reviewPinnedChange(ctx, req, wt, state, foldUsage) {
92
- const { root, config } = ctx;
91
+ const { root } = ctx;
93
92
  const { role } = req;
94
93
  const ref = mergeRefName(role);
95
94
  // Both uses below — a rejection and a strike-cap discard — end a pinned change without
96
- // keeping it: drop the held-selector note and the pin ref together, so the two-part cleanup
97
- // cannot drift between them. The verify's checkout is a pooled slot (plans/worktree-pool.md,
95
+ // keeping it: drop the pin ref. The verify's checkout is a pooled slot (plans/worktree-pool.md,
98
96
  // "Vets and merges"): verifyRequest releases the lease in its finally, and a slot is reused, so
99
97
  // this path must not remove the worktree it ran in.
100
98
  const discardPin = async () => {
101
- state.mergeReviewError = undefined;
102
99
  await deleteRef(root, ref);
103
100
  };
104
101
  // Already stopping: the gate never starts, so nothing is persisted or folded — the ref stays
@@ -163,19 +160,6 @@ export async function reviewPinnedChange(ctx, req, wt, state, foldUsage) {
163
160
  }
164
161
  if (gate.decision === "failed") {
165
162
  state.lastError = `review failed: ${gate.detail}`;
166
- // A permanent provider configuration error can never succeed until the config changes, so
167
- // record the held pin with the reviewer selector that failed: leftover recovery refuses to
168
- // re-queue while that selector stands and clears the hold once it changes (BUGS.md
169
- // 2026-10-06). A discarded pin is gone, so no hold is recorded for it.
170
- if (gate.configError && !gate.discarded) {
171
- const selector = modelSelectorField(reviewRunConfig(config)).model;
172
- state.mergeReviewError = {
173
- sha: req.sha,
174
- ...(selector ? { selector } : {}),
175
- message: state.lastError,
176
- };
177
- warnEventBestEffort(root, role, `review blocked by a permanent provider configuration error${selector ? ` on ${selector}` : ""}: ${gate.detail} — the pinned change is held until the model config changes`);
178
- }
179
163
  // Strike-cap discard: the gate says so directly (it reset the worktree off the pin) — the
180
164
  // commit is gone and the ref goes too. An under-cap failure — a dead reviewer — keeps the
181
165
  // pin as it is: the gate never commits, so the pin still names the tree the next re-merge
@@ -195,8 +179,6 @@ export async function reviewPinnedChange(ctx, req, wt, state, foldUsage) {
195
179
  await setRef(root, ref, sha);
196
180
  req = { ...req, sha };
197
181
  }
198
- // The change may merge: nothing is held against the previous reviewer selector any more.
199
- state.mergeReviewError = undefined;
200
182
  return { kind: "gate", gate, sha };
201
183
  }
202
184
  /** The failed merge outcomes that KEEP the pin for another attempt: an under-cap review
@@ -291,7 +273,6 @@ async function mergeApprovedChangeIn(ctx, req) {
291
273
  const wt = await ensureDetachedWorktree(ctx.root, mergeWorktreePath(ctx.root), req.sha);
292
274
  const pinPatch = await patchId(wt, ctx.mainBranch, req.sha).catch(() => null);
293
275
  let red;
294
- let blocked;
295
276
  const result = await mergeToMain({
296
277
  root: ctx.root,
297
278
  role: req.role,
@@ -303,9 +284,6 @@ async function mergeApprovedChangeIn(ctx, req) {
303
284
  onMergeCheckRed: (check, outcome) => {
304
285
  red = { check, outcome };
305
286
  },
306
- onMergeBlocked: (reason) => {
307
- blocked = reason;
308
- },
309
287
  recheckResolved: async (resolvedWt) => {
310
288
  if (ctx.signal().aborted)
311
289
  return { verdict: "retry" };
@@ -357,12 +335,9 @@ async function mergeApprovedChangeIn(ctx, req) {
357
335
  }
358
336
  if (result === "merge_blocked" && red)
359
337
  return mergeCheckRed(ctx, req.role, wt, red);
360
- if (result === "merge_blocked" && blocked !== undefined) {
361
- return mergeBlocked(ctx, req.role, wt, blocked);
362
- }
363
338
  // merge_conflict / merge_blocked: keep the ref — the next run's recovery re-merges it.
364
- // (Cross-check blocks no longer come through here: merge-check-failures.ts's mergeBlocked
365
- // counts them.)
339
+ // (The deterministic cross-check blocks — fix-claim and backlog structure — no longer come
340
+ // through here: since part 3m/3 they are repairable and land for the main watch.)
366
341
  // A rejected recheck already wrote its own lastError and deleted its ref — leave it.
367
342
  if (result !== "rejected")
368
343
  ctx.state.lastError = `merge failed: ${result}`;