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,46 +1,47 @@
1
- /** Two of the three halves of a landing (plans/merge-queue.md 5/5, PLANS.md land-queue speed
2
- * 2c): the per-change vet (vetRequest) that landing-vetting.ts's vetting stage runs for every
3
- * queued change, and the merge (landVetted) its one merge slot runs over the vetted ones — a
1
+ // vocabulary: plain
2
+ /** Two of the three halves of a merge (plans/merge-queue.md 5/5, PLANS.md land-queue speed
3
+ * 2c): the per-change verify (verifyRequest) that merge-verify.ts's verify stage runs for every
4
+ * queued change, and the merge (mergeApproved) its one merge slot runs over the approved ones — a
4
5
  * stack of two or more sharing a single build check, with a per-change fallback. The stack
5
- * lander landVetted drives (assemble → one shared check → ff, with re-stack and bisect) lives
6
- * beside it in landing-stack.ts. The shared review gate (reviewPinnedChange) and the
7
- * one-change landing (landApprovedChange) live beside both in landing-core.ts. */
6
+ * merger mergeApproved drives (assemble → one shared check → ff, with re-stack and bisect) lives
7
+ * beside it in merge-stack.ts. The shared review gate (reviewPinnedChange) and the
8
+ * one-change merge (mergeApprovedChange) live beside both in merge-core.ts. */
8
9
  import { leaseSlot } from "../git/worktree-pool.js";
9
10
  import { slotCount } from "../config/config.js";
10
11
  import { mergeWorktreePath } from "../paths.js";
11
- import { landApprovedChange, reviewPinnedChange, syncPinToMain, } from "./landing-core.js";
12
- import { attributeRedCheck } from "./landing-check-failures.js";
12
+ import { mergeApprovedChange, reviewPinnedChange, syncPinToMain, } from "./merge-core.js";
13
+ import { attributeRedCheck } from "./merge-check-failures.js";
13
14
  import { errorMessage } from "../text/text.js";
