tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -1,7 +1,6 @@
1
1
  import { mergeApproved } from "./merge-batch.js";
2
2
  import { removeMergeChange, resolveWaitStage, setMergeChangeStatus } from "./merge-slot.js";
3
- import { seamConcurrencyKey } from "../config/config-views.js";
4
- import { withProviderLease } from "../concurrency/provider-lease.js";
3
+ import { withRunPermit } from "../concurrency/run-permit.js";
5
4
  import { dropMerge, entryRevisionFields, queuedMergeFiles } from "./merge-queue.js";
6
5
  import { errorMessage } from "../text/text.js";
7
6
  import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
@@ -23,8 +22,9 @@ async function postReviewDue(config, root, commit) {
23
22
  return files === null || !isExemptDiff(files, config.review.exemptPaths);
24
23
  }
25
24
  /** One poll of the merge queue (the scheduler's WHEN: after the gates, never while a restart
26
- * or a disk hold is pending — a reviewer-provider hold does not stop the whole drain, it parks
27
- * only the entries a pre-merge review gates, per the role-scoped startHeld): start a
25
+ * or a disk hold is pending — a reviewer-provider hold does not stop the whole drain: it parks
26
+ * only the post-merge review drain, whose no-role startHeld reads it, never a merge entry, so
27
+ * no verify or landing waits on the reviewer): start a
28
28
  * verification for every queued change the pipeline does not already hold (drainVerify, in
29
29
  * merge-verify.ts), and start the merge when the slot is free and something is approved
30
30
  * (drainMerge). A queued merge is COMMITTED work awaiting completion, not a new run, so the
@@ -123,22 +123,20 @@ function startMerge(ctx, p, picks) {
123
123
  runPi: async (w, prompt, s, cfg) => {
124
124
  // A parked resolver names its wait (resolve-wait, then resolving once granted) the way
125
125
  // a parked check does (withCheckPermit): on 2026-10-12 one waited 2.5 min for a permit
126
- // with the merge queue behind it, reading as "merging". The lease is keyed on the
127
- // resolver config's provider (the `cfg` it is handed), so the conflict resolution
128
- // counts against the strong tier's provider cap like any other strong-tier run
126
+ // with the merge queue behind it, reading as "merging". The resolution waits on the
127
+ // one team total (maxConcurrent) like any other MERGE_TIER run
129
128
  // (plans/provider-concurrency.md §4). canLeaseAtOnce is acquireLease's own fast path
130
129
  // decided synchronously, so a free permit shows no wait; MERGE_TIER ignores the
131
130
  // maintenance reserve.
132
- const key = cfg ? seamConcurrencyKey(cfg, ctx.piDefault) : undefined;
133
131
  const hooks = resolveWaitStage(root, v.entry.role);
134
- if (!semaphore.canLeaseAtOnce(MERGE_TIER, key))
132
+ if (!semaphore.canLeaseAtOnce(MERGE_TIER))
135
133
  hooks.waiting();
136
- const lease = await semaphore.acquireLease(MERGE_TIER, key);
134
+ const lease = await semaphore.acquireLease(MERGE_TIER);
137
135
  hooks.granted();
138
136
  try {
139
- // The resolver's lease rides in the provider-lease store for its run, so the run
140
- // re-keys it to the provider it actually uses (plans/provider-concurrency.md §5).
141
- return await withProviderLease(lease, semaphore, () => v.author.runMergePi(w, prompt, s, cfg), { piDefault: ctx.piDefault });
137
+ // The resolver's lease rides in the run-permit store for its run, so an idle
138
+ // pause inside it (a failed review's in-place retry) re-takes the same permit.
139
+ return await withRunPermit(lease, () => v.author.runMergePi(w, prompt, s, cfg));
142
140
  }
143
141
  finally {
144
142
  lease.release();
@@ -43,15 +43,14 @@ export function guardMergeTask(root, role, promise) {
43
43
  warnEventBestEffort(root, TUMWATER_SOURCE, `merge task for ${role} could not settle: ${errorMessage(err)}`);
44
44
  });
45
45
  }
46
- /** Take a permit at `tier` (on `key` when given) unless `signal` fires first: resolves to its
47
- * release, or to null when the signal won — a grant that arrives after that is handed straight
48
- * back (a hop, never a leak), so a parked verification that a shutdown settles holds nothing. When
49
- * `key` is set the permit is a keyed lease, so the verification also counts against its provider's
50
- * cap (plans/provider-concurrency.md §4). */
51
- export async function acquireUnlessAborted(semaphore, tier, signal, key) {
46
+ /** Take a permit at `tier` unless `signal` fires first: resolves to its release, or to null
47
+ * when the signal won — a grant that arrives after that is handed straight back (a hop, never a
48
+ * leak), so a parked verification that a shutdown settles holds nothing. A verification's
49
+ * permit is the one team total (`maxConcurrent`). */
50
+ export async function acquireUnlessAborted(semaphore, tier, signal) {
52
51
  if (signal.aborted)
53
52
  return null;
54
- const granted = semaphore.acquireLease(tier, key);
53
+ const granted = semaphore.acquireLease(tier);
55
54
  const stopped = new Promise((resolve) => signal.addEventListener("abort", () => resolve(false), { once: true }));
56
55
  if (await Promise.race([granted, stopped])) {
57
56
  return granted;
@@ -1,5 +1,4 @@
1
- import { withProviderLease } from "../concurrency/provider-lease.js";
2
- import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
1
+ import { withRunPermit } from "../concurrency/run-permit.js";
3
2
  import { subscribeEvents, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
4
3
  import { deleteRef, headOf, subjectOfCommit } from "../git/git.js";
5
4
  import { ensureDetachedWorktree } from "../git/worktree.js";
@@ -14,7 +13,7 @@ import { abortOnShutdown, acquireUnlessAborted, guardMergeTask } from "./merge-p
14
13
  import { resolveWaitStage } from "./merge-slot.js";
15
14
  import { mergeToMain } from "./merge-to-main.js";
16
15
  import { MERGE_TIER, resolveAuthor, VERIFY_TIER } from "./merge-verify.js";
17
- /** One poll of the post-merge reviews (review.afterMerge): a rejected-and-not-fixed commit is
16
+ /** One poll of the post-merge reviews: a rejected-and-not-fixed commit is
18
17
  * answered by the reviewer's own landed fix (Forward, never back 1/6) — never by a revert — and
19
18
  * the next unreviewed commit starts its review when a permit is free right now. One review at a
20
19
  * time; starts a task and returns. */
@@ -35,12 +34,11 @@ function startPostReview(ctx, p, entry) {
35
34
  const task = { promise: Promise.resolve(), controller: new AbortController(), roles: [], userAborted: false, parked: true };
36
35
  const detach = abortOnShutdown(ctx.signal, task.controller);
37
36
  const author = resolveAuthor(ctx, entry.role);
38
- const key = seamConcurrencyKey(reviewRunConfig(ctx.liveConfig), ctx.piDefault);
39
37
  const run = (async () => {
40
38
  try {
41
- if (!semaphore.canLeaseAtOnce(VERIFY_TIER, key))
39
+ if (!semaphore.canLeaseAtOnce(VERIFY_TIER))
42
40
  return; // permits are busy; try again next poll
43
- const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, task.controller.signal, key);
41
+ const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, task.controller.signal);
44
42
  task.parked = false;
45
43
  if (lease === null)
46
44
  return;
@@ -125,19 +123,18 @@ async function fileDroppedReview(ctx, entry, failure) {
125
123
  * is never a throw. */
126
124
  async function mergeReviewerFix(ctx, entry, fix) {
127
125
  const author = resolveAuthor(ctx, entry.role);
128
- // The fix merge's conflict resolver rides the same provider lease the merge slot's resolver
129
- // does (merge-drain.ts startMerge): the MERGE_TIER lease is keyed on the resolver config's
130
- // provider and rides in the provider-lease store, so a conflict resolution counts against the
131
- // strong tier's cap like any other strong-tier run instead of acquiring a provider unchecked.
126
+ // The fix merge's conflict resolver rides the same team permit the merge slot's resolver
127
+ // does (merge-drain.ts startMerge): the MERGE_TIER lease is acquired on the one team total
128
+ // and rides in the run-permit store, so a conflict resolution counts against maxConcurrent
129
+ // like any other strong-tier run.
132
130
  const runPi = async (wt, prompt, sessionName, config) => {
133
- const key = config ? seamConcurrencyKey(config, ctx.piDefault) : undefined;
134
131
  const hooks = resolveWaitStage(ctx.root, entry.role);
135
- if (!ctx.semaphore.canLeaseAtOnce(MERGE_TIER, key))
132
+ if (!ctx.semaphore.canLeaseAtOnce(MERGE_TIER))
136
133
  hooks.waiting();
137
- const lease = await ctx.semaphore.acquireLease(MERGE_TIER, key);
134
+ const lease = await ctx.semaphore.acquireLease(MERGE_TIER);
138
135
  hooks.granted();
139
136
  try {
140
- return await withProviderLease(lease, ctx.semaphore, () => author.runMergePi(wt, prompt, sessionName, config), { piDefault: ctx.piDefault });
137
+ return await withRunPermit(lease, () => author.runMergePi(wt, prompt, sessionName, config));
141
138
  }
142
139
  finally {
143
140
  lease.release();
@@ -3,7 +3,7 @@
3
3
  * in execution order, plus the count of queue files the reader skipped. Pure function of the
4
4
  * collected view and a `now` instant — no I/O, no clock reads — so the age it prints can be
5
5
  * pinned by a test. */
6
- import { humanAgo, humanSeconds, secondsSince, secondsUntil } from "../text/datetime.js";
6
+ import { humanAgo, humanSeconds, humanSince, secondsUntil } from "../text/datetime.js";
7
7
  import { roleLabel } from "../roles/role-labels.js";
8
8
  import { shortSha } from "../text/format.js";
9
9
  import { filterKeepPositions } from "../collections.js";
@@ -24,11 +24,11 @@ export function queueEntryMergeLabel(sha, inFlight, now) {
24
24
  return "approved, waiting to merge";
25
25
  let label = "merging";
26
26
  if (change.startedAt)
27
- label += ` ${humanSeconds(secondsSince(change.startedAt, now))}`;
27
+ label += ` ${humanSince(change.startedAt, now)}`;
28
28
  if (change.stage)
29
29
  label += ` · ${MERGE_STAGE_LABELS[change.stage]}`;
30
30
  if (change.stageSince !== undefined)
31
- label += ` ${humanSeconds(secondsSince(change.stageSince, now))}`;
31
+ label += ` ${humanSince(change.stageSince, now)}`;
32
32
  return label;
33
33
  }
34
34
  /** One re-submitted change's revision note (plans/revise-rejected.md): `revision <round>`,
@@ -6,7 +6,7 @@
6
6
  * retriable result) or once main's head stops being red (a `main_red` pin). */
7
7
  import { enqueueMerge, queuedMerges } from "./merge-queue.js";
8
8
  import { RETRIABLE_MERGE_RESULTS } from "./merge-core.js";
9
- import { pinnedRetrySha } from "../runner/leftover.js";
9
+ import { pinnedRetrySha } from "../runner/runner-leftover.js";
10
10
  import { branchHead } from "../git/git.js";
11
11
  import { cachedBaselineVerdict } from "../baseline/main-baseline.js";
12
12
  import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
@@ -192,14 +192,6 @@ export function checkWaitStage(root, roles) {
192
192
  export function resolveWaitStage(root, role) {
193
193
  return waitStageHooks(root, [role], "resolve-wait", "resolving");
194
194
  }
195
- /** The verification's hooks around a re-key onto a full provider (plans/provider-concurrency.md §5,
196
- * "A re-key wait is visible"): `provider-wait` while the verification's lease parks on the target
197
- * provider's full cap, `reviewing` once the key frees and the review proceeds. resolveWaitStage's
198
- * shape for the re-key path — a verification that normally shares its review seam never
199
- * re-keys, so the stage fires only when a run actually moves to a different, full provider. */
200
- export function providerWaitStage(root, role) {
201
- return waitStageHooks(root, [role], "provider-wait", "reviewing");
202
- }
203
195
  /** The marker's per-change records, whichever shape it has: `changes` as written, or — for a
204
196
  * marker an older generation wrote with one top-level change — that change, `merging` since the
205
197
  * marker's start at the marker's stage. Reads both shapes into records, so addMergeChange's
@@ -2,8 +2,8 @@
2
2
  * merge-batch.ts, whose header scopes that file to the verify and merge halves): assemble the
3
3
  * approved changes as a cherry-picked stack on main's current tip, run ONE scope-`batch` build
4
4
  * check over the combined tree, and fast-forward main through the captured shas — with the
5
- * re-stack loop for a main that moves mid-check, the doc-only exemption and its cross-checks,
6
- * and the per-attempt abort observation. The bisect over a red stack (mergeApproved, in
5
+ * re-stack loop for a main that moves mid-check, the doc-only exemption, and the per-attempt
6
+ * abort observation. The bisect over a red stack (mergeApproved, in
7
7
  * merge-batch.ts) drives this file's mergeStack one prefix at a time. */
8
8
  import { committerIdentArgs, gitTry } from "../git/git-run.js";
9
9
  import { branchHead, deleteRef, headOf } from "../git/git.js";
@@ -11,7 +11,6 @@ import { treeDiffFiles } from "../git/git-diff.js";
11
11
  import { mergeRefName, mergeLockDir } from "../paths.js";
12
12
  import { ensureDetachedWorktree } from "../git/worktree.js";
13
13
  import { useWorktree } from "../git/worktree-use.js";
14
- import { exemptSkipBlockReason } from "./merge-to-main.js";
15
14
  import { logNewQuestions } from "./merge-questions.js";
16
15
  import { openQuestions } from "../backlog/backlog.js";
17
16
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
@@ -81,11 +80,13 @@ async function assembleStack(root, mainBranch, wtPath, entries) {
81
80
  * the gate's own doc-only test (isExemptDiff over config.review.exemptPaths). A re-stack whose
82
81
  * new commits from main were doc-only rebuilds the checked tree with nothing but doc bytes
83
82
  * changed, and a doc-only delta cannot break the build: the reasoning verifyMerge
84
- * (src/merge/merge-to-main.ts) applies when it skips its in-lock re-check for a moved
85
- * doc-only merge. Its false-fix cross-check has no counterpart here — the delta is
86
- * commits main already merged through their own gate, not a claim this batch makes.
87
- * --no-renames so a rename out of a code path lists the deleted source, not only its exempt
88
- * destination. An unreadable diff is not exempt: the caller runs the check. */
83
+ * (src/merge/merge-to-main.ts) applies when it lets a moved doc-only merge through with no
84
+ * check. Since part 3m/3 that arm runs no cross-check, and neither does this delta — a false
85
+ * fix claim and a broken backlog structure are repairable by a later commit and reported by
86
+ * the main watch, and this delta is commits main already merged through their own gate, not a
87
+ * claim this batch makes. --no-renames so a rename out of a code path lists the deleted
88
+ * source, not only its exempt destination. An unreadable diff is not exempt: the caller runs
89
+ * the check. */
89
90
  async function exemptTreeDelta(root, from, to, exemptPaths) {
90
91
  const files = await treeDiffFiles(root, from, to);
91
92
  return files !== null && isExemptDiff(files, exemptPaths);
@@ -97,9 +98,10 @@ async function exemptTreeDelta(root, from, to, exemptPaths) {
97
98
  * won, up to BATCH_RESTACK_ATTEMPTS times, and a re-stack whose tree is the checked tree plus
98
99
  * doc-only commits skips its re-check (exemptTreeDelta). A stack whose whole delta ahead of
99
100
  * its assembly base is review-exempt skips the check too — a doc-only delta cannot break the
100
- * build (the reasoning verifyMerge's exempt arm applies) — and runs the same cross-checks
101
- * that arm runs, the backlog heading structure and the false-fix claim; one that fails either
102
- * returns `conflict`, so the per-change fallback applies the gate's own verdict per change.
101
+ * build (the reasoning verifyMerge's exempt arm applies). Since part 3m/3 that arm runs no
102
+ * cross-check and neither does this skip: a false fix claim and a broken backlog structure are
103
+ * repairable by a later commit and reported by the main watch, so the doc-only stack merges on
104
+ * the exemption for the watch to report on main's tip.
103
105
  * A primary checkout on main that holds uncommitted or untracked files the stack changes
104
106
  * returns `blocked` before any check runs, with no re-stack.
105
107
  * On `landed` every entry's ref is deleted and the red-main baseline is seeded with the tip
@@ -146,13 +148,10 @@ async function mergeStackIn(ctx, wtPath, entries) {
146
148
  // exemption instead of paying a suite run no code change asked for).
147
149
  const stackFiles = await treeDiffFiles(ctx.root, base, tip);
148
150
  if (stackFiles !== null && isExemptDiff(stackFiles, exemptPaths)) {
149
- // The same cross-checks verifyMerge's exempt arm runs (merge-to-main.ts's
150
- // exemptSkipBlockReason): the skip must not wave through an md-only edit the gate would
151
- // have rejected. A failure here cannot name a red check (none ran), so it hands the
152
- // stack to the one-at-a-time fallback, whose exempt arm blocks or rejects each change
153
- // with the proper handling. No baseline seeds — nothing ran on this tip.
154
- if (await exemptSkipBlockReason({ root: ctx.root, role: entries[0].role, mainBranch: ctx.mainBranch }, wtPath, stackFiles))
155
- return { kind: "conflict" };
151
+ // Since part 3m/3 an md-only stack's repairable findings (a false fix claim, a broken
152
+ // backlog structure) no longer refuse anything — the main watch reports them on main's
153
+ // tip after the merge — so the skip runs no cross-check and no check: doc-only cannot
154
+ // break the build. No baseline seeds — nothing ran on this tip.
156
155
  }
157
156
  else {
158
157
  // The merge cell names the check while it runs, then the merge steps after it — the ff
@@ -10,12 +10,10 @@ import { runScopedBuildCheck } from "../build/build-check-scoped.js";
10
10
  import { detectBuildCheck, gateCommandOf, mergeCommandOf } from "../build/build-check-detect.js";
11
11
  import { onlyKnownMainFailures } from "../baseline/main-watch.js";
12
12
  import { unverifiedTreeOutcome } from "../build/build-check-events.js";
13
- import { describeCheck, failureHeadline } from "../build/build-check-report.js";
13
+ import { redCheckHeadline } from "../build/build-check-report.js";
14
14
  import { MERGE_FLAKY_WARNING_PREFIX, quarantineTests } from "../review/known-flakes.js";
15
15
  import { noteGreenBaseline, noteInheritedGreen } from "../baseline/main-baseline.js";
16
16
  import { isExemptDiff } from "../review/exemptions.js";
17
- import { falseFixReason } from "../verdict/fix-claim.js";
18
- import { backlogStructureReason } from "../backlog/backlog-structure.js";
19
17
  import { withLock } from "../concurrency/lock.js";
20
18
  import { buildConflictPrompt } from "../prompt/gate-prompts.js";
21
19
  import { mergeLockDir } from "../paths.js";
@@ -121,42 +119,13 @@ async function tryMerge(ctx, wt, summary, verifiedHead) {
121
119
  return "changed";
122
120
  }, ctx.lockTimeoutMs);
123
121
  }
124
- /** The backlog-structure cross-check both verifyMerge paths run (exempt and code diffs
125
- * alike): when the tree ahead of main duplicates or drops a `## ` section heading, warn on the
126
- * role's feed with the one "merge blocked:" phrasing and return the reason (null when the
127
- * headings are intact). Shared so the wording and the block action cannot drift between the
128
- * paths. */
129
- async function structureBlocked(ctx, wt, files) {
130
- const structure = await backlogStructureReason(wt, ctx.mainBranch, files);
131
- if (!structure)
132
- return null;
133
- warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${structure}`);
134
- return structure;
135
- }
136
- /** The exempt arm's full cross-check — backlog structure, then fix-claim — shared by
137
- * verifyMerge and merge-batch.ts's stack skip: an md-only tree delta that skips the build
138
- * check must not wave through an edit the gate would have rejected. Returns the first blocking
139
- * reason (a structure reason already warned as "merge blocked: ..."), or null when the skip
140
- * may proceed. */
141
- export async function exemptSkipBlockReason(ctx, wt, files) {
142
- const structure = await structureBlocked(ctx, wt, files);
143
- if (structure)
144
- return structure;
145
- const fix = await falseFixReason(wt, ctx.mainBranch, files);
146
- // Warn like structureBlocked does: a fix-claim block that used to be silent read as five
147
- // unexplained merge_blocked retries on the feed (BUGS.md 2026-10-02). The batch stack-skip
148
- // caller may warn twice for one change (here, then again when the fallback's exempt arm
149
- // blocks it) — a duplicated line is harmless next to a silent block.
150
- if (fix)
151
- warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${fix}`);
152
- return fix ?? null;
153
- }
154
122
  /** Verify the exact tree about to reach main — the post-rebase head (BUGS.md 2026-09-08: the
155
123
  * gate's pre-check ran outside this lock against a head the rebase may have rewritten, so the
156
- * bytes that become main were never run through a check). Returns false when the tree is
157
- * structurally unsound (backlogStructureReason: a duplicated or dropped `## ` section heading,
158
- * with a warning event so a broken conflict resolution shows on the dashboards) or when the
159
- * project's declared check FAILS on the rebased tree; every other outcome merges. Skips:
124
+ * bytes that become main were never run through a check). Returns false only when the project's
125
+ * declared check FAILS on the rebased tree; every other outcome merges. Since part 3m/3 no
126
+ * other finding refuses a diff here: a conflict resolution that duplicates or drops a `## `
127
+ * section heading, or a false fix claim, is repairable by a later commit, so it lands and the
128
+ * main watch reports it on main's tip after the merge. Skips:
160
129
  * - a no-op rebase whose exact tree the gate verified (`rebasedHead === verifiedHead`): main
161
130
  * did not move under us, so the merge tree is byte-identical to what the gate's pre-check
162
131
  * already ran green. Seed the red-main baseline with the SHA that becomes main: every role's
@@ -200,27 +169,13 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
200
169
  return true;
201
170
  }
202
171
  const files = await aheadOfMainFiles(wt, ctx.mainBranch);
203
- if (isExemptDiff(files, ctx.exemptPaths)) {
204
- // Same cross-checks as the gate (fix-claim.ts, backlog-structure.ts): the in-lock re-check
205
- // must not wave through an md-only edit the gate would have rejected — and this is the
206
- // site that catches what the gate cannot see: a conflict resolution happens AFTER the
207
- // gate, inside this lock, so a resolution that kept both sides of a `## Done` conflict and
208
- // duplicated the heading merges here or not at all (PLANS.md 2026-09-25, 9eaae5ac).
209
- const reason = await exemptSkipBlockReason(ctx, wt, files);
210
- if (reason !== null) {
211
- ctx.onMergeBlocked?.(reason);
212
- return false;
213
- }
172
+ // An exempt (md-only) diff cannot break the build and — since part 3m/3 — carries no finding
173
+ // this merge must refuse: a false fix claim and a broken backlog structure are repairable by
174
+ // a later commit, so they land for the main watch to report on main's tip. The old rationale —
175
+ // "must not wave through an md-only edit the gate would have rejected" — is gone with the
176
+ // gate's narrowing: the gate no longer rejects those findings.
177
+ if (isExemptDiff(files, ctx.exemptPaths))
214
178
  return true;
215
- }
216
- // The heading check for code diffs too, before the build check: a conflict resolution that
217
- // broke backlog structure reads as an explained block on the dashboards, not as an
218
- // unexplained red check run on a tree that could never merge.
219
- const structure = await structureBlocked(ctx, wt, files);
220
- if (structure) {
221
- ctx.onMergeBlocked?.(structure);
222
- return false;
223
- }
224
179
  if (await onlyExemptDriftFromVerified(ctx, wt, rebasedHead, verifiedHead))
225
180
  return true;
226
181
  // The run itself (build_check event, environmental-skip warning) lives in
@@ -235,7 +190,7 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
235
190
  const failed = check.outcome;
236
191
  const retry = await runMergeCheck();
237
192
  if (retry?.outcome.status === "passed") {
238
- warnEventBestEffort(ctx.root, ctx.role, `${MERGE_FLAKY_WARNING_PREFIX}${failureHeadline(failed.outputTail) ?? describeCheck(check.check)}`);
193
+ warnEventBestEffort(ctx.root, ctx.role, `${MERGE_FLAKY_WARNING_PREFIX}${redCheckHeadline(check.check, failed.outputTail)}`);
239
194
  quarantineTests(ctx.root, failed.failingTests, failed.outputTail);
240
195
  check = retry;
241
196
  }
@@ -1,13 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { gateRoleConfig } from "../gates/budget-gates.js";
4
- import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
5
- import { providerWaitStage } from "./merge-slot.js";
6
- import { withProviderLease } from "../concurrency/provider-lease.js";
4
+ import { withRunPermit } from "../concurrency/run-permit.js";
7
5
  import { RoleRunner } from "../runner/runner.js";
8
6
  import { branchHead, commitMessage, isMergedInto, refSha } from "../git/git.js";
9
7
  import { commitRunOf } from "../git/commit-message.js";
10
- import { recoveredMergeEntry, recoveredMetadata } from "../runner/leftover.js";
8
+ import { recoveredMergeEntry, recoveredMetadata } from "../runner/runner-leftover.js";
11
9
  import { addMergeChange, mergeUsage, removeMergeChange, setMergeChangeStatus } from "./merge-slot.js";
12
10
  import { dropMerge, enqueueMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile, tornHeadRole } from "./merge-queue.js";
13
11
  import { mergeRefName } from "../paths.js";
@@ -160,9 +158,10 @@ export async function drainVerify(ctx, p) {
160
158
  // (its role was busy when the queue was listed) or claimed its role. Start nothing for it.
161
159
  if (!fs.existsSync(file) || liveRoles(p).has(entry.role))
162
160
  continue;
163
- // A pre-merge entry during a reviewer hold stays queued instead of starting a parked verify
164
- // that fills the verify limit; a review.afterMerge role's entry keeps draining (its review
165
- // runs after merging, so the hold is not for it) — ctx's role-scoped start gate decides.
161
+ // A reviewer hold parks no named entry — every entry keeps draining (no verify, start or
162
+ // landing waits on the reviewer); only a restart or disk hold (the start gate's other legs)
163
+ // parks it here, and a parked verification re-checks the gate when its permit comes. The
164
+ // start gate decides.
166
165
  if (ctx.startHeld(entry.role))
167
166
  continue;
168
167
  startVerify(ctx, p, entry, file);
@@ -186,13 +185,6 @@ export function startVerify(ctx, p, entry, file) {
186
185
  const { root, mainBranch, signal, semaphore } = ctx;
187
186
  const { role } = entry;
188
187
  const author = resolveAuthor(ctx, role);
189
- // The verification's provider key: the review seam's key for the author's role, so its reviewer
190
- // run counts toward the same provider cap as the roles that share it
191
- // (plans/provider-concurrency.md §4). The gate off runs no model, so then there is no key — the
192
- // verification is bounded by the team total alone.
193
- const reviewKey = ctx.liveConfig.review.enabled
194
- ? seamConcurrencyKey(reviewRunConfig(author.config), ctx.piDefault)
195
- : undefined;
196
188
  const verify = {
197
189
  promise: Promise.resolve(), // Replaced below; the placeholder satisfies the type.
198
190
  controller: new AbortController(),
@@ -204,7 +196,7 @@ export function startVerify(ctx, p, entry, file) {
204
196
  p.verifying.set(role, verify);
205
197
  const task = (async () => {
206
198
  try {
207
- const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, verify.controller.signal, reviewKey);
199
+ const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, verify.controller.signal);
208
200
  verify.parked = false;
209
201
  if (lease === null)
210
202
  return; // a shutdown while parked
@@ -217,11 +209,10 @@ export function startVerify(ctx, p, entry, file) {
217
209
  const startedAt = Date.now();
218
210
  try {
219
211
  addMergeChange(root, entry, liveRoles(p));
220
- // The verification's lease rides in the provider-lease store for the verification's body,
221
- // so a review run that lands on a different provider than the verification's review seam
222
- // re-keys it there, and a re-key park is shown by providerWaitStage; its slot wait drops
223
- // the key (pool-slot rule).
224
- verdict = await withProviderLease(lease, semaphore, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts), reviewHeld: () => ctx.startHeld() }, {
212
+ // The verification's lease rides in the run-permit store for the verification's body,
213
+ // so an idle pause inside it (a failed review's in-place retry) re-takes the same
214
+ // team permit; its slot wait hands the permit back too.
215
+ verdict = await withRunPermit(lease, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts), reviewHeld: () => ctx.startHeld() }, {
225
216
  role,
226
217
  sha: entry.sha,
227
218
  runNumber: entry.run,
@@ -229,7 +220,7 @@ export function startVerify(ctx, p, entry, file) {
229
220
  body: entry.body,
230
221
  highFriction: entry.highFriction,
231
222
  ...entryRevisionFields(entry),
232
- }, { state: author.state, foldUsage, runPi: (w, prompt, s, cfg) => author.runMergePi(w, prompt, s, cfg), runGatePi: (opts) => author.runGatePi(opts) }), { piDefault: ctx.piDefault, hooks: providerWaitStage(root, role) });
223
+ }, { state: author.state, foldUsage, runPi: (w, prompt, s, cfg) => author.runMergePi(w, prompt, s, cfg), runGatePi: (opts) => author.runGatePi(opts) }));
233
224
  }
234
225
  catch (err) {
235
226
  verdict = { kind: "result", result: "error" };
@@ -58,57 +58,67 @@ function writeNote(text) {
58
58
  process.stdout.write(`notes saved (${Buffer.byteLength(text, "utf8")} bytes)\n`);
59
59
  return 0;
60
60
  }
61
+ function cmdWrite(args) {
62
+ const [, flag, flagArg] = args;
63
+ if (flag === undefined)
64
+ return writeNote(readStdin());
65
+ if (flag === "--file") {
66
+ if (flagArg === undefined || args.length > 3)
67
+ return refuse(USAGE);
68
+ let text;
69
+ try {
70
+ text = fs.readFileSync(flagArg, "utf8");
71
+ }
72
+ catch (err) {
73
+ return refuse(`cannot read ${JSON.stringify(flagArg)}: ${errorMessage(err)}`);
74
+ }
75
+ return writeNote(text);
76
+ }
77
+ return refuse(USAGE);
78
+ }
79
+ function cmdShow(flag) {
80
+ if (flag !== undefined)
81
+ return refuse(USAGE);
82
+ let note;
83
+ try {
84
+ note = readNote();
85
+ }
86
+ catch (err) {
87
+ return refuse(`could not read the note: ${errorMessage(err)}`);
88
+ }
89
+ // The note is model-authored text, so strip terminal controls at this display
90
+ // boundary — the same treatment say()/fail()/the TUI frame give every untrusted
91
+ // string, so a prompt-injected note with an ANSI escape (cursor move, title set,
92
+ // OSC 52 clipboard write, carriage-return overwrite) cannot drive the operator's
93
+ // terminal when they run `tumwater-notes show`.
94
+ process.stdout.write(stripTerminalControls(note));
95
+ return 0;
96
+ }
97
+ function cmdClear(flag) {
98
+ if (flag !== undefined)
99
+ return refuse(USAGE);
100
+ try {
101
+ writeRoleNote(NOTES_PATH, "");
102
+ }
103
+ catch (err) {
104
+ return refuse(`could not clear the note: ${errorMessage(err)}`);
105
+ }
106
+ process.stdout.write("notes cleared\n");
107
+ return 0;
108
+ }
61
109
  function main() {
62
110
  if (NOTES_PATH === undefined || NOTES_PATH === "") {
63
111
  return refuse("this run keeps no notes");
64
112
  }
65
113
  const args = process.argv.slice(2);
66
- const [command, flag, flagArg] = args;
114
+ const [command, flag] = args;
67
115
  switch (command) {
68
116
  case "write":
69
- if (flag === undefined)
70
- return writeNote(readStdin());
71
- if (flag === "--file") {
72
- if (flagArg === undefined || args.length > 3)
73
- return refuse(USAGE);
74
- let text;
75
- try {
76
- text = fs.readFileSync(flagArg, "utf8");
77
- }
78
- catch (err) {
79
- return refuse(`cannot read ${JSON.stringify(flagArg)}: ${errorMessage(err)}`);
80
- }
81
- return writeNote(text);
82
- }
83
- return refuse(USAGE);
117
+ return cmdWrite(args);
84
118
  case "show":
85
- if (flag !== undefined)
86
- return refuse(USAGE);
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));
100
- return 0;
119
+ return cmdShow(flag);
101
120
  case "clear":
102
- if (flag !== undefined)
103
- return refuse(USAGE);
104
- try {
105
- writeRoleNote(NOTES_PATH, "");
106
- }
107
- catch (err) {
108
- return refuse(`could not clear the note: ${errorMessage(err)}`);
109
- }
110
- process.stdout.write("notes cleared\n");
111
- return 0;
121
+ return cmdClear(flag);
112
122
  default:
113
123
  process.stderr.write(`${USAGE}\n`);
114
124
  return 2;