tumwater 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,14 +1,17 @@
1
- // vocabulary: plain
2
1
  import { openQuestions } from "../backlog/backlog.js";
3
2
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
3
  import { changeBaseRev, commitMessage, headOf } from "../git/git.js";
5
4
  import { aheadOfMainFiles, treeDiffFiles } from "../git/git-diff.js";
6
5
  import { resolvedDiffDiverges } from "./merge-diff.js";
7
- import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
6
+ import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, overwriteBlockedReason, primaryCheckoutBlockers, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
8
7
  import { resolveBacklogInsertConflicts } from "./backlog-conflicts.js";
9
8
  import { abortSync } from "../git/worktree.js";
10
9
  import { runScopedBuildCheck } from "../build/build-check-scoped.js";
11
- import { detectBuildCheck, gateCommandOf } from "../build/build-check-detect.js";
10
+ import { detectBuildCheck, gateCommandOf, mergeCommandOf } from "../build/build-check-detect.js";
11
+ import { onlyKnownMainFailures } from "../baseline/main-watch.js";
12
+ import { unverifiedTreeOutcome } from "../build/build-check-events.js";
13
+ import { describeCheck, failureHeadline } from "../build/build-check-report.js";
14
+ import { MERGE_FLAKY_WARNING_PREFIX, quarantineTests } from "../review/known-flakes.js";
12
15
  import { noteGreenBaseline, noteInheritedGreen } from "../baseline/main-baseline.js";
13
16
  import { isExemptDiff } from "../review/exemptions.js";
14
17
  import { falseFixReason } from "../verdict/fix-claim.js";
@@ -24,7 +27,7 @@ import { resolverConfig } from "../config/config-views.js";
24
27
  * history linear), verify the rebased tree when it differs from what was reviewed, and
25
28
  * fast-forward. On conflict, makes one pi-driven resolution attempt
26
29
  * (outside the lock) before giving up. A routine conflict is normal operation, not a warning:
27
- * success merges as an ordinary `merged` event and failure surfaces via the tick's merge_conflict
30
+ * success merges as an ordinary `merged` event and failure surfaces via the run's merge_conflict
28
31
  * result — no separate log line for the hand-off itself. A merged diff that adds entries under
29
32
  * QUESTIONS.md's ## Open also emits one `question_posted` per new heading alongside the `merged`
30
33
  * event, so `tumwater logs` shows what the team is asking for (plans/questions-outbox.md).