14
- import { landStack } from "./landing-stack.js";
15
- /** Vet one request — the whole of a vetting-stage task: lease a pooled slot for the role, check
15
+ import { mergeStack } from "./merge-stack.js";
16
+ /** Verify one request — the whole of a verify-stage task: lease a pooled slot for the role, check
16
17
  * its pin out detached there, rebase it onto main's current tip (syncPinToMain — so the head
17
18
  * approved here is the head its merge starts from, and a conflict leaves the pin for the gate
18
19
  * and, at the merge, mergeToMain's resolver), and run the review gate, the verdict persisted
19
20
  * immediately by reviewPinnedChange (the drain's write-back of an approved change happens only
20
- * once it lands, so a crash must not lose what the vet earned). The lease is released in every
21
- * outcome (plans/worktree-pool.md, "Vets and merges"): the vet passes its result on through the
22
- * landing ref and VettedLanding, never through the slot, and every merge-side step re-ensures at
21
+ * once it merges, so a crash must not lose what the verify earned). The lease is released in every
22
+ * outcome (plans/worktree-pool.md, "Vets and merges"): the verify passes its result on through the
23
+ * merge ref and ApprovedMerge, never through the slot, and every merge-side step re-ensures at
23
24
  * a commit. leaseSlot holds the slot through the disk-floor registry and resets it to the pin, so
24
- * two vets rebasing at once — or one rebasing while a merge moves main — cannot interfere. */
25
- export async function vetRequest(ctx, req, w) {
25
+ * two verifies rebasing at once — or one rebasing while a merge moves main — cannot interfere. */
26
+ export async function verifyRequest(ctx, req, w) {
26
27
  let lease;
27
28
  try {
28
29
  lease = await leaseSlot(ctx.root, {
29
30
  role: req.role,
30
- purpose: "vet",
31
+ purpose: "verify",
31
32
  ref: req.sha,
32
33
  signal: ctx.signal(),
33
34
  // The live view's pool budget, same rule as the tick path: a broken tumwater.json
34
- // must not error the vet before its reviewer runs.
35
+ // must not error the verify before its reviewer runs.
35
36
  slotBudget: slotCount(ctx.config),
36
37
  });
37
38
  }
38
39
  catch (err) {
39
- // An abort while waiting for a free slot is the vet never starting — the same terminal
40
+ // An abort while waiting for a free slot is the verify never starting — the same terminal
40
41
  // "aborted" reviewPinnedChange returns when it observes an already-fired signal.
41
42
  if (err instanceof Error && err.name === "AbortError")
42
43
  return { kind: "result", result: "aborted" };
43
- // The queue entry outlived its pinned commit — the land queue outlives the ref by design
44
+ // The queue entry outlived its pinned commit — the merge queue outlives the ref by design
44
45
  // (a crash between pin and drop, or an outside gc), so a checkout of `req.sha` can fail
45
46
  // with the commit gone. Degrade to a terminal "error" for this request so the drain drops
46
47
  // its entry and the queue advances; a throw would keep the entry, and a lost pin would then
@@ -50,13 +51,13 @@ export async function vetRequest(ctx, req, w) {
50
51
  return { kind: "result", result: "error" };
51
52
  }
52
53
  try {
53
- return await vetRequestIn(ctx, req, w, lease.dir);
54
+ return await verifyRequestIn(ctx, req, w, lease.dir);
54
55
  }
55
56
  finally {
56
57
  lease.release();
57
58
  }
58
59
  }
59
- async function vetRequestIn(ctx, req, w, wt) {
60
+ async function verifyRequestIn(ctx, req, w, wt) {
60
61
  const synced = await syncPinToMain(ctx, wt, req);
61
62
  const outcome = await reviewPinnedChange(ctx, synced, wt, w.state, w.foldUsage);
62
63
  if (outcome.kind === "result")
@@ -66,67 +67,67 @@ async function vetRequestIn(ctx, req, w, wt) {
66
67
  const { verifiedHead } = outcome.gate;
67
68
  return verifiedHead === outcome.sha ? { kind: "stack", sha: outcome.sha, verifiedHead } : { kind: "stack", sha: outcome.sha };
68
69
  }
69
- /** Land changes whose own vet already approved them — the merge slot's whole task
70
- * (landing-drain.ts, land-queue speed 2c), over every vetted entry up to landBatchMax. `vetted` is
71
- * in queue order, each request at the head its vet approved (the synced pin its landing ref names);
72
- * none is gated again, so no model review runs here (an adversarial review of a stack would blur
73
- * which change a criticism applies to, so each change was reviewed alone). The flow:
70
+ /** Merge changes whose own verify already approved them — the merge slot's whole task
71
+ * (merge-drain.ts, land-queue speed 2c), over every approved entry up to mergeBatchMax. `approved`
72
+ * is in queue order, each request at the head its verify approved (the synced pin its merge ref
73
+ * names); none is gated again, so no model review runs here (an adversarial review of a stack would
74
+ * blur which change a criticism applies to, so each change was reviewed alone). The flow:
74
75
  *
75
- * One change — land it through landApprovedChange: git + ff, plus an in-lock scope-`landing`
76
- * re-check whenever main moved since its vet (and a seeded baseline when it did not and its vet's
77
- * pre-check ran green on exactly that head).
76
+ * One change — merge it through mergeApprovedChange: git + ff, plus an in-lock scope-`merge`
77
+ * re-check whenever main moved since its verify (and a seeded baseline when it did not and its
78
+ * verify's pre-check ran green on exactly that head).
78
79
  *
79
80
  * Two or more — assemble the stack in the shared `_merge` checkout, checked out detached at
80
- * main's CURRENT tip, then cherry-pick each change's full range from that tip to its head to land,
81
+ * main's CURRENT tip, then cherry-pick each change's full range from that tip to its head to merge,
81
82
  * in queue order — `base..sha`, every commit ahead of main (normally one), capturing each
82
83
  * post-pick tip — and run ONE scope-`batch` runScopedBuildCheck over the combined tree: the
83
- * expensive, deterministic half the stack shares. null (no declared check) → land directly;
84
+ * expensive, deterministic half the stack shares. null (no declared check) → merge directly;
84
85
  * "failed" (a red tree or a timeout remapped to a reject — the tree is unverified) → bisect;
85
86
  * "skipped" (no npm / broken toolchain — the helper already warned) → proceed, never
86
87
  * fail-closed; "passed" → green. Green: under the merge lock, ffStackToMain ff's main through the
87
88
  * stack in ONE fast-forward, emits one `merged` event per change, and — only when the check
88
89
  * PASSED on exactly that tip — seeds noteGreenBaseline with the stacked tip. ff failure (main
89
90
  * moved while the check ran: the window is the whole check, and main still has writers outside the
90
- * land queue) → RE-STACK: assemble the same changes afresh on main's new tip and go round again,
91
+ * merge queue) → RE-STACK: assemble the same changes afresh on main's new tip and go round again,
91
92
  * up to BATCH_RESTACK_ATTEMPTS times, stopping before any attempt on an abort. A re-stacked tree
92
93
  * that differs from the last tree a check ran on only in review-exempt paths goes straight to the
93
94
  * ff; any other re-stack pays one more check. Only a race lost on every attempt leaves each change
94
- * not yet landed its ref with "merge_blocked", nothing seeded, for leftover recovery.
95
+ * not yet merged its ref with "merge_blocked", nothing seeded, for leftover recovery.
95
96
  *
96
- * Red → LAND THE LARGEST PASSING PREFIX: bisect in queue order, each
97
- * step the same assemble → check → ff (landStack) over a prefix of the changes not yet landed
98
- * — the first half of the ones the last red check ran over — so every prefix that lands, lands
99
- * on its own green check with nothing rewritten before its ff. A green prefix lands and the rest of
100
- * the red run is bisected next; a red one is halved. The one change a red check ran over alone is
101
- * attributed through main's own baseline (landing-check-failures.ts's attributeRedCheck): main
97
+ * Red → MERGE THE LARGEST PASSING PREFIX: bisect in queue order, each
98
+ * step the same assemble → check → ff (mergeStack) over a prefix of the changes not yet merged
99
+ * — the first half of the ones the last red check ran over — so every prefix that merges, merges
100
+ * on its own green check with nothing rewritten before its ff. A green prefix merges and the rest
101
+ * of the red run is bisected next; a red one is halved. The one change a red check ran over alone
102
+ * is attributed through main's own baseline (merge-check-failures.ts's attributeRedCheck): main
102
103
  * green → rejected with the check's reasons, no pi run; main red → "main_red", pin kept. Its
103
104
  * red is the second one observed with it in the tree, so a single flaky run never rejects a change.
104
105
  * The changes after it stay unattempted for the next merge. A stack of N with one broken change
105
106
  * costs about log2(N) + 1 extra checks, never a second model review.
106
107
  *
107
108
  * Un-assemblable (a cherry-pick conflict, on the first stack or any prefix) → ABANDON the changes
108
- * not yet landed to one-at-a-time, in queue order, stopping at the first non-terminal outcome, each
109
- * through landApprovedChange — no second gate and no model run but mergeToMain's conflict
109
+ * not yet merged to one-at-a-time, in queue order, stopping at the first non-terminal outcome, each
110
+ * through mergeApprovedChange — no second gate and no model run but mergeToMain's conflict
110
111
  * resolver. main is never left red: the only bytes this path ff's are a checked tip or per-change
111
- * landings re-verified in-lock whenever they differ from what their vet judged.
112
+ * merges re-verified in-lock whenever they differ from what their verify judged.
112
113
  *
113
114
  * An abort is observed between steps, not only by the pi runs it kills: before each
114
- * assembly-and-check attempt and before each one-change or fallback landing, so a stopping merge
115
+ * assembly-and-check attempt and before each one-change or fallback merge, so a stopping merge
115
116
  * ends at its next step boundary (BUGS.md 2026-09-23). A shared check that has already run is
116
117
  * followed through: its fast-forward is the merge's bounded commit point.
117
118
  *
118
119
  * Returns one result per request in order; `undefined` means unattempted (behind a bisect's
119
120
  * attributed change, or a fallback early stop) — entry and ref kept for the next merge. On an
120
121
  * abort every request without a result reads "aborted" (refs kept). `wiringFor` must return the
121
- * same wiring for a role on every call (its usage accumulator is the landing's). Never throws for a
122
- * failed landing — per-change landApprovedChange failures degrade to "error", and so does a throw
122
+ * same wiring for a role on every call (its usage accumulator is the merge's). Never throws for a
123
+ * failed merge — per-change mergeApprovedChange failures degrade to "error", and so does a throw
123
124
  * from any bisect step after the first stack attempt, on the change at the front of that step (a
124
125
  * prefix may already be on main, and a throw would lose its "changed"); only the first stack
125
126
  * attempt's git plumbing propagates, and the merge slot then keeps every entry. */
126
- export async function landVetted(ctx, vetted, wiringFor) {
127
- const results = vetted.map(() => undefined);
128
- const report = (s, status) => ctx.onChangeStatus?.(vetted[s].role, status);
129
- const landerCtx = (w) => ({
127
+ export async function mergeApproved(ctx, approved, wiringFor) {
128
+ const results = approved.map(() => undefined);
129
+ const report = (s, status) => ctx.onChangeStatus?.(approved[s].role, status);
130
+ const mergerCtx = (w) => ({
130
131
  root: ctx.root,
131
132
  mainBranch: ctx.mainBranch,
132
133
  config: ctx.config,
@@ -145,62 +146,62 @@ export async function landVetted(ctx, vetted, wiringFor) {
145
146
  await ctx.onResult?.(s, result);
146
147
  };
147
148
  const finish = () => aborted ? results.map((r) => r ?? "aborted") : results;
148
- // The landing step the degenerate case and the abandon fallback share: land one request
149
- // through landApprovedChange on its role's wiring, and degrade a throw to "error" on that
149
+ // The merge step the degenerate case and the abandon fallback share: merge one request
150
+ // through mergeApprovedChange on its role's wiring, and degrade a throw to "error" on that
150
151
  // change (its ref kept for recovery) with the reason on the role's state — a throw means
151
152
  // this change's plumbing failed, never that an earlier one is lost (see the catches below
152
- // and test/lander-errors.test.ts, which pins the degradation).
153
- const landOne = async (s, req) => {
153
+ // and test/merger-errors.test.ts, which pins the degradation).
154
+ const mergeOne = async (s, req) => {
154
155
  try {
155
- await emit(s, await landApprovedChange(landerCtx(wiringFor(req.role)), req));
156
+ await emit(s, await mergeApprovedChange(mergerCtx(wiringFor(req.role)), req));
156
157
  }
157
158
  catch (err) {
158
159
  wiringFor(req.role).state.lastError = errorMessage(err);
159
160
  await emit(s, "error");
160
161
  }
161
162
  };
162
- // ── The degenerate case: one vetted change lands on its own ──────────────────────────
163
- if (vetted.length === 1) {
164
- const req = vetted[0];
165
- report(0, "landing");
166
- await landOne(0, req);
163
+ // ── The degenerate case: one approved change merges on its own ──────────────────────────
164
+ if (approved.length === 1) {
165
+ const req = approved[0];
166
+ report(0, "merging");
167
+ await mergeOne(0, req);
167
168
  return results;
168
169
  }
169
170
  // ── Assemble the stack in the shared `_merge` checkout: ONE check over the whole tree, and
170
171
  // on a red one, the largest passing prefix
171
172
  // The shared `_merge` checkout hosts every assembly and the attribution's baseline check:
172
- // the drain is serial, so one checkout is enough, and no vet runs for a role while its
173
+ // the drain is serial, so one checkout is enough, and no verify runs for a role while its
173
174
  // change is being merged (plans/worktree-pool.md, part 2c).
174
175
  const wtPath = mergeWorktreePath(ctx.root);
175
- const entries = vetted.map((req) => ({ role: req.role, sha: req.sha, summary: req.summary }));
176
+ const entries = approved.map((req) => ({ role: req.role, sha: req.sha, summary: req.summary }));
176
177
  let abandon = false;
177
- // Every stacked change is landing now: they share the assembly, the check, and the ff.
178
- for (let s = 0; s < vetted.length; s++)
179
- report(s, "landing");
180
- // Stack positions [0, landedCount) are on main. `suspect` is how many of the rest, from the
178
+ // Every stacked change is merging now: they share the assembly, the check, and the ff.
179
+ for (let s = 0; s < approved.length; s++)
180
+ report(s, "merging");
181
+ // Stack positions [0, mergeDoneCount) are on main. `suspect` is how many of the rest, from the
181
182
  // front, the last red check ran over — the prefix that still holds what broke it; null when
182
183
  // no red check stands against the rest, which are then tried together.
183
- let landedCount = 0;
184
+ let mergeDoneCount = 0;
184
185
  let suspect = null;
185
- while (landedCount < entries.length) {
186
- const front = landedCount;
186
+ while (mergeDoneCount < entries.length) {
187
+ const front = mergeDoneCount;
187
188
  const size = suspect === null ? entries.length - front : suspect === 1 ? 1 : Math.floor(suspect / 2);
188
189
  const firstAttempt = front === 0 && suspect === null;
189
190
  let outcome;
190
191
  try {
191
- outcome = await landStack(ctx, wtPath, entries.slice(front, front + size));
192
+ outcome = await mergeStack(ctx, wtPath, entries.slice(front, front + size));
192
193
  if (outcome.kind === "red" && size === 1) {
193
194
  // The one change this red check ran over alone: attribute it, and leave every change
194
195
  // after it unattempted for the next drain.
195
196
  const { state } = wiringFor(entries[front].role);
196
197
  const entry = entries[front];
197
- await emit(front, await attributeRedCheck(ctx, entry.role, entry.sha, "batch check", outcome, state));
198
+ await emit(front, await attributeRedCheck(ctx, entry.role, entry.sha, "merge check", outcome, state));
198
199
  report(front, "done");
199
200
  break;
200
201
  }
201
202
  }
202
203
  catch (err) {
203
- // The first attempt's plumbing propagates, as it always has — nothing has landed. A
204
+ // The first attempt's plumbing propagates, as it always has — nothing has merged. A
204
205
  // later step's degrades to "error" on the change at its front (ref kept for recovery),
205
206
  // the rest unattempted, so the prefixes already on main keep their "changed".
206
207
  if (firstAttempt)
@@ -210,13 +211,13 @@ export async function landVetted(ctx, vetted, wiringFor) {
210
211
  report(front, "done");
211
212
  break;
212
213
  }
213
- if (outcome.kind === "landed") {
214
+ if (outcome.kind === "landed") { // vocabulary-ok: persisted state value or event type, renamed by a later series
214
215
  for (let s = front; s < front + size; s++) {
215
216
  await emit(s, "changed");
216
217
  report(s, "done");
217
218
  }
218
- landedCount += size;
219
- // The shared `_merge` checkout persists across merges, so a landed stack leaves it.
219
+ mergeDoneCount += size;
220
+ // The shared `_merge` checkout persists across merges, so a merged stack leaves it.
220
221
  // What broke the red run is still in its remainder, unless this prefix WAS that run —
221
222
  // then the red did not reproduce, and the rest are tried together.
222
223
  suspect = suspect !== null && suspect > size ? suspect - size : null;
@@ -230,8 +231,8 @@ export async function landVetted(ctx, vetted, wiringFor) {
230
231
  }
231
232
  else if (outcome.kind === "blocked") {
232
233
  // The race was lost on every attempt — main is moving faster than a check completes.
233
- // Every change not yet landed keeps its ref with "merge_blocked": leftover recovery
234
- // re-lands each through its own gate + tryMerge, whose in-lock check cannot lose it.
234
+ // Every change not yet merged keeps its ref with "merge_blocked": leftover recovery
235
+ // re-merges each through its own gate + tryMerge, whose in-lock check cannot lose it.
235
236
  for (let s = front; s < entries.length; s++)
236
237
  await emit(s, "merge_blocked");
237
238
  break;
@@ -242,20 +243,20 @@ export async function landVetted(ctx, vetted, wiringFor) {
242
243
  }
243
244
  }
244
245
  if (abandon) {
245
- // One-at-a-time through landApprovedChange, queue order, stopping at the first non-terminal
246
- // outcome (the rest keep entry + ref for the next merge). Each request lands its approved
247
- // head (the synced pin its vet judged, not the bare pin) — no second gate, so no model run:
246
+ // One-at-a-time through mergeApprovedChange, queue order, stopping at the first non-terminal
247
+ // outcome (the rest keep entry + ref for the next merge). Each request merges its approved
248
+ // head (the synced pin its verify judged, not the bare pin) — no second gate, so no model run:
248
249
  // re-gating would rebase onto the main the earlier entries just moved, and the rewritten
249
250
  // sha misses the approved short-circuit. main is never left red — mergeToMain's in-lock
250
- // rebase + verifyLanding re-check every change whose tree differs from the one its vet
251
- // judged. Only the change being landed reads `landing`; the rest are back to awaiting
252
- // their turn, and each one landed is done.
253
- for (let s = landedCount; s < vetted.length; s++)
254
- report(s, "vetted");
255
- for (let s = landedCount; s < vetted.length; s++) {
256
- const req = vetted[s];
257
- report(s, "landing");
258
- await landOne(s, req);
251
+ // rebase + verifyMerge re-check every change whose tree differs from the one its verify
252
+ // judged. Only the change being merged reads `merging`; the rest are back to awaiting
253
+ // their turn, and each one merged is done.
254
+ for (let s = mergeDoneCount; s < approved.length; s++)
255
+ report(s, "approved");
256
+ for (let s = mergeDoneCount; s < approved.length; s++) {
257
+ const req = approved[s];
258
+ report(s, "merging");
259
+ await mergeOne(s, req);
259
260
  if (results[s] === "aborted")
260
261
  aborted = true;
261
262
  report(s, "done");
@@ -265,7 +266,7 @@ export async function landVetted(ctx, vetted, wiringFor) {
265
266
  }
266
267
  // A change the merge stopped short of is out of it (its entry and ref wait for the next
267
268
  // merge, or an abort's write-back), so it must not keep a live row meanwhile.
268
- for (let s = 0; s < vetted.length; s++) {
269
+ for (let s = 0; s < approved.length; s++) {
269
270
  if (results[s] === undefined)
270
271
  report(s, "done");
271
272
  }
@@ -1,53 +1,56 @@
1
+ // vocabulary: plain
1
2
  import { deleteRef, headOf, patchId } from "../git/git.js";
2
- import { landingRefName } from "../paths.js";
3
+ import { mergeRefName } from "../paths.js";
3
4
  import { recordReview } from "../tick/tick-apply.js";
4
5
  import { saveLoopState } from "../loop/loop-state.js";
5
6
  import { unverifiedTreeOutcome } from "../build/build-check-events.js";
6
- import { checkFailureReasons } from "../build/build-check-report.js";
7
+ import { checkFailureReasons, describeCheck, failureHeadline } from "../build/build-check-report.js";
7
8
  import { mainTipVerdict } from "../baseline/main-red.js";
8
- import { logEventBestEffort } from "../events/events.js";
9
+ import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
9
10
  import { mainRedNotMine } from "../text/phrases.js";
10
- /** Consecutive red in-lock landing checks of one patch (LoopState.landingCheckFailures) before
11
- * the landing is attributed instead of retried: the first red keeps the pin for one more
11
+ import { normalizeClusterKey } from "../failure/failure-cluster.js";
12
+ import { MERGE_FLAKY_WARNING_PREFIX, isKnownFlake } from "../review/known-flakes.js";
13
+ /** Consecutive red in-lock merge checks of one patch (LoopState.mergeCheckFailures) before
14
+ * the merge is attributed instead of retried: the first red keeps the pin for one more
12
15
  * attempt — a load flake gets its retry, as the gate's pre-check gets one (PLANS.md land-queue
13
16
  * 1/3) — and the second is judged by main's own verdict at its tip (attributeRedCheck). Without
14
- * a limit a change whose gate passes but whose landing check fails (a cheaper check.gateCommand
17
+ * a limit a change whose gate passes but whose merge check fails (a cheaper check.gateCommand
15
18
  * than the full check) would re-queue as merge_blocked forever, its role never authoring. */
16
- export const LANDING_CHECK_FAILURE_LIMIT = 2;
17
- /** Whether a deterministic failure keeps its pin for one more re-land instead of being
19
+ export const MERGE_CHECK_FAILURE_LIMIT = 2;
20
+ /** Whether a deterministic failure keeps its pin for one more re-merge instead of being
18
21
  * attributed to the change: always when the patch-id could not be read (it cannot be matched
19
- * to a prior strike, so it never reaches LANDING_CHECK_FAILURE_LIMIT — the pre-cap behavior,
22
+ * to a prior strike, so it never reaches MERGE_CHECK_FAILURE_LIMIT — the pre-cap behavior,
20
23
  * never a wrong attribution), otherwise while the strike count is still under the limit. The
21
24
  * one home of that rule: tallyCheckFailure's strike store and keepPinOrAttribute's
22
25
  * keep-or-attribute branch both read it, so the two cannot disagree on when the limit is
23
26
  * reached. */
24
27
  function pinKept(patch, count) {
25
- return patch === null || count < LANDING_CHECK_FAILURE_LIMIT;
28
+ return patch === null || count < MERGE_CHECK_FAILURE_LIMIT;
26
29
  }
27
30
  /** Tally one deterministic failure of the same patch toward attribution: read the worktree's
28
- * head and its patch-id against main, carry LoopState.landingCheckFailures's count across
29
- * re-lands of the identical patch (a clean rebase onto a moved main keeps the patch-id, so the
31
+ * head and its patch-id against main, carry LoopState.mergeCheckFailures's count across
32
+ * re-merges of the identical patch (a clean rebase onto a moved main keeps the patch-id, so the
30
33
  * retries of one change add up while a different change starts fresh), and store the strike.
31
34
  * An unreadable patch-id cannot be matched, so pinKept never counts it toward the limit.
32
35
  * Keeping the strike only under the limit (pinKept) is what ends the streak at attribution. */
33
36
  async function tallyCheckFailure(ctx, wt) {
34
37
  const head = await headOf(wt, "HEAD");
35
38
  const patch = await patchId(wt, ctx.mainBranch, head);
36
- const prior = ctx.state.landingCheckFailures;
39
+ const prior = ctx.state.mergeCheckFailures;
37
40
  const count = (patch !== null && prior?.patchId === patch ? prior.count : 0) + 1;
38
41
  if (pinKept(patch, count)) {
39
- ctx.state.landingCheckFailures = patch === null ? undefined : { patchId: patch, count };
42
+ ctx.state.mergeCheckFailures = patch === null ? undefined : { patchId: patch, count };
40
43
  }
41
44
  else {
42
- ctx.state.landingCheckFailures = undefined;
45
+ ctx.state.mergeCheckFailures = undefined;
43
46
  }
44
47
  return { head, patch, count };
45
48
  }
46
- /** The shared re-land rule of the two deterministic-block paths below (landingCheckRed and
47
- * landingBlocked): tally the strike on this patch (tallyCheckFailure), then either keep the
49
+ /** The shared re-merge rule of the two deterministic-block paths below (mergeCheckRed and
50
+ * mergeBlocked): tally the strike on this patch (tallyCheckFailure), then either keep the
48
51
  * pin for one more attempt — merge_blocked with `reason` named in lastError — or hand the head
49
52
  * to the caller's terminal sink `attribute`. Both paths decide keep-vs-attribute through
50
- * pinKept, so the LANDING_CHECK_FAILURE_LIMIT rule has one home. */
53
+ * pinKept, so the MERGE_CHECK_FAILURE_LIMIT rule has one home. */
51
54
  async function keepPinOrAttribute(ctx, wt, reason, attribute) {
52
55
  const { head, patch, count } = await tallyCheckFailure(ctx, wt);
53
56
  if (pinKept(patch, count)) {
@@ -56,9 +59,9 @@ async function keepPinOrAttribute(ctx, wt, reason, attribute) {
56
59
  }
57
60
  return attribute(head);
58
61
  }
59
- /** The terminal deterministic-reject sink shared by landingBlocked and attributeRedCheck:
62
+ /** The terminal deterministic-reject sink shared by mergeBlocked and attributeRedCheck:
60
63
  * record the reject review, reset the unreviewed-failure streak, persist state, delete the
61
- * landing ref, and log review_rejected. The shared `_merge` checkout is left in place
64
+ * merge ref, and log review_rejected. The shared `_merge` checkout is left in place
62
65
  * (plans/worktree-pool.md, part 2c): it persists across merges like `_gate-main`, and a
63
66
  * rejection discards the pinned ref, not the checkout. Returns "rejected". review.ts's reject
64
67
  * path is a different sink — it resets the branch instead of deleting the ref — and keeps its
@@ -67,58 +70,92 @@ async function rejectChange(ctx, role, head, reasons, state) {
67
70
  recordReview(state, "reject", reasons, head);
68
71
  state.unreviewFailures = 0;
69
72
  saveLoopState(ctx.root, state);
70
- await deleteRef(ctx.root, landingRefName(role));
71
- logEventBestEffort(ctx.root, { loop: role, type: "review_rejected", head, reasons });
73
+ await deleteRef(ctx.root, mergeRefName(role));
74
+ logEventBestEffort(ctx.root, { source: role, type: "review_rejected", head, reasons });
72
75
  return "rejected";
73
76
  }
74
77
  /** The one-line reason a red check's lastError carries: checkFailureReasons' first entry (it
75
- * always returns at least one). Shared by the blocked-landing paths below — landingCheckRed's
78
+ * always returns at least one). Shared by the blocked-merge paths below — mergeCheckRed's
76
79
  * two branches and attributeRedCheck's unverified branch — so the "name the red by its first
77
80
  * reason" rule has one home while the full reason list stays where a whole review needs it
78
81
  * (attributeRedCheck's reject reasons). */
79
82
  function firstCheckReason(red) {
80
83
  return checkFailureReasons(red.check, red.outcome)[0];
81
84
  }
82
- /** A landing blocked because its in-lock check went red on the rebased tree (landing-merge.ts's
83
- * verifyLanding). The count is keyed by the patch-id; under LANDING_CHECK_FAILURE_LIMIT the
84
- * pin is kept (merge_blocked) for recovery's re-land; at the limit the red is attributed like
85
+ /** A merge blocked because its in-lock check went red on the rebased tree (merge-to-main.ts's
86
+ * verifyMerge). The count is keyed by the patch-id; under MERGE_CHECK_FAILURE_LIMIT the
87
+ * pin is kept (merge_blocked) for recovery's re-merge; at the limit the red is attributed like
85
88
  * any single-change red. The other merge_blocked causes — a fast-forward that fails on a dirty
86
89
  * primary checkout, or main moving under the ff — are never the change's fault and never
87
90
  * counted. An unverified red made no verdict about the tree — the run spanned a host sleep
88
91
  * (BUGS.md 2026-09-30) — so it is not a strike against the patch: the pin stays for the next
89
92
  * attempt and the lastError names the sleep instead of a test failure. */
90
- export async function landingCheckRed(ctx, role, wt, red) {
93
+ export async function mergeCheckRed(ctx, role, wt, red) {
91
94
  if (unverifiedTreeOutcome(red.outcome)) {
92
95
  ctx.state.lastError = `merge failed: ${firstCheckReason(red)}`;
93
96
  return "merge_blocked";
94
97
  }
95
- return keepPinOrAttribute(ctx, wt, firstCheckReason(red), (head) => attributeRedCheck(ctx, role, head, "landing check", red, ctx.state));
98
+ const reason = firstCheckReason(red);
99
+ let attributed = false;
100
+ const result = await keepPinOrAttribute(ctx, wt, reason, (head) => {
101
+ attributed = true;
102
+ return attributeRedCheck(ctx, role, head, "merge check", red, ctx.state);
103
+ });
104
+ // The first red of a patch's in-lock check is exactly the retry that exists for load flakes,
105
+ // so it is the ONE signal that could name a flaky test after the fact — and lastError, the
106
+ // only place this reason is written, is overwritten by the leftover-recovery tick a second
107
+ // later (BUGS.md 2026-10-14). Log it as a warning so the failure digest and a later audit can
108
+ // name the flaky test; the second red at MERGE_CHECK_FAILURE_LIMIT is attributed through
109
+ // review_rejected, whose reasons are already durable.
110
+ if (result === "merge_blocked" && !attributed) {
111
+ warnEventBestEffort(ctx.root, role, `merge check failed (kept for one retry): ${reason}`);
112
+ if (ctx.state.mergeCheckFailures) {
113
+ ctx.state.mergeCheckFailures.headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ /** Record a merge check that failed and then passed on its kept-pin retry as a flake, in the
119
+ * form the gate's pre-check uses (MERGE_FLAKY_WARNING_PREFIX), so the gate's known-flake memory,
120
+ * doctor's `flaky checks` and the tick prompt's flake note count it (PLANS.md 2026-10-16). Reads
121
+ * the headline mergeCheckRed stored on the strike record only when its patch-id is the merged
122
+ * pin's (a stale strike of a discarded pin must not be logged); a pin that was never red (or
123
+ * only cross-check blocked) has none and logs nothing. Call before the strike is cleared. */
124
+ export function noteMergeRetryPassed(ctx, role, pinPatch) {
125
+ const strike = ctx.state.mergeCheckFailures;
126
+ const headline = strike?.patchId === pinPatch ? strike?.headline : undefined;
127
+ if (headline)
128
+ warnEventBestEffort(ctx.root, role, MERGE_FLAKY_WARNING_PREFIX + headline);
96
129
  }
97
- /** A landing blocked by a deterministic cross-check (onLandingBlocked's fix-claim or
130
+ /** A merge blocked by a deterministic cross-check (onMergeBlocked's fix-claim or
98
131
  * backlog-structure reason — a property of the tree ahead of main, not a flaky run). The
99
- * block is counted like a red landing check, keyed by the same patch-id: under
100
- * LANDING_CHECK_FAILURE_LIMIT the pin is kept for one recovery re-land — main may have moved
132
+ * block is counted like a red merge check, keyed by the same patch-id: under
133
+ * MERGE_CHECK_FAILURE_LIMIT the pin is kept for one recovery re-merge — main may have moved
101
134
  * under the rebase, and a differently-rebased tree can pass the cross-check — but at the
102
135
  * limit the block is attributed to the change: rejected deterministically with the block
103
136
  * reason, the pin deleted, no model run. Without this a resolution that trips the same
104
137
  * cross-check on every re-resolve pays a fresh conflict-resolver run each tick, forever
105
138
  * (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). */
106
- export async function landingBlocked(ctx, role, wt, blocked) {
107
- return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`landing blocked: ${blocked}`], ctx.state));
139
+ export async function mergeBlocked(ctx, role, wt, blocked) {
140
+ return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`merge blocked: ${blocked}`], ctx.state));
108
141
  }
109
- /** Attribute a check that went red over ONE change's tree after its vet approved it — a batch
110
- * bisect's last step (landing-batch.ts), or a single landing's in-lock check at
111
- * LANDING_CHECK_FAILURE_LIMIT — by the gate's rule: ask main's own
112
- * verdict at its current tip (mainTipVerdict — usually a cache hit, since every landing seeds
142
+ /** Attribute a check that went red over ONE change's tree after its verify approved it — a batch
143
+ * bisect's last step (merge-batch.ts), or a single merge's in-lock check at
144
+ * MERGE_CHECK_FAILURE_LIMIT — by the gate's rule: ask main's own
145
+ * verdict at its current tip (mainTipVerdict — usually a cache hit, since every merge seeds
113
146
  * the SHA it moved main to). Main green → the change broke the check: rejected deterministically
114
147
  * — reasons in lastReview for the author's next tick, the strike count reset, ref deleted,
115
148
  * review_rejected logged, no pi run. Main red → not this change's failure: "main_red" with the
116
- * ref kept and unreviewFailures untouched, so recovery re-lands it once main-red.ts's repair
149
+ * ref kept and unreviewFailures untouched, so recovery re-merges it once main-red.ts's repair
117
150
  * turns main green. No verdict → reject, the safe default, and the reasons say so. The verdict
118
- * is persisted before it returns. */
151
+ * is persisted before it returns. A red whose headline matches a flake the gate or a merge
152
+ * retry recorded (isKnownFlake) is excused once per headline per pin (LoopState.mergeFlakeExcused):
153
+ * "merge_blocked" with the ref kept, before main's verdict, because that verdict is usually a
154
+ * cached green that would reject a change over a flaky test (PLANS.md 2026-10-16, BUGS.md
155
+ * 2026-10-06). A repeat of the same headline is attributed as above. */
119
156
  export async function attributeRedCheck(ctx, role, head, label, red, state) {
120
157
  // An unverified red — a run that spanned a host sleep, the tree never judged — is not the
121
- // change's failure and not a strike: keep the ref for recovery's re-land and name the sleep
158
+ // change's failure and not a strike: keep the ref for recovery's re-merge and name the sleep
122
159
  // (BUGS.md 2026-09-30). Main's own verdict is beside the point: the attribution question is
123
160
  // only live when the check produced a verdict.
124
161
  if (unverifiedTreeOutcome(red.outcome)) {
@@ -126,6 +163,15 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
126
163
  saveLoopState(ctx.root, state);
127
164
  return "merge_blocked";
128
165
  }
166
+ const headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
167
+ const key = normalizeClusterKey(headline);
168
+ if (isKnownFlake(ctx.root, headline) && state.mergeFlakeExcused !== key) {
169
+ state.mergeFlakeExcused = key;
170
+ state.lastError = `${label} failed: known-flaky failure, merge kept: ${headline}`;
171
+ warnEventBestEffort(ctx.root, role, `${label}: known-flaky failure, merge kept: ${headline}`);
172
+ saveLoopState(ctx.root, state);
173
+ return "merge_blocked";
174
+ }
129
175
  const main = await mainTipVerdict(ctx.root, role, ctx.mainBranch, ctx.config);
130
176
  if (main.status === "red") {
131
177
  state.lastError = `${label} failed: ${mainRedNotMine(main.sha)}`;
@@ -134,7 +180,7 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
134
180
  }
135
181
  const reasons = checkFailureReasons(red.check, red.outcome);
136
182
  if (main.status === "unavailable") {
137
- reasons.push(`main's own baseline was unavailable (${main.why}), so the red ${label} is attributed to this change`);
183
+ reasons.push(`main's own baseline was unavailable (${main.why}), so the failed ${label} is attributed to this change`);
138
184
  }
139
185
  // Terminal rejection: the sink deletes the pin; the shared `_merge` checkout persists.
140
186
  return rejectChange(ctx, role, head, reasons, state);