tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,9 +1,10 @@
1
+ // vocabulary: plain
1
2
  import { openQuestions } from "../backlog/backlog.js";
2
3
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
3
4
  import { changeBaseRev, commitMessage, headOf } from "../git/git.js";
4
5
  import { aheadOfMainFiles, treeDiffFiles } from "../git/git-diff.js";
5
- import { resolvedDiffDiverges } from "./landing-diff.js";
6
- import { conflictedFiles, continueRebase, ffMainTo, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./landing-git.js";
6
+ import { resolvedDiffDiverges } from "./merge-diff.js";
7
+ import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
7
8
  import { resolveBacklogInsertConflicts } from "./backlog-conflicts.js";
8
9
  import { abortSync } from "../git/worktree.js";
9
10
  import { runScopedBuildCheck } from "../build/build-check-scoped.js";
@@ -15,25 +16,25 @@ import { backlogStructureReason } from "../backlog/backlog-structure.js";
15
16
  import { withLock } from "../concurrency/lock.js";
16
17
  import { buildConflictPrompt } from "../prompt/gate-prompts.js";
17
18
  import { mergeLockDir } from "../paths.js";
18
- import { logNewQuestions } from "./landing-questions.js";
19
- import { checkWaitStage, setLandingStage } from "./landing-slot.js";
19
+ import { logNewQuestions } from "./merge-questions.js";
20
+ import { checkWaitStage, setMergeStage } from "./merge-slot.js";
20
21
  import { syncRootInstall } from "../build/dep-install.js";
21
22
  import { resolverConfig } from "../config/config-views.js";
22
- /** Land the worktree branch on main under the shared merge lock: rebase it onto main (keeping
23
+ /** Merge the worktree branch on main under the shared merge lock: rebase it onto main (keeping
23
24
  * history linear), verify the rebased tree when it differs from what was reviewed, and
24
25
  * fast-forward. On conflict, makes one pi-driven resolution attempt
25
26
  * (outside the lock) before giving up. A routine conflict is normal operation, not a warning:
26
- * success lands as an ordinary `merged` event and failure surfaces via the tick's merge_conflict
27
+ * success merges as an ordinary `merged` event and failure surfaces via the tick's merge_conflict
27
28
  * result — no separate log line for the hand-off itself. A merged diff that adds entries under
28
29
  * QUESTIONS.md's ## Open also emits one `question_posted` per new heading alongside the `merged`
29
- * event, so `tumwater logs` shows what the fleet is asking for (plans/questions-outbox.md).
30
+ * event, so `tumwater logs` shows what the team is asking for (plans/questions-outbox.md).
30
31
  * `verifiedHead` is the head the review gate's pre-check just ran green on
31
32
  * (GateResult.verifiedHead) — when the rebase turns out to be a no-op it names the exact tree
32
- * about to land, so the in-lock re-check can both skip and seed the baseline from it; pass
33
+ * about to merge, so the in-lock re-check can both skip and seed the baseline from it; pass
33
34
  * undefined when no fresh green observation was made (exempt diff, review disabled,
34
35
  * already-approved early return). */
35
36
  export async function mergeToMain(ctx, wt, summary, verifiedHead) {
36
- // The branch tip before ANY rebase of this landing. Captured once here — not per tryMerge —
37
+ // The branch tip before ANY rebase of this merge. Captured once here — not per tryMerge —
37
38
  // because the conflict-retry path rebases twice: after pi resolves, the second attempt's
38
39
  // rebase is a no-op even though its tree (the resolution) was never checked.
39
40
  const preMergeHead = await headOf(wt, "HEAD");
@@ -46,8 +47,8 @@ export async function mergeToMain(ctx, wt, summary, verifiedHead) {
46
47
  // removes lines the reviewed change never removed, has left the reviewer's scope: those bytes
47
48
  // were never judged, and the in-lock build check cannot judge intent (BUGS.md 2026-10-01: a
48
49
  // resolver restored 158 lines main had deliberately reverted). Re-run the gate over the
49
- // resolved tree before anything lands; a resolution that stays inside the reviewed change's
50
- // lines — including one that drops branch edits main superseded — re-lands with no extra run.
50
+ // resolved tree before anything merges; a resolution that stays inside the reviewed change's
51
+ // lines — including one that drops branch edits main superseded — re-merges with no extra run.
51
52
  if (ctx.recheckResolved && (await resolvedDiffDiverges(ctx, wt, preMergeHead))) {
52
53
  const recheck = await ctx.recheckResolved(wt);
53
54
  if (recheck.verdict === "rejected")
@@ -59,66 +60,69 @@ export async function mergeToMain(ctx, wt, summary, verifiedHead) {
59
60
  return tryMerge(ctx, wt, summary, verifiedHead);
60
61
  }
61
62
  /** Emit one `question_posted` event per entry QUESTIONS.md's ## Open gained since `before`,
62
- * recorded by landing-questions.ts's logNewQuestions alongside this module's `merged` event. */
63
+ * recorded by merge-questions.ts's logNewQuestions alongside this module's `merged` event. */
63
64
  async function tryMerge(ctx, wt, summary, verifiedHead) {
64
65
  return withLock(mergeLockDir(ctx.root), async () => {
65
66
  // Capture the Open questions before the rebase so a merged diff that posts new ones can
66
- // emit one question_posted per entry. The lock keeps no other merge landing between capture
67
+ // emit one question_posted per entry. The lock keeps no other merge between capture
67
68
  // and compare, so the diff is exact; on the conflict path only the second tryMerge call ever
68
69
  // reaches the post-ff code, so nothing double-emits.
69
70
  const before = openQuestions(ctx.root);
70
71
  if (!(await rebaseOntoMain(wt, ctx.mainBranch)))
71
72
  return "merge_conflict";
72
73
  // The gate's pre-check ran OUTSIDE this lock against the head as it stood then; a rebase
73
- // that rewrote anything means the tree about to land is new bytes (BUGS.md 2026-09-08).
74
+ // that rewrote anything means the tree about to merge is new bytes (BUGS.md 2026-09-08).
74
75
  // Re-verify exactly what will become main before fast-forwarding.
75
76
  // The worktree's POST-REBASE HEAD: fetched once and used for both the re-check and the
76
77
  // fast-forward. A branch ref tracks its own tip through the rebase (so this is
77
78
  // behavior-preserving for role branches), but a pinned bare sha does not move when git
78
79
  // rebase rewrites it — ff'ing main to the original pin would fail as merge_blocked whenever
79
- // main moved between commit and landing, which under concurrency is the common case (review
80
+ // main moved between commit and merge, which under concurrency is the common case (review
80
81
  // runs outside this lock).
81
82
  const target = await headOf(wt, "HEAD");
82
- if (!(await verifyLanding(ctx, wt, target, verifiedHead)))
83
+ if (!(await verifyMerge(ctx, wt, target, verifiedHead)))
83
84
  return "merge_blocked";
84
85
  const mainBefore = await headOf(ctx.root, ctx.mainBranch);
85
- if (!(await ffMainTo(ctx.root, target, ctx.mainBranch)))
86
+ const ffFailure = await ffMainToOrWhy(ctx.root, target, ctx.mainBranch);
87
+ if (ffFailure !== null) {
88
+ warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${ffFailure}`);
86
89
  return "merge_blocked";
90
+ }
87
91
  const commit = await headOf(ctx.root, ctx.mainBranch);
88
92
  // A fast-forward to a commit main already contains is a no-op ("Already up to date"): the
89
- // change is already an ancestor of main (a leftover-recovery re-land of an entry main
93
+ // change is already an ancestor of main (a leftover-recovery re-merge of an entry main
90
94
  // holds), so main's head is some later commit that did not produce this summary. Logging a
91
- // `merged` event there would count the already-counted landing a second time and pair the
95
+ // `merged` event there would count the already-counted merge a second time and pair the
92
96
  // change's summary with the wrong commit (BUGS.md 2026-10-10). Only a fast-forward that
93
- // moved main records a landing.
97
+ // moved main records a merge.
94
98
  // Best-effort: main has already fast-forwarded, so an unwritable events feed must not turn
95
- // a completed landing into a rejected one and skip syncRootInstall below — the same
99
+ // a completed merge into a rejected one and skip syncRootInstall below — the same
96
100
  // no-torn-feed policy as the orchestrator's other post-effect logging.
97
101
  if (commit !== mainBefore)
98
- logEventBestEffort(ctx.root, { loop: ctx.role, type: "merged", commit, summary });
102
+ logEventBestEffort(ctx.root, { source: ctx.role, type: "merged", commit, summary });
99
103
  logNewQuestions(ctx.root, before, ctx.role);
100
- // Still under the lock: a landing that moved main's lockfile re-syncs the root install
101
- // every worktree resolves through before the next landing's check runs (BUGS.md 2026-10-01).
104
+ // Still under the lock: a merge that moved main's lockfile re-syncs the root install
105
+ // every worktree resolves through before the next merge's check runs (BUGS.md 2026-10-01).
102
106
  await syncRootInstall(ctx.root, ctx.role);
103
107
  return "changed";
104
108
  });
105
109
  }
106
- /** The backlog-structure cross-check both verifyLanding paths run (exempt and code diffs
110
+ /** The backlog-structure cross-check both verifyMerge paths run (exempt and code diffs
107
111
  * alike): when the tree ahead of main duplicates or drops a `## ` section heading, warn on the
108
- * role's feed with the one "landing blocked:" phrasing and return the reason (null when the
112
+ * role's feed with the one "merge blocked:" phrasing and return the reason (null when the
109
113
  * headings are intact). Shared so the wording and the block action cannot drift between the
110
114
  * paths. */
111
115
  async function structureBlocked(ctx, wt, files) {
112
116
  const structure = await backlogStructureReason(wt, ctx.mainBranch, files);
113
117
  if (!structure)
114
118
  return null;
115
- warnEventBestEffort(ctx.root, ctx.role, `landing blocked: ${structure}`);
119
+ warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${structure}`);
116
120
  return structure;
117
121
  }
118
122
  /** The exempt arm's full cross-check — backlog structure, then fix-claim — shared by
119
- * verifyLanding and landing-batch.ts's stack skip: an md-only tree delta that skips the build
123
+ * verifyMerge and merge-batch.ts's stack skip: an md-only tree delta that skips the build
120
124
  * check must not wave through an edit the gate would have rejected. Returns the first blocking
121
- * reason (a structure reason already warned as "landing blocked: ..."), or null when the skip
125
+ * reason (a structure reason already warned as "merge blocked: ..."), or null when the skip
122
126
  * may proceed. */
123
127
  export async function exemptSkipBlockReason(ctx, wt, files) {
124
128
  const structure = await structureBlocked(ctx, wt, files);
@@ -130,43 +134,43 @@ export async function exemptSkipBlockReason(ctx, wt, files) {
130
134
  // caller may warn twice for one change (here, then again when the fallback's exempt arm
131
135
  // blocks it) — a duplicated line is harmless next to a silent block.
132
136
  if (fix)
133
- warnEventBestEffort(ctx.root, ctx.role, `landing blocked: ${fix}`);
137
+ warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${fix}`);
134
138
  return fix ?? null;
135
139
  }
136
- /** Verify the exact tree about to land on main — the post-rebase head (BUGS.md 2026-09-08: the
140
+ /** Verify the exact tree about to reach main — the post-rebase head (BUGS.md 2026-09-08: the
137
141
  * gate's pre-check ran outside this lock against a head the rebase may have rewritten, so the
138
142
  * bytes that become main were never run through a check). Returns false when the tree is
139
143
  * structurally unsound (backlogStructureReason: a duplicated or dropped `## ` section heading,
140
144
  * with a warning event so a broken conflict resolution shows on the dashboards) or when the
141
- * project's declared check FAILS on the rebased tree; every other outcome lands. Skips:
145
+ * project's declared check FAILS on the rebased tree; every other outcome merges. Skips:
142
146
  * - a no-op rebase whose exact tree the gate verified (`rebasedHead === verifiedHead`): main
143
- * did not move under us, so the landing tree is byte-identical to what the gate's pre-check
147
+ * did not move under us, so the merge tree is byte-identical to what the gate's pre-check
144
148
  * already ran green. Seed the red-main baseline with the SHA that becomes main: every role's
145
149
  * next fresh tick then hits the cache instead of re-running the full suite on an
146
150
  * already-verified tree. A no-op rebase the gate did NOT verify — its pre-check was skipped
147
- * (a timeout, a killed run) — falls through and runs the landing-scope check, which fails
151
+ * (a timeout, a killed run) — falls through and runs the merge-scope check, which fails
148
152
  * closed on the same timeout; an exempt diff or a project with no declared check falls
149
153
  * through to its own arm below. Not taken when a `check.gateCommand` is configured: the gate
150
154
  * then ran only that, not the full check.
151
155
  * - doc-only delta ahead of main (the gate's own exemption test): cannot break the build.
152
156
  * - a code change whose rebased tree differs from the gate-verified one only in exempt paths
153
157
  * (onlyExemptDriftFromVerified): main moved under the review by doc commits alone, so the
154
- * bytes about to land are the verified ones plus a delta that same rule exempts.
158
+ * bytes about to merge are the verified ones plus a delta that same rule exempts.
155
159
  * - no declared check at all: nothing to run, exactly like the gate skipping its pre-check.
156
160
  * An environmental skip (no npm / broken toolchain) warns and proceeds — deliberately NOT
157
161
  * fail-closed, so a broken toolchain that would fail every check regardless of the tree
158
- * (BUGS.md 2026-09-15) cannot wedge every landing behind the merge lock. A TIMEOUT is not
162
+ * (BUGS.md 2026-09-15) cannot wedge every merge behind the merge lock. A TIMEOUT is not
159
163
  * environmental here: the tree is unverified, so runScopedBuildCheck remaps it to a failed
160
- * check and the landing rejects (BUGS.md: an unverified landing must not reach main). */
161
- async function verifyLanding(ctx, wt, rebasedHead, verifiedHead) {
164
+ * check and the merge rejects (BUGS.md: an unverified merge must not reach main). */
165
+ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
162
166
  // Trust only the tree the gate actually verified. The identity means the in-lock rebase was
163
167
  // a no-op on exactly the head the pre-check ran green on (a real conflict resolution's
164
168
  // re-review sets verifiedHead to the resolved head, and that second rebase is then a no-op).
165
169
  // A no-op rebase the gate never verified — its pre-check timed out or was killed and left
166
- // GateResult.verifiedHead undefined — falls through and runs the landing-scope check, which
170
+ // GateResult.verifiedHead undefined — falls through and runs the merge-scope check, which
167
171
  // fails closed on the same timeout. With a check.gateCommand configured the gate ran only
168
172
  // that cheaper check, so even a verified no-op rebase must run the full check here: the
169
- // landing scope is what verifies a single change (and each change of an abandoned stack),
173
+ // merge scope is what verifies a single change (and each change of an abandoned stack),
170
174
  // and a gateCommand green must never seed the baseline.
171
175
  if (rebasedHead === verifiedHead && gateCommandOf(ctx.config) === undefined) {
172
176
  noteGreenBaseline(rebasedHead);
@@ -178,34 +182,34 @@ async function verifyLanding(ctx, wt, rebasedHead, verifiedHead) {
178
182
  // must not wave through an md-only edit the gate would have rejected — and this is the
179
183
  // site that catches what the gate cannot see: a conflict resolution happens AFTER the
180
184
  // gate, inside this lock, so a resolution that kept both sides of a `## Done` conflict and
181
- // duplicated the heading lands here or not at all (PLANS.md 2026-09-25, 9eaae5ac).
185
+ // duplicated the heading merges here or not at all (PLANS.md 2026-09-25, 9eaae5ac).
182
186
  const reason = await exemptSkipBlockReason(ctx, wt, files);
183
187
  if (reason !== null) {
184
- ctx.onLandingBlocked?.(reason);
188
+ ctx.onMergeBlocked?.(reason);
185
189
  return false;
186
190
  }
187
191
  return true;
188
192
  }
189
193
  // The heading check for code diffs too, before the build check: a conflict resolution that
190
194
  // broke backlog structure reads as an explained block on the dashboards, not as an
191
- // unexplained red check run on a tree that could never land.
195
+ // unexplained red check run on a tree that could never merge.
192
196
  const structure = await structureBlocked(ctx, wt, files);
193
197
  if (structure) {
194
- ctx.onLandingBlocked?.(structure);
198
+ ctx.onMergeBlocked?.(structure);
195
199
  return false;
196
200
  }
197
201
  if (await onlyExemptDriftFromVerified(ctx, wt, rebasedHead, verifiedHead))
198
202
  return true;
199
203
  // The run itself (build_check event, environmental-skip warning) lives in
200
- // runScopedBuildCheck, shared with the review gate's pre-check. The landing cell names the
204
+ // runScopedBuildCheck, shared with the review gate's pre-check. The merge cell names the
201
205
  // check (or its wait for a permit) while it runs, then the merge again for the ff.
202
- setLandingStage(ctx.root, ctx.role, "build-check");
203
- const check = await runScopedBuildCheck(ctx.root, ctx.role, "landing", wt, ctx.config, undefined, undefined, undefined, checkWaitStage(ctx.root, [ctx.role]));
204
- setLandingStage(ctx.root, ctx.role, "merging");
206
+ 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]));
208
+ setMergeStage(ctx.root, ctx.role, "merging");
205
209
  if (!check)
206
210
  return true; // No declared check: nothing to run, exactly like the gate skipping its pre-check.
207
211
  if (check.outcome.status === "failed") {
208
- ctx.onLandingCheckRed?.(check.check, check.outcome);
212
+ ctx.onMergeCheckRed?.(check.check, check.outcome);
209
213
  return false;
210
214
  }
211
215
  if (check.outcome.status === "skipped")
@@ -215,12 +219,12 @@ async function verifyLanding(ctx, wt, rebasedHead, verifiedHead) {
215
219
  noteGreenBaseline(rebasedHead);
216
220
  return true;
217
221
  }
218
- /** True, after seeding the red-main baseline, when the tree about to land differs from the tree the
219
- * gate's pre-check ran green on (`verifiedHead`) only in review-exempt paths. That is the usual
222
+ /** True, after seeding the red-main baseline, when the tree about to merge differs from the tree
223
+ * the gate's pre-check ran green on (`verifiedHead`) only in review-exempt paths. That is the usual
220
224
  * shape of "main moved under the review" here: 43% of main's commits are doc-only (plan, readme
221
- * and qa land them all day), and 165 of 750 in-lock checks over 2026-10-02..12 re-ran the full
225
+ * and qa merge them all day), and 165 of 750 in-lock checks over 2026-10-02..12 re-ran the full
222
226
  * suite, holding the merge lock, for a tree that differed from its verified one by markdown
223
- * alone. The exempt-diff rule above already lets such a delta land with no check at all, so the
227
+ * alone. The exempt-diff rule above already lets such a delta merge with no check at all, so the
224
228
  * same rule covers drift on top of a verified tree. An identical tree (empty drift, a rebase
225
229
  * that only rewrote SHAs) is the verified bytes themselves and seeds an observed green. Any
226
230
  * other drift seeds an inherited one, which attribution re-checks before trusting
@@ -264,7 +268,7 @@ async function resolveConflict(ctx, wt, preMergeHead) {
264
268
  return false;
265
269
  const files = await conflictedFiles(wt);
266
270
  // Deterministic first pass (plans/parallel-work-instances.md, part 3/7): insert-only
267
- // conflicts in the backlog markdown — two landings pasting different entries as the first
271
+ // conflicts in the backlog markdown — two merges pasting different entries as the first
268
272
  // under ## Done — carry no authored bytes, so they are settled without a model run. Only the
269
273
  // files it could not resolve (code, or a same-entry edit) reach the resolver below.
270
274
  const remaining = await resolveBacklogInsertConflicts(wt, files);
@@ -274,7 +278,7 @@ async function resolveConflict(ctx, wt, preMergeHead) {
274
278
  return continueOrAbortRebase(wt);
275
279
  }
276
280
  // The prompt names the project's own check, detected the way the in-lock re-check detects it,
277
- // as the one verifyLanding runs on the resolution — so the resolver neither guesses a runner
281
+ // as the one verifyMerge runs on the resolution — so the resolver neither guesses a runner
278
282
  // (BUGS.md 2026-10-05) nor re-runs the check itself while the merge slot waits on it.
279
283
  const check = detectBuildCheck(wt, ctx.config) ?? undefined;
280
284
  // Show both sides' intent: the change's own commit message and the main commits that touched
@@ -283,20 +287,26 @@ async function resolveConflict(ctx, wt, preMergeHead) {
283
287
  const since = await changeBaseRev(wt, ctx.mainBranch, preMergeHead);
284
288
  const change = (await commitMessage(wt, preMergeHead)) ?? "";
285
289
  const { commits, omitted } = await mainCommitsTouching(wt, since, ctx.mainBranch, remaining);
286
- // The landing cell names the resolver run, not "merging" (a no-op outside a queued landing):
287
- // the run holds the merge slot, and every queued landing waits on it. Back to `merging` once it
288
- // ends, whatever happened — the merge's git steps come next, or the landing ends.
289
- setLandingStage(ctx.root, ctx.role, "resolving");
290
+ // The merge cell names the resolver run, not "merging" (a no-op outside a queued merge):
291
+ // the run holds the merge slot, and every queued merge waits on it. Back to `merging` once it
292
+ // ends, whatever happened — the merge's git steps come next, or the merge ends.
293
+ setMergeStage(ctx.root, ctx.role, "resolving");
290
294
  let pi;
291
295
  try {
292
- pi = await ctx.runPi(wt, buildConflictPrompt(ctx.role, remaining, check, { change, main: commits, mainOmitted: omitted }), `tumwater-${ctx.role}-${ctx.tick}-conflict`,
296
+ pi = await ctx.runPi(wt,
297
+ // Each file's hunk ranges, so the resolver reads the hunks rather than the file from line 1.
298
+ buildConflictPrompt(ctx.role, conflictedFileHunks(wt, remaining), check, {
299
+ change,
300
+ main: commits,
301
+ mainOmitted: omitted,
302
+ }), `tumwater-${ctx.role}-${ctx.tick}-conflict`,
293
303
  // The resolver rides the strong tier (plans/model-tiers.md part 4/8): resolution is rare,
294
- // tolerant of latency, and edits code inside landing. Its spend still folds into the
304
+ // tolerant of latency, and edits code inside merge. Its spend still folds into the
295
305
  // authoring role's usage — the fold belongs to the loop's wiring, not to the config.
296
306
  resolverConfig(ctx.config));
297
307
  }
298
308
  finally {
299
- setLandingStage(ctx.root, ctx.role, "merging");
309
+ setMergeStage(ctx.root, ctx.role, "merging");
300
310
  }
301
311
  if (!pi.ok || hasConflictMarkers(wt, remaining)) {
302
312
  await abortSync(wt);
@@ -0,0 +1,212 @@
1
+ // vocabulary: plain
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { gateRoleConfig } from "../gates/budget-gates.js";
5
+ import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
6
+ import { providerWaitStage } from "./merge-slot.js";
7
+ import { withProviderLease } from "../concurrency/provider-lease.js";
8
+ import { LoopRunner } from "../loop/loop.js";
9
+ import { branchHead, isMergedInto } from "../git/git.js";
10
+ import { addMergeChange, mergeUsage, removeMergeChange, setMergeChangeStatus } from "./merge-slot.js";
11
+ import { dropMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile } from "./merge-queue.js";
12
+ import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
13
+ import { errorMessage } from "../text/text.js";
14
+ import { verifyRequest } from "./merge-batch.js";
15
+ import { abortOnShutdown, acquireUnlessAborted, discardPinnedRefs, guardMergeTask, liveRoles, settleMergeOutcome, } from "./merge-pipeline.js";
16
+ /** The semaphore tier a verification waits at, below every roleTier (0/1): committed work whose
17
+ * author the interlock has already blocked jumps ahead of parked role waiters rather than starving
18
+ * behind them (BUGS.md 2026-09-18). Exported for merge-drain.ts, whose merge tier sits one
19
+ * below it. */
20
+ export const VERIFY_TIER = -1;
21
+ /** How many verifications may be in flight (parked or running) at once: every `maxConcurrent`
22
+ * permit but one, so a deep merge queue never takes the last slot from the roles that could
23
+ * author — at least one, so `maxConcurrent` 1 still merges. The merge's short conflict-resolution
24
+ * run is not counted: it is the one serial step every queued change waits on. */
25
+ export function verifyLimit(maxConcurrent) {
26
+ return Math.max(1, maxConcurrent - 1);
27
+ }
28
+ /** Resolve a merge entry's authoring runner: the live runner when the role is enabled
29
+ * (runners are never removed from the array on disable — only a warning event fires); a role
30
+ * 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
32
+ * on. Both share the live config, like every runner — the director takes directorConfig (the live
33
+ * config, its budget-gate exemption, unless a used-up window moved it), every other role takes
34
+ * roleConfig (gateRoleConfig's single home for the rule; the scheduler assigns the live runners
35
+ * by it every poll). */
36
+ function resolveAuthor(ctx, role) {
37
+ const { root, mainBranch, signal, runners, liveConfig, roleConfig, directorConfig } = ctx;
38
+ return (runners.find((r) => r.role === role) ??
39
+ new LoopRunner(root, role, gateRoleConfig(role, liveConfig, roleConfig, directorConfig), mainBranch, signal));
40
+ }
41
+ /** Drop a torn queue head. A torn head (torn or foreign — a partial write an older build left,
42
+ * 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) {
48
+ const stale = staleHeadFile(root);
49
+ if (!stale)
50
+ return;
51
+ dropMerge(stale);
52
+ warnEventBestEffort(root, TUMWATER_SOURCE, `merge queue head ${path.basename(stale)} is unreadable (torn or foreign) — dropped so the queue keeps moving`);
53
+ }
54
+ /** Already-merged verdicts cached per (root, sha) against the main head they were computed
55
+ * against. drainVerify re-runs its dedupe against main every poll, but a queued entry's sha is
56
+ * fixed and merge-base --is-ancestor against the same head always returns the same answer — the
57
+ * verdict can only change when main itself moves (a merge's fast-forward). Serving the verdict
58
+ * from this cache makes a poll whose queue is waiting on busy authors or the merge slot cost one
59
+ * branchHead ref-file read instead of one git spawn per entry, and a moved main recomputes each
60
+ * entry's verdict once (the new head misses the cache). Bounded: verdicts computed against an
61
+ * older head are stale falses no future poll can reuse, so they are the first pruned. */
62
+ const mergedIntoMainCache = new Map();
63
+ const MERGED_INTO_MAIN_CACHE_MAX = 128;
64
+ /** The cached dedupe verdict for `sha` against main at `head`, computing it through
65
+ * isMergedInto on a cache miss. `head` is the caller's already-resolved main head ("" when main
66
+ * does not exist yet — isMergedInto then fails and reads false, a verdict worth caching like any
67
+ * other since the key matches). */
68
+ async function mergedIntoMain(root, sha, mainBranch, head) {
69
+ const key = `${root}\u0000${sha}`;
70
+ const hit = mergedIntoMainCache.get(key);
71
+ if (hit?.head === head)
72
+ return hit.merged;
73
+ const merged = await isMergedInto(root, sha, mainBranch);
74
+ if (mergedIntoMainCache.size >= MERGED_INTO_MAIN_CACHE_MAX && !mergedIntoMainCache.has(key)) {
75
+ for (const [k, v] of mergedIntoMainCache)
76
+ if (v.head !== head)
77
+ mergedIntoMainCache.delete(k);
78
+ if (mergedIntoMainCache.size >= MERGED_INTO_MAIN_CACHE_MAX)
79
+ mergedIntoMainCache.clear();
80
+ }
81
+ mergedIntoMainCache.set(key, { head, merged });
82
+ return merged;
83
+ }
84
+ /** Start a verification, in queue order, for every queued entry whose role the pipeline does not
85
+ * already hold, up to verifyLimit — the shared semaphore bounds how many of them run. The torn-head
86
+ * drop comes first, and each entry is deduped against main before its verification starts: an entry
87
+ * whose sha main already holds (a crash between the fast-forward and the drop) is dropped without a
88
+ * run, with its marker record; a crash mid-verification leaves both entry and ref, so the entry is
89
+ * verified again — the established crash semantics. */
90
+ export async function drainVerify(ctx, p) {
91
+ dropTornHead(ctx.root);
92
+ const queue = queuedMergeFiles(ctx.root);
93
+ // One main-head read per poll with a non-empty queue (branchHead's ref-file fast path — no
94
+ // spawn), serving every entry's cached dedupe verdict below.
95
+ const mainHead = queue.length === 0 ? "" : ((await branchHead(ctx.root, ctx.mainBranch)) ?? "");
96
+ for (const { entry, file } of queue) {
97
+ if (p.verifying.size >= verifyLimit(ctx.semaphore.limit))
98
+ return;
99
+ if (liveRoles(p).has(entry.role))
100
+ continue;
101
+ if (await mergedIntoMain(ctx.root, entry.sha, ctx.mainBranch, mainHead)) {
102
+ dropMerge(file);
103
+ removeMergeChange(ctx.root, entry.role);
104
+ continue;
105
+ }
106
+ // The dedupe awaited git: a task that settled meanwhile may have dropped this very entry
107
+ // (its role was busy when the queue was listed) or claimed its role. Start nothing for it.
108
+ if (!fs.existsSync(file) || liveRoles(p).has(entry.role))
109
+ continue;
110
+ startVerify(ctx, p, entry, file);
111
+ }
112
+ }
113
+ /** Start one verification task and record it in `p.verifying`: parked until a shared permit comes,
114
+ * at VERIFY_TIER (a shutdown settles a parked verification at once, and one granted while the start
115
+ * gate is closed hands its permit back — either way nothing ran, and its entry and pin stay queued
116
+ * with no outcome written), then, holding the permit for its whole length: open the change's
117
+ * marker record and verify it — checkout at the pin, rebase onto main, gate check, review
118
+ * (verifyRequest), the verdict persisted by the gate. An approval moves it to `approved` (record
119
+ * `approved, awaiting merge`). Any other verdict — rejected, a review_error of either kind,
120
+ * main_red, a lost pin's "error", aborted — is final for this queue entry, so its outcome is
121
+ * written at once and its entry dropped, which frees its author on the next poll while the
122
+ * other verifications run on (BUGS.md 2026-09-23's early-rejection rule). A user-aborted
123
+ * verification discards its pin; a shutdown keeps it. Never rejects for a failed merge: an
124
+ * unexpected throw becomes an "error" outcome. Exported for
125
+ * test/fixtures/orchestrator-fixtures.ts's mergeHead, which merges one queue entry through the
126
+ * pipeline without draining the rest of the queue. */
127
+ export function startVerify(ctx, p, entry, file) {
128
+ const { root, mainBranch, signal, semaphore } = ctx;
129
+ const { role } = entry;
130
+ const author = resolveAuthor(ctx, role);
131
+ // The verification's provider key: the review seam's key for the author's role, so its reviewer
132
+ // run counts toward the same provider cap as the roles that share it
133
+ // (plans/provider-concurrency.md §4). The gate off runs no model, so then there is no key — the
134
+ // verification is bounded by the team total alone.
135
+ const reviewKey = ctx.liveConfig.review.enabled
136
+ ? seamConcurrencyKey(reviewRunConfig(author.config), ctx.piDefault)
137
+ : undefined;
138
+ const verify = {
139
+ promise: Promise.resolve(), // Replaced below; the placeholder satisfies the type.
140
+ controller: new AbortController(),
141
+ roles: [role],
142
+ userAborted: false,
143
+ parked: true,
144
+ };
145
+ const detachShutdown = abortOnShutdown(signal, verify.controller);
146
+ p.verifying.set(role, verify);
147
+ const task = (async () => {
148
+ try {
149
+ const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, verify.controller.signal, reviewKey);
150
+ verify.parked = false;
151
+ if (lease === null)
152
+ return; // a shutdown while parked
153
+ if (verify.controller.signal.aborted || ctx.startHeld()) {
154
+ lease.release(); // stopping, or a restart / team hold closed the gate while it waited
155
+ return;
156
+ }
157
+ const { usage, foldUsage } = mergeUsage(author, root);
158
+ let verdict;
159
+ const startedAt = Date.now();
160
+ try {
161
+ addMergeChange(root, entry, liveRoles(p));
162
+ // The verification's lease rides in the provider-lease store for the verification's body,
163
+ // so a review run that lands on a different provider than the verification's review seam
164
+ // re-keys it there, and a re-key park is shown by providerWaitStage; its slot wait drops
165
+ // 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) }, {
167
+ role,
168
+ sha: entry.sha,
169
+ tick: entry.run,
170
+ summary: entry.summary,
171
+ body: entry.body,
172
+ highFriction: entry.highFriction,
173
+ ...entryRevisionFields(entry),
174
+ }, { 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) });
175
+ }
176
+ catch (err) {
177
+ verdict = { kind: "result", result: "error" };
178
+ author.state.lastError = errorMessage(err);
179
+ }
180
+ finally {
181
+ lease.release();
182
+ }
183
+ if (verdict.kind === "stack" && !verify.userAborted) {
184
+ p.approved.set(role, {
185
+ entry,
186
+ file,
187
+ sha: verdict.sha,
188
+ ...(verdict.verifiedHead !== undefined ? { verifiedHead: verdict.verifiedHead } : {}),
189
+ startedAt,
190
+ author,
191
+ usage,
192
+ foldUsage,
193
+ roles: [role],
194
+ controller: new AbortController(),
195
+ userAborted: false,
196
+ });
197
+ setMergeChangeStatus(root, role, "approved");
198
+ }
199
+ else {
200
+ const result = verdict.kind === "stack" ? "aborted" : verdict.result;
201
+ await settleMergeOutcome(root, entry, author.state, result, Date.now() - startedAt, usage, file);
202
+ if (verify.userAborted)
203
+ await discardPinnedRefs(root, [role]);
204
+ }
205
+ }
206
+ finally {
207
+ detachShutdown();
208
+ p.verifying.delete(role);
209
+ }
210
+ })();
211
+ verify.promise = guardMergeTask(root, role, task);
212
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // vocabulary: plain
3
+ /** The `tumwater-notes` command — the second bin entry (dist/src/notes-cli.js), the one
4
+ * carrier for writing a role's note on every backend (plans/notes-command.md "The
5
+ * `tumwater-notes` command"). A run finds it on its PATH (src/agent/notes-shim.ts) and hands
6
+ * it only the note `TUMWATER_NOTES_PATH` names — the path comes from the environment tumwater
7
+ * set, never from an argument. It replaces the three legacy carriers (the pi extension, and
8
+ * the MCP server for Claude Code and OpenCode), validating and writing through the same
9
+ * validateRoleNote / writeRoleNote as those, so the 4 KB cap and atomic write cannot drift.
10
+ *
11
+ * Subcommands:
12
+ * write replace the note with stdin (refuses empty input)
13
+ * write --file <p> replace the note with the file at <p>
14
+ * show print the note (nothing when there is none)
15
+ * clear empty the note
16
+ *
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. */
19
+ import fs from "node:fs";
20
+ import { validateRoleNote, writeRoleNote } from "./agent/role-notes-write.js";
21
+ /** The note's path, read ONLY from the environment — never from an argument. */
22
+ const NOTES_PATH = process.env.TUMWATER_NOTES_PATH;
23
+ const USAGE = [
24
+ "usage: tumwater-notes <write [--file <path>] | show | clear>",
25
+ " write replaces the note with stdin, clear empties it, show prints it (--file reads <path> instead of stdin)",
26
+ " the note is the file TUMWATER_NOTES_PATH names",
27
+ ].join("\n");
28
+ /** Read the child's stdin to EOF as UTF-8. */
29
+ function readStdin() {
30
+ return fs.readFileSync(0, "utf8");
31
+ }
32
+ /** Read the note's current text; a missing note reads as empty (show prints nothing). */
33
+ function readNote() {
34
+ try {
35
+ return fs.readFileSync(NOTES_PATH, "utf8");
36
+ }
37
+ catch {
38
+ return "";
39
+ }
40
+ }
41
+ /** Refuse with `message` on stderr and exit 1 — the exit code for every expected refusal. */
42
+ function refuse(message) {
43
+ process.stderr.write(`${message}\n`);
44
+ return 1;
45
+ }
46
+ /** Write a non-empty note and announce it; assumes NOTES_PATH is set. Returns the exit code. */
47
+ function writeNote(text) {
48
+ if (text === "") {
49
+ return refuse("nothing to write — use `tumwater-notes clear` to empty the note");
50
+ }
51
+ const error = validateRoleNote(text);
52
+ if (error)
53
+ return refuse(error);
54
+ writeRoleNote(NOTES_PATH, text);
55
+ process.stdout.write(`notes saved (${Buffer.byteLength(text, "utf8")} bytes)\n`);
56
+ return 0;
57
+ }
58
+ function main() {
59
+ if (NOTES_PATH === undefined || NOTES_PATH === "") {
60
+ return refuse("this run keeps no notes");
61
+ }
62
+ const args = process.argv.slice(2);
63
+ const [command, flag, flagArg] = args;
64
+ switch (command) {
65
+ case "write":
66
+ if (flag === undefined)
67
+ return writeNote(readStdin());
68
+ if (flag === "--file") {
69
+ if (flagArg === undefined || args.length > 3)
70
+ return refuse(USAGE);
71
+ let text;
72
+ try {
73
+ text = fs.readFileSync(flagArg, "utf8");
74
+ }
75
+ catch {
76
+ return refuse(`cannot read ${flagArg}`);
77
+ }
78
+ return writeNote(text);
79
+ }
80
+ return refuse(USAGE);
81
+ case "show":
82
+ if (flag !== undefined)
83
+ return refuse(USAGE);
84
+ process.stdout.write(readNote());
85
+ return 0;
86
+ case "clear":
87
+ if (flag !== undefined)
88
+ return refuse(USAGE);
89
+ writeRoleNote(NOTES_PATH, "");
90
+ process.stdout.write("notes cleared\n");
91
+ return 0;
92
+ default:
93
+ process.stderr.write(`${USAGE}\n`);
94
+ return 2;
95
+ }
96
+ }
97
+ process.exit(main());