@@ -80,6 +83,11 @@ async function tryMerge(ctx, wt, summary, verifiedHead) {
80
83
  // main moved between commit and merge, which under concurrency is the common case (review
81
84
  // runs outside this lock).
82
85
  const target = await headOf(wt, "HEAD");
86
+ const dirty = await primaryCheckoutBlockers(ctx.root, ctx.mainBranch, target);
87
+ if (dirty !== null) {
88
+ warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${overwriteBlockedReason(dirty)}`);
89
+ return "merge_blocked";
90
+ }
83
91
  if (!(await verifyMerge(ctx, wt, target, verifiedHead)))
84
92
  return "merge_blocked";
85
93
  const mainBefore = await headOf(ctx.root, ctx.mainBranch);
@@ -98,14 +106,20 @@ async function tryMerge(ctx, wt, summary, verifiedHead) {
98
106
  // Best-effort: main has already fast-forwarded, so an unwritable events feed must not turn
99
107
  // a completed merge into a rejected one and skip syncRootInstall below — the same
100
108
  // no-torn-feed policy as the orchestrator's other post-effect logging.
101
- if (commit !== mainBefore)
102
- logEventBestEffort(ctx.root, { source: ctx.role, type: "merged", commit, summary });
109
+ if (commit !== mainBefore) {
110
+ logEventBestEffort(ctx.root, {
111
+ source: ctx.role,
112
+ type: "merged",
113
+ commit,
114
+ summary,
115
+ });
116
+ }
103
117
  logNewQuestions(ctx.root, before, ctx.role);
104
118
  // Still under the lock: a merge that moved main's lockfile re-syncs the root install
105
119
  // every worktree resolves through before the next merge's check runs (BUGS.md 2026-10-01).
106
120
  await syncRootInstall(ctx.root, ctx.role);
107
121
  return "changed";
108
- });
122
+ }, ctx.lockTimeoutMs);
109
123
  }
110
124
  /** The backlog-structure cross-check both verifyMerge paths run (exempt and code diffs
111
125
  * alike): when the tree ahead of main duplicates or drops a `## ` section heading, warn on the
@@ -146,12 +160,16 @@ export async function exemptSkipBlockReason(ctx, wt, files) {
146
160
  * - a no-op rebase whose exact tree the gate verified (`rebasedHead === verifiedHead`): main
147
161
  * did not move under us, so the merge tree is byte-identical to what the gate's pre-check
148
162
  * already ran green. Seed the red-main baseline with the SHA that becomes main: every role's
149
- * next fresh tick then hits the cache instead of re-running the full suite on an
163
+ * next fresh run then hits the cache instead of re-running the full suite on an
150
164
  * already-verified tree. A no-op rebase the gate did NOT verify — its pre-check was skipped
151
165
  * (a timeout, a killed run) — falls through and runs the merge-scope check, which fails
152
166
  * closed on the same timeout; an exempt diff or a project with no declared check falls
153
- * through to its own arm below. Not taken when a `check.gateCommand` is configured: the gate
154
- * then ran only that, not the full check.
167
+ * through to its own arm below. Taken (Fast merge "Defaults") whenever a `check.mergeCommand`
168
+ * resolves — to its own value or, when absent, to `check.gateCommand`. In the fallback case
169
+ * (no separate `mergeCommand`) the in-lock command IS the gate command the gate already ran
170
+ * green on the rebased head, so the in-lock run would add nothing; with a separate
171
+ * `check.mergeCommand` the gate's green was the gate command, not the merge one. Either way
172
+ * it skips and seeds nothing (the main watch decides main's verdict).
155
173
  * - doc-only delta ahead of main (the gate's own exemption test): cannot break the build.
156
174
  * - a code change whose rebased tree differs from the gate-verified one only in exempt paths
157
175
  * (onlyExemptDriftFromVerified): main moved under the review by doc commits alone, so the
@@ -168,10 +186,15 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
168
186
  // re-review sets verifiedHead to the resolved head, and that second rebase is then a no-op).
169
187
  // A no-op rebase the gate never verified — its pre-check timed out or was killed and left
170
188
  // GateResult.verifiedHead undefined — falls through and runs the merge-scope check, which
171
- // fails closed on the same timeout. With a check.gateCommand configured the gate ran only
172
- // that cheaper check, so even a verified no-op rebase must run the full check here: the
173
- // merge scope is what verifies a single change (and each change of an abandoned stack),
174
- // and a gateCommand green must never seed the baseline.
189
+ // fails closed on the same timeout. With a check.gateCommand or check.mergeCommand
190
+ // configured (Fast merge "Defaults": the lock runs the gate command when mergeCommand is
191
+ // absent): in the fallback case (no separate `mergeCommand`) the in-lock command IS the gate
192
+ // command the gate already ran green on the rebased head, so the verified no-op rebase adds
193
+ // nothing — skip it; with a separate `mergeCommand` the gate's green was the gate command.
194
+ // Either way it seeds nothing — a cheaper command's green never seeds the baseline: main's
195
+ // verdicts come from the main watch's full check on the tip.
196
+ if (rebasedHead === verifiedHead && mergeCommandOf(ctx.config) !== undefined)
197
+ return true;
175
198
  if (rebasedHead === verifiedHead && gateCommandOf(ctx.config) === undefined) {
176
199
  noteGreenBaseline(rebasedHead);
177
200
  return true;
@@ -204,19 +227,40 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
204
227
  // runScopedBuildCheck, shared with the review gate's pre-check. The merge cell names the
205
228
  // check (or its wait for a permit) while it runs, then the merge again for the ff.
206
229
  setMergeStage(ctx.root, ctx.role, "build-check");
207
- const check = await runScopedBuildCheck(ctx.root, ctx.role, "merge", wt, ctx.config, undefined, undefined, undefined, checkWaitStage(ctx.root, [ctx.role]));
230
+ const runMergeCheck = () => runScopedBuildCheck(ctx.root, ctx.role, "merge", wt, ctx.config, undefined, undefined, undefined, checkWaitStage(ctx.root, [ctx.role]));
231
+ let check = await runMergeCheck();
232
+ if (check?.outcome.status === "failed" && !unverifiedTreeOutcome(check.outcome) && !onlyKnownMainFailures(ctx.root, check.outcome.failingTests)) {
233
+ // One immediate re-run of the same tree: a pass makes the failed run a flake. The change
234
+ // merges and each test the failed run named is quarantined.
235
+ const failed = check.outcome;
236
+ const retry = await runMergeCheck();
237
+ if (retry?.outcome.status === "passed") {
238
+ warnEventBestEffort(ctx.root, ctx.role, `${MERGE_FLAKY_WARNING_PREFIX}${failureHeadline(failed.outputTail) ?? describeCheck(check.check)}`);
239
+ quarantineTests(ctx.root, failed.failingTests, failed.outputTail);
240
+ check = retry;
241
+ }
242
+ }
208
243
  setMergeStage(ctx.root, ctx.role, "merging");
209
244
  if (!check)
210
245
  return true; // No declared check: nothing to run, exactly like the gate skipping its pre-check.
211
246
  if (check.outcome.status === "failed") {
247
+ // Failing only what main already fails is not a red change: the fast-forward goes ahead, with
248
+ // no strike and no green seeded for a tree that did not pass.
249
+ const known = unverifiedTreeOutcome(check.outcome) ? null : onlyKnownMainFailures(ctx.root, check.outcome.failingTests);
250
+ if (known) {
251
+ warnEventBestEffort(ctx.root, ctx.role, `merge check fails only main's known failures: ${known.join(", ")}`);
252
+ return true;
253
+ }
212
254
  ctx.onMergeCheckRed?.(check.check, check.outcome);
213
255
  return false;
214
256
  }
215
257
  if (check.outcome.status === "skipped")
216
258
  return true; // No npm / broken toolchain — the helper already warned.
217
- // Green on exactly the tree that becomes main: seed it so the next tick's red-main baseline
259
+ // Green on exactly the tree that becomes main: seed it so the next run's red-main baseline
218
260
  // check is a cache hit instead of one redundant full-suite run.
219
- noteGreenBaseline(rebasedHead);
261
+ // A mergeCommand green is not the full check's: main's verdict comes from the main watch.
262
+ if (mergeCommandOf(ctx.config) === undefined)
263
+ noteGreenBaseline(rebasedHead);
220
264
  return true;
221
265
  }
222
266
  /** True, after seeding the red-main baseline, when the tree about to merge differs from the tree
@@ -229,14 +273,20 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
229
273
  * that only rewrote SHAs) is the verified bytes themselves and seeds an observed green. Any
230
274
  * other drift seeds an inherited one, which attribution re-checks before trusting
231
275
  * (main-baseline.ts's observedOnly). Not taken without a verified head (the pre-check was skipped
232
- * or never ran) or with a `check.gateCommand`, for the same reasons the no-op skip above is not.
276
+ * or never ran). With an in-lock command configured (a gate or merge command — Fast merge
277
+ * "Defaults": the lock runs the gate command when mergeCommand is absent) the drift merges
278
+ * without a seed: the main watch decides main's verdict.
233
279
  * A git failure means "not proven" and the check runs. */
234
280
  async function onlyExemptDriftFromVerified(ctx, wt, rebasedHead, verifiedHead) {
235
- if (verifiedHead === undefined || gateCommandOf(ctx.config) !== undefined)
281
+ if (verifiedHead === undefined)
282
+ return false;
283
+ if (gateCommandOf(ctx.config) !== undefined && mergeCommandOf(ctx.config) === undefined)
236
284
  return false;
237
285
  const drift = await treeDiffFiles(wt, verifiedHead, rebasedHead);
238
286
  if (drift === null || !isExemptDiff(drift, ctx.exemptPaths))
239
287
  return false;
288
+ if (mergeCommandOf(ctx.config) !== undefined)
289
+ return true; // no seed: the watch decides main's verdict
240
290
  if (drift.length === 0)
241
291
  noteGreenBaseline(rebasedHead);
242
292
  else
@@ -269,12 +319,13 @@ async function resolveConflict(ctx, wt, preMergeHead) {
269
319
  const files = await conflictedFiles(wt);
270
320
  // Deterministic first pass (plans/parallel-work-instances.md, part 3/7): insert-only
271
321
  // conflicts in the backlog markdown — two merges pasting different entries as the first
272
- // under ## Done — carry no authored bytes, so they are settled without a model run. Only the
273
- // files it could not resolve (code, or a same-entry edit) reach the resolver below.
322
+ // under ## Done, or new entries above an entry the other side deleted — carry no authored
323
+ // bytes, so they are settled without a model run. Only the files it could not resolve (code,
324
+ // or a same-entry edit) reach the resolver below.
274
325
  const remaining = await resolveBacklogInsertConflicts(wt, files);
275
326
  if (remaining.length === 0) {
276
327
  // A second stop is only possible when the branch holds more than the one insert-only
277
- // commit. One attempt per tick.
328
+ // commit. One attempt per run.
278
329
  return continueOrAbortRebase(wt);
279
330
  }
280
331
  // The prompt names the project's own check, detected the way the in-lock re-check detects it,
@@ -299,10 +350,10 @@ async function resolveConflict(ctx, wt, preMergeHead) {
299
350
  change,
300
351
  main: commits,
301
352
  mainOmitted: omitted,
302
- }), `tumwater-${ctx.role}-${ctx.tick}-conflict`,
353
+ }), `tumwater-${ctx.role}-${ctx.runNumber}-conflict`,
303
354
  // The resolver rides the strong tier (plans/model-tiers.md part 4/8): resolution is rare,
304
355
  // tolerant of latency, and edits code inside merge. Its spend still folds into the
305
- // authoring role's usage — the fold belongs to the loop's wiring, not to the config.
356
+ // authoring role's usage — the fold belongs to the runner's wiring, not to the config.
306
357
  resolverConfig(ctx.config));
307
358
  }
308
359
  finally {
@@ -312,7 +363,7 @@ async function resolveConflict(ctx, wt, preMergeHead) {
312
363
  await abortSync(wt);
313
364
  return false;
314
365
  }
315
- // A second stop is only possible when pi itself authored extra commits during the tick.
316
- // One resolution attempt per tick.
366
+ // A second stop is only possible when pi itself authored extra commits during the run.
367
+ // One resolution attempt per run.
317
368
  return continueOrAbortRebase(wt);
318
369
  }
@@ -0,0 +1,83 @@
1
+ /** Operator reordering of a waiting merge (PLANS.md `tumwater queue --top <n>`): moving the
2
+ * Nth queued merge to the head of the execution order. Split out of merge-queue.ts so the
3
+ * queue stays the pure reader/writer it is: this module owns the reorder half of an
4
+ * unattempted entry, renaming its queue file to a stamp one below the queue's minimum — the
5
+ * merge-queue twin of inbox-top.ts (which does the same for the prompt queue). It mirrors
6
+ * inbox-top.ts's rename-and-event pairing and merge-cancel.ts's stance: no lock, one file per
7
+ * entry, tolerant of a concurrent drop. */
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+ import { logEventBestEffort } from "../events/events.js";
11
+ import { errCode } from "../errno.js";
12
+ import { queueFileStamp, lowestStampName } from "../files/file-queue.js";
13
+ import { queuedMergeFiles } from "./merge-queue.js";
14
+ /** Move the Nth queued merge — 1-based, as `tumwater queue` numbers it and `--cancel`
15
+ * resolves it — to the head of the execution order, so it is the next due entry the queue
16
+ * takes when ready. Position 1 reports `already-first` and changes nothing; an out-of-range
17
+ * position (below 1 or past the readable depth) reports `out-of-range` with the current
18
+ * depth and changes nothing. The Nth entry is resolved against `queuedMergeFiles` and the
19
+ * same `topQueuedFile` the by-sha twin shares, so the CLI and the surfaces cannot drift on
20
+ * what topping means. */
21
+ export function moveQueuedMergeToTop(root, position) {
22
+ const files = queuedMergeFiles(root);
23
+ if (position < 1 || position > files.length)
24
+ return { status: "out-of-range", depth: files.length };
25
+ return topQueuedFile(root, files, position - 1);
26
+ }
27
+ /** Move the post-position body of a top move to the head of the execution order: the same
28
+ * rename-and-event work both the position and by-sha variants share. `index` is a 0-based
29
+ * index into `files`. Position 1 (index 0) reports `already-first` and changes nothing. The
30
+ * move is one rename: the entry JSON is never rewritten, so every field (`enqueuedAt`,
31
+ * `notBefore`, `retry`, `revisionRound`, `priorReview`) is unchanged; giving the file a
32
+ * stamp one below the queue's readable minimum sorts it first (falling back to `Date.now()`
33
+ * when no stamp parses), stepping one lower while the destination exists (a concurrent
34
+ * writer may have just taken the name we computed). One `merge_topped` event is logged under
35
+ * the entry's role. The rename is guarded: if the target vanished between the list and the
36
+ * rename (a concurrent drain took the change), the move answers `gone` and changes nothing
37
+ * rather than throwing. No lock and no pin write: the module mirrors merge-cancel.ts's "one
38
+ * file per entry, tolerant of a concurrent drop" stance. */
39
+ function topQueuedFile(root, files, index) {
40
+ const target = files[index];
41
+ if (index === 0) {
42
+ return {
43
+ status: "already-first",
44
+ role: target.entry.role,
45
+ sha: target.entry.sha,
46
+ summary: target.entry.summary,
47
+ };
48
+ }
49
+ const stamps = files
50
+ .map((q) => queueFileStamp(path.basename(q.file)))
51
+ .filter((s) => s !== null);
52
+ const dir = path.dirname(target.file);
53
+ const newFile = lowestStampName(stamps, dir, ".json");
54
+ try {
55
+ fs.renameSync(target.file, newFile);
56
+ }
57
+ catch (err) {
58
+ // The queue drained the target between our list and this rename (an orchestrator took
59
+ // the change): a concurrent drop is a normal race, never an error — the entry has
60
+ // already left the queue, so there is nothing to top. Any other rename failure is real.
61
+ if (errCode(err) === "ENOENT")
62
+ return { status: "gone" };
63
+ throw err;
64
+ }
65
+ logEventBestEffort(root, {
66
+ source: target.entry.role,
67
+ type: "merge_topped",
68
+ commit: target.entry.sha,
69
+ summary: target.entry.summary,
70
+ });
71
+ return { status: "topped", role: target.entry.role, sha: target.entry.sha, summary: target.entry.summary };
72
+ }
73
+ /** Move the queued merge whose entry's pinned sha is `sha` to the head of the queue (the
74
+ * browser twin of `tumwater queue --top <n>`, resolving by sha because the dashboard rows
75
+ * carry one). An entry whose sha matches none is `{ status: "gone" }` and changes nothing;
76
+ * otherwise it runs the same `topQueuedFile` the position variant uses. */
77
+ export function moveQueuedMergeToTopBySha(root, sha) {
78
+ const files = queuedMergeFiles(root);
79
+ const i = files.findIndex((q) => q.entry.sha.toLowerCase() === sha.toLowerCase());
80
+ if (i < 0)
81
+ return { status: "gone" };
82
+ return topQueuedFile(root, files, i);
83
+ }
@@ -1,14 +1,16 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { gateRoleConfig } from "../gates/budget-gates.js";
5
4
  import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
6
5
  import { providerWaitStage } from "./merge-slot.js";
7
6
  import { withProviderLease } from "../concurrency/provider-lease.js";
8
- import { LoopRunner } from "../loop/loop.js";
9
- import { branchHead, isMergedInto } from "../git/git.js";
7
+ import { RoleRunner } from "../runner/runner.js";
8
+ import { branchHead, commitMessage, isMergedInto, refSha } from "../git/git.js";
9
+ import { commitRunOf } from "../git/commit-message.js";
10
+ import { recoveredMergeEntry, recoveredMetadata } from "../runner/leftover.js";
10
11
  import { addMergeChange, mergeUsage, removeMergeChange, setMergeChangeStatus } from "./merge-slot.js";
11
- import { dropMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile } from "./merge-queue.js";
12
+ import { dropMerge, enqueueMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile, tornHeadRole } from "./merge-queue.js";
13
+ import { mergeRefName } from "../paths.js";
12
14
  import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
13
15
  import { errorMessage } from "../text/text.js";
14
16
  import { verifyRequest } from "./merge-batch.js";
@@ -18,6 +20,13 @@ import { abortOnShutdown, acquireUnlessAborted, discardPinnedRefs, guardMergeTas
18
20
  * behind them (BUGS.md 2026-09-18). Exported for merge-drain.ts, whose merge tier sits one
19
21
  * below it. */
20
22
  export const VERIFY_TIER = -1;
23
+ /** The tier the merge's conflict-resolution runs wait at, one below VERIFY_TIER: ahead of any
24
+ * verification parked for a permit, because the merge is the one serial step every queued change
25
+ * waits on. (Nothing that holds a permit ever waits on the merge: no verification takes the merge
26
+ * lock, and the resolver runs outside it — merge-to-main.ts's mergeToMain — so this cannot
27
+ * deadlock.) Shared by merge-drain.ts's merge slot and merge-post-review.ts's fix landing, so the
28
+ * reviewer's fix-merge resolver rides the same tier and the two cannot drift. */
29
+ export const MERGE_TIER = VERIFY_TIER - 1;
21
30
  /** How many verifications may be in flight (parked or running) at once: every `maxConcurrent`
22
31
  * permit but one, so a deep merge queue never takes the last slot from the roles that could
23
32
  * author — at least one, so `maxConcurrent` 1 still merges. The merge's short conflict-resolution
@@ -28,28 +37,68 @@ export function verifyLimit(maxConcurrent) {
28
37
  /** Resolve a merge entry's authoring runner: the live runner when the role is enabled
29
38
  * (runners are never removed from the array on disable — only a warning event fires); a role
30
39
  * disabled before this process started has no runner, so a throwaway one supplies the same
31
- * wiring (loop-pi.ts, runMergePi, foldMergeUsage) and a disk-loaded state to fold and save
40
+ * wiring (runner-pi.ts, runMergePi, foldMergeUsage) and a disk-loaded state to fold and save
32
41
  * on. Both share the live config, like every runner — the director takes directorConfig (the live
33
42
  * config, its budget-gate exemption, unless a used-up window moved it), every other role takes
34
43
  * roleConfig (gateRoleConfig's single home for the rule; the scheduler assigns the live runners
35
44
  * by it every poll). */
36
- function resolveAuthor(ctx, role) {
45
+ export function resolveAuthor(ctx, role) {
37
46
  const { root, mainBranch, signal, runners, liveConfig, roleConfig, directorConfig } = ctx;
38
47
  return (runners.find((r) => r.role === role) ??
39
- new LoopRunner(root, role, gateRoleConfig(role, liveConfig, roleConfig, directorConfig), mainBranch, signal));
48
+ new RoleRunner(root, role, gateRoleConfig(role, liveConfig, roleConfig, directorConfig), mainBranch, signal));
40
49
  }
41
50
  /** Drop a torn queue head. A torn head (torn or foreign — a partial write an older build left,
42
51
  * a hand-placed file) makes headMerge read null forever — nothing else drops it, stranding
43
- * every live entry behind it and pinning their authors' ticks via the interlock. Drop it with
44
- * one warning; the healthy entries behind it are verified in the same poll. The crashed
45
- * entry's commit, if any, still rides its merge ref into next-tick leftover recovery
46
- * (BUGS.md 2026-09-17). */
47
- function dropTornHead(root) {
52
+ * every live entry behind it and pinning their authors' runs via the interlock. Drop it with
53
+ * one warning naming the file (and, when the head's raw bytes still carry a role, that role);
54
+ * the healthy entries behind it are verified in the same poll. When a role is recovered, that
55
+ * role's merge pin is read and, if it resolves, the change is re-queued fresh — in the same
56
+ * shape leftover recovery builds (recoveredMergeEntry: the commit message's real summary,
57
+ * body, high-friction flag, and revision round, plus the authoring run from its Run trailer) —
58
+ * before the drop, so the same poll's queuedMergeFiles sees it and no landing waits forever on
59
+ * the one unreadable file (BUGS.md 2026-09-17). The re-queue is best-effort: a write failure
60
+ * (ENOSPC/EMFILE) or an unreadable commit must not wedge the queue, so the torn head is still
61
+ * dropped (the pin survives and leftover recovery re-queues the full shape on the role's next
62
+ * run), and the drain's poll never aborts. A torn head with no recoverable role or no pin drops
63
+ * with the warning alone; the crashed entry's commit, if any, still rides its merge ref into
64
+ * next-run leftover recovery. Exported for test/merge-queue.test.ts's torn-head step. */
65
+ export async function dropTornHead(root) {
48
66
  const stale = staleHeadFile(root);
49
67
  if (!stale)
50
68
  return;
69
+ const role = tornHeadRole(stale);
70
+ let requeued = false;
71
+ let requeueFailed = false;
72
+ if (role) {
73
+ const sha = await refSha(root, mergeRefName(role)).catch(() => null);
74
+ if (sha) {
75
+ try {
76
+ // The shape leftover recovery builds. `run` is recovered from the commit's Run trailer
77
+ // (the drain has no run number of its own) so session naming keeps the authoring run;
78
+ // a hand-made commit with no trailer falls back to the zero slot.
79
+ const meta = await recoveredMetadata(root, sha);
80
+ const message = await commitMessage(root, sha);
81
+ const run = message === null ? 0 : (commitRunOf(message) ?? 0);
82
+ enqueueMerge(root, recoveredMergeEntry(role, sha, meta, run));
83
+ requeued = true;
84
+ }
85
+ catch {
86
+ // A re-queue that cannot be built or written (ENOSPC/EMFILE on the queue write, an
87
+ // unreadable commit) must not abort the poll or strand the head: drop it anyway and let
88
+ // the still-held pin feed next-run leftover recovery, which rebuilds the full shape.
89
+ requeueFailed = true;
90
+ }
91
+ }
92
+ }
51
93
  dropMerge(stale);
52
- warnEventBestEffort(root, TUMWATER_SOURCE, `merge queue head ${path.basename(stale)} is unreadable (torn or foreign) — dropped so the queue keeps moving`);
94
+ const roleNote = !role
95
+ ? ""
96
+ : requeued
97
+ ? `; ${role}'s pinned change was re-queued`
98
+ : requeueFailed
99
+ ? `; ${role}'s pinned change could not be re-queued — left for leftover recovery`
100
+ : `; ${role}'s pin is unreadable — left for leftover recovery`;
101
+ warnEventBestEffort(root, TUMWATER_SOURCE, `merge queue head ${path.basename(stale)} is unreadable (torn or foreign) — dropped so the queue keeps moving${roleNote}`);
53
102
  }
54
103
  /** Already-merged verdicts cached per (root, sha) against the main head they were computed
55
104
  * against. drainVerify re-runs its dedupe against main every poll, but a queued entry's sha is
@@ -88,7 +137,7 @@ async function mergedIntoMain(root, sha, mainBranch, head) {
88
137
  * run, with its marker record; a crash mid-verification leaves both entry and ref, so the entry is
89
138
  * verified again — the established crash semantics. */
90
139
  export async function drainVerify(ctx, p) {
91
- dropTornHead(ctx.root);
140
+ await dropTornHead(ctx.root);
92
141
  const queue = queuedMergeFiles(ctx.root);
93
142
  // One main-head read per poll with a non-empty queue (branchHead's ref-file fast path — no
94
143
  // spawn), serving every entry's cached dedupe verdict below.
@@ -98,6 +147,9 @@ export async function drainVerify(ctx, p) {
98
147
  return;
99
148
  if (liveRoles(p).has(entry.role))
100
149
  continue;
150
+ // A re-queued review_error waits out its delay; the entry stays queued and holds its author.
151
+ if (entry.notBefore !== undefined && Date.now() < entry.notBefore)
152
+ continue;
101
153
  if (await mergedIntoMain(ctx.root, entry.sha, ctx.mainBranch, mainHead)) {
102
154
  dropMerge(file);
103
155
  removeMergeChange(ctx.root, entry.role);
@@ -107,6 +159,11 @@ export async function drainVerify(ctx, p) {
107
159
  // (its role was busy when the queue was listed) or claimed its role. Start nothing for it.
108
160
  if (!fs.existsSync(file) || liveRoles(p).has(entry.role))
109
161
  continue;
162
+ // A pre-merge entry during a reviewer hold stays queued instead of starting a parked verify
163
+ // that fills the verify limit; a review.afterMerge role's entry keeps draining (its review
164
+ // runs after merging, so the hold is not for it) — ctx's role-scoped start gate decides.
165
+ if (ctx.startHeld(entry.role))
166
+ continue;
110
167
  startVerify(ctx, p, entry, file);
111
168
  }
112
169
  }
@@ -150,7 +207,7 @@ export function startVerify(ctx, p, entry, file) {
150
207
  verify.parked = false;
151
208
  if (lease === null)
152
209
  return; // a shutdown while parked
153
- if (verify.controller.signal.aborted || ctx.startHeld()) {
210
+ if (verify.controller.signal.aborted || ctx.startHeld(role)) {
154
211
  lease.release(); // stopping, or a restart / team hold closed the gate while it waited
155
212
  return;
156
213
  }
@@ -163,10 +220,10 @@ export function startVerify(ctx, p, entry, file) {
163
220
  // so a review run that lands on a different provider than the verification's review seam
164
221
  // re-keys it there, and a re-key park is shown by providerWaitStage; its slot wait drops
165
222
  // the key (pool-slot rule).
166
- verdict = await withProviderLease(lease, semaphore, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts) }, {
223
+ verdict = await withProviderLease(lease, semaphore, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts), reviewHeld: () => ctx.startHeld() }, {
167
224
  role,
168
225
  sha: entry.sha,
169
- tick: entry.run,
226
+ runNumber: entry.run,
170
227
  summary: entry.summary,
171
228
  body: entry.body,
172
229
  highFriction: entry.highFriction,
@@ -198,7 +255,7 @@ export function startVerify(ctx, p, entry, file) {
198
255
  }
199
256
  else {
200
257
  const result = verdict.kind === "stack" ? "aborted" : verdict.result;
201
- await settleMergeOutcome(root, entry, author.state, result, Date.now() - startedAt, usage, file);
258
+ await settleMergeOutcome(root, entry, author.state, result, Date.now() - startedAt, usage, file, mainBranch);
202
259
  if (verify.userAborted)
203
260
  await discardPinnedRefs(root, [role]);
204
261
  }
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // vocabulary: plain
3
2
  /** The `tumwater-notes` command — the second bin entry (dist/src/notes-cli.js), the one
4
3
  * carrier for writing a role's note on every backend (plans/notes-command.md "The
5
4
  * `tumwater-notes` command"). A run finds it on its PATH (src/agent/notes-shim.ts) and hands
@@ -14,9 +13,12 @@
14
13
  * show print the note (nothing when there is none)
15
14
  * clear empty the note
16
15
  *
17
- * Exits 0 on success, 1 on a refusal (empty input, over-cap note, or no
18
- * TUMWATER_NOTES_PATH), and 2 with usage for anything else. */
16
+ * Exits 0 on success, 1 on a refusal (empty input, over-cap note, a note file that cannot be
17
+ * written or read, or no TUMWATER_NOTES_PATH), and 2 with usage for an unknown subcommand (a
18
+ * known subcommand with bad arguments refuses with usage and exit 1). */
19
19
  import fs from "node:fs";
20
+ import { readTextIfExists } from "./files/files.js";
21
+ import { errorMessage, stripTerminalControls } from "./text/text.js";
20
22
  import { validateRoleNote, writeRoleNote } from "./agent/role-notes-write.js";
21
23
  /** The note's path, read ONLY from the environment — never from an argument. */
22
24
  const NOTES_PATH = process.env.TUMWATER_NOTES_PATH;
@@ -29,14 +31,10 @@ const USAGE = [
29
31
  function readStdin() {
30
32
  return fs.readFileSync(0, "utf8");
31
33
  }
32
- /** Read the note's current text; a missing note reads as empty (show prints nothing). */
34
+ /** Read the note's current text; a missing note reads as empty (show prints nothing). Any
35
+ * other read failure (EACCES, EISDIR) throws, so it is never mistaken for an empty note. */
33
36
  function readNote() {
34
- try {
35
- return fs.readFileSync(NOTES_PATH, "utf8");
36
- }
37
- catch {
38
- return "";
39
- }
37
+ return readTextIfExists(NOTES_PATH) ?? "";
40
38
  }
41
39
  /** Refuse with `message` on stderr and exit 1 — the exit code for every expected refusal. */
42
40
  function refuse(message) {
@@ -51,7 +49,12 @@ function writeNote(text) {
51
49
  const error = validateRoleNote(text);
52
50
  if (error)
53
51
  return refuse(error);
54
- writeRoleNote(NOTES_PATH, text);
52
+ try {
53
+ writeRoleNote(NOTES_PATH, text);
54
+ }
55
+ catch (err) {
56
+ return refuse(`could not save the note: ${errorMessage(err)}`);
57
+ }
55
58
  process.stdout.write(`notes saved (${Buffer.byteLength(text, "utf8")} bytes)\n`);
56
59
  return 0;
57
60
  }
@@ -72,8 +75,8 @@ function main() {
72
75
  try {
73
76
  text = fs.readFileSync(flagArg, "utf8");
74
77
  }
75
- catch {
76
- return refuse(`cannot read ${flagArg}`);
78
+ catch (err) {
79
+ return refuse(`cannot read ${JSON.stringify(flagArg)}: ${errorMessage(err)}`);
77
80
  }
78
81
  return writeNote(text);
79
82
  }
@@ -81,12 +84,29 @@ function main() {
81
84
  case "show":
82
85
  if (flag !== undefined)
83
86
  return refuse(USAGE);
84
- process.stdout.write(readNote());
87
+ let note;
88
+ try {
89
+ note = readNote();
90
+ }
91
+ catch (err) {
92
+ return refuse(`could not read the note: ${errorMessage(err)}`);
93
+ }
94
+ // The note is model-authored text, so strip terminal controls at this display
95
+ // boundary — the same treatment say()/fail()/the TUI frame give every untrusted
96
+ // string, so a prompt-injected note with an ANSI escape (cursor move, title set,
97
+ // OSC 52 clipboard write, carriage-return overwrite) cannot drive the operator's
98
+ // terminal when they run `tumwater-notes show`.
99
+ process.stdout.write(stripTerminalControls(note));
85
100
  return 0;
86
101
  case "clear":
87
102
  if (flag !== undefined)
88
103
  return refuse(USAGE);
89
- writeRoleNote(NOTES_PATH, "");
104
+ try {
105
+ writeRoleNote(NOTES_PATH, "");
106
+ }
107
+ catch (err) {
108
+ return refuse(`could not clear the note: ${errorMessage(err)}`);
109
+ }
90
110
  process.stdout.write("notes cleared\n");
91
111
  return 0;
92
112
  default: