tumwater 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -7,35 +7,67 @@
7
7
  * for that shape has an empty base section and both sides non-empty; keeping both inserted sides
8
8
  * adds no authored bytes, so no model run is needed.
9
9
  *
10
- * One more shape settles the same way: inside a live `## Done`/`## Fixed` section, the change
11
- * added whole `### ` entries right above lines that main has since removed or rewritten — a
12
- * change committed before its repository's history moved to the archive, whose entries sat
13
- * right below the insertion point. The change's side is then its new entries followed by
14
- * exactly the base lines, and keeping those entries on top of main's side again adds no
15
- * authored bytes. Anything else — the same entry edited on both sides, a code file, a deleted
16
- * side — goes to the pi resolver untouched. */
10
+ * Two more shapes settle the same way:
11
+ * - Entries above a deleted entry, in any `## ` section: one side added whole `### ` entries
12
+ * right above an entry the other side deleted. Typically the plan role prepends a plan on
13
+ * main while the change finishes the plan below it into PLANS-DONE.md, or the other way
14
+ * round. The inserting side is its new entries followed by exactly the base lines and the
15
+ * deleting side is empty, so the new entries alone carry both sides' edits.
16
+ * - Entries above a rewritten history: inside a live `## Done`/`## Fixed` section, the change
17
+ * added whole `### ` entries right above lines that main has since rewritten — a change
18
+ * committed before its repository's history moved to the archive, whose entries sat right
19
+ * below the insertion point. The change's side is then its new entries followed by exactly
20
+ * the base lines, and keeping those entries on top of main's side again adds no authored
21
+ * bytes.
22
+ * A fourth shape settles entry by entry instead of hunk by hunk (mergeBacklogEntries): each side
23
+ * finished or added different whole entries, so git's hunks cut through entries and match none
24
+ * of the shapes above. It reads the three index stages of PLANS.md, BUGS.md and QUESTIONS.md,
25
+ * not the hunks.
26
+ * Anything else — the same entry edited on both sides, a code file, a file one side deleted —
27
+ * goes to the pi resolver untouched. */
17
28
  import fs from "node:fs";
18
29
  import path from "node:path";
19
- import { BACKLOG_FILES, FINISHED_ARCHIVES } from "../backlog/backlog-md.js";
20
- import { writeTextAtomic } from "../files/files.js";
21
- import { git } from "../git/git-run.js";
22
- /** The lines the change inserted in `hunk`, when it is one of the two shapes this pass settles,
23
- * or null. Insert-only: an empty base section and both sides carrying lines — all of the
24
- * change's side. Entries above a rewritten history (see the module comment): the hunk sits in a
25
- * finished section, and the change's side is whole `### ` entries followed by exactly the base
26
- * lines — the part before them. */
27
- function insertedLines(hunk, lines) {
28
- if (hunk.base.length === 0)
29
- return hunk.ours.length > 0 && hunk.theirs.length > 0 ? hunk.theirs : null;
30
- if (!inFinishedSection(lines, hunk.start) || hunk.theirs.length <= hunk.base.length)
30
+ import { BACKLOG_FILES, FINISHED_ARCHIVES, fenceTracker } from "../backlog/backlog-md.js";
31
+ import { readTextOrNull, writeTextAtomic } from "../files/files.js";
32
+ import { git, gitTry } from "../git/git-run.js";
33
+ /** The lines that replace `hunk`, when it is one of the shapes this pass settles (see the
34
+ * module comment), or null. Insert-only: an empty base section and both sides carrying lines —
35
+ * the change's side stacked on main's. Entries above a deleted entry: one side empty, the other
36
+ * whole `### ` entries followed by exactly the base lines — those entries alone. Entries above
37
+ * a rewritten history: the hunk sits in a finished section, and the change's side is whole
38
+ * `### ` entries followed by exactly the base lines — those entries stacked on main's side. */
39
+ function settledLines(hunk, lines) {
40
+ if (hunk.base.length === 0) {
41
+ return hunk.ours.length > 0 && hunk.theirs.length > 0 ? stacked(hunk.theirs, hunk.ours) : null;
42
+ }
43
+ if (hunk.theirs.length === 0)
44
+ return entriesAboveBase(hunk.ours, hunk.base);
45
+ if (hunk.ours.length === 0)
46
+ return entriesAboveBase(hunk.theirs, hunk.base);
47
+ if (!inFinishedSection(lines, hunk.start))
48
+ return null;
49
+ const inserted = entriesAboveBase(hunk.theirs, hunk.base);
50
+ return inserted === null ? null : stacked(inserted, hunk.ours);
51
+ }
52
+ /** The lines `side` adds above the `base` lines, when `side` is whole `### ` entries followed
53
+ * by exactly those base lines, or null — a side no longer than the base, a base line changed or
54
+ * missing, or added lines that do not open at a `### ` heading. */
55
+ function entriesAboveBase(side, base) {
56
+ if (side.length <= base.length)
31
57
  return null;
32
- const cut = hunk.theirs.length - hunk.base.length;
33
- if (hunk.theirs.slice(cut).some((line, i) => line !== hunk.base[i]))
58
+ const cut = side.length - base.length;
59
+ if (side.slice(cut).some((line, i) => line !== base[i]))
34
60
  return null;
35
- const inserted = hunk.theirs.slice(0, cut);
61
+ const inserted = side.slice(0, cut);
36
62
  const first = inserted.find((line) => line.trim() !== "");
37
63
  return first?.startsWith("### ") ? inserted : null;
38
64
  }
65
+ /** `upper` then `lower`, with a blank separator when neither already supplies one, so the
66
+ * later merge (the change, `upper`) stays first. */
67
+ function stacked(upper, lower) {
68
+ const separated = lower.length === 0 || upper[upper.length - 1] === "" || lower[0] === "";
69
+ return separated ? [...upper, ...lower] : [...upper, "", ...lower];
70
+ }
39
71
  /** True when the nearest `## ` heading above line `at` is a finished section (`## Done` or
40
72
  * `## Fixed`), with no conflict marker in between — an earlier hunk's sides leave the section
41
73
  * unknown, so the answer is then no. */
@@ -52,10 +84,8 @@ function inFinishedSection(lines, at) {
52
84
  /** The finished sections' titles, from FINISHED_ARCHIVES. */
53
85
  const FINISHED_SECTIONS = new Set(FINISHED_ARCHIVES.map((a) => a.section));
54
86
  /** Rewrite every settleable conflict hunk in `text` and return the result, or null when any
55
- * hunk is neither shape insertedLines accepts (or the markers are malformed) — the caller then
56
- * leaves the file for the model resolver. Each hunk is replaced by the lines the change (theirs)
57
- * inserted, a blank separator when neither side already supplies one, then main's (ours)
58
- * lines, so the later merge stays first. */
87
+ * hunk is no shape settledLines accepts (or the markers are malformed) — the caller then
88
+ * leaves the file for the model resolver. Each hunk is replaced by settledLines' lines. */
59
89
  export function resolveInsertOnlyText(text) {
60
90
  const lines = text.split("\n");
61
91
  const hunks = parseHunks(lines);
@@ -63,16 +93,13 @@ export function resolveInsertOnlyText(text) {
63
93
  return null;
64
94
  if (hunks.length === 0)
65
95
  return null;
66
- const inserted = hunks.map((hunk) => insertedLines(hunk, lines));
67
- if (inserted.some((ins) => ins === null))
96
+ const settled = hunks.map((hunk) => settledLines(hunk, lines));
97
+ if (settled.some((replacement) => replacement === null))
68
98
  return null;
69
99
  // Replace from the end so earlier indices stay valid.
70
100
  for (let h = hunks.length - 1; h >= 0; h--) {
71
101
  const hunk = hunks[h];
72
- const theirs = inserted[h];
73
- const separated = hunk.ours.length === 0 || theirs[theirs.length - 1] === "" || hunk.ours[0] === "";
74
- const replacement = separated ? [...theirs, ...hunk.ours] : [...theirs, "", ...hunk.ours];
75
- lines.splice(hunk.start, hunk.end - hunk.start + 1, ...replacement);
102
+ lines.splice(hunk.start, hunk.end - hunk.start + 1, ...settled[h]);
76
103
  }
77
104
  return lines.join("\n");
78
105
  }
@@ -117,6 +144,133 @@ function collectUntilMarker(lines, start, marker, into) {
117
144
  into.push(lines[i++]);
118
145
  return i >= lines.length ? null : i + 1;
119
146
  }
147
+ /** Split `text` into ParsedBacklog blocks, or null when a fence is still open at EOF or a
148
+ * section holds two entries with the same heading line. */
149
+ function parseBacklogBlocks(text) {
150
+ const fenced = fenceTracker();
151
+ const parsed = { preamble: "", sections: [] };
152
+ let current = [];
153
+ let flush = (block) => {
154
+ parsed.preamble = block;
155
+ };
156
+ const close = () => {
157
+ while (current.length > 0 && current[current.length - 1].trim() === "")
158
+ current.pop();
159
+ flush(current.join("\n"));
160
+ current = [];
161
+ };
162
+ for (const line of text.split("\n")) {
163
+ const structural = !fenced.inside(line);
164
+ if (structural && line.startsWith("## ")) {
165
+ close();
166
+ const section = { header: "", entries: [] };
167
+ parsed.sections.push(section);
168
+ flush = (block) => {
169
+ section.header = block;
170
+ };
171
+ }
172
+ else if (structural && line.startsWith("### ") && parsed.sections.length > 0) {
173
+ close();
174
+ const section = parsed.sections[parsed.sections.length - 1];
175
+ flush = (block) => section.entries.push({ key: line, text: block });
176
+ // The entry's heading line is `line` itself; the closure above captured it.
177
+ }
178
+ current.push(line);
179
+ }
180
+ if (fenced.open())
181
+ return null;
182
+ close();
183
+ for (const section of parsed.sections) {
184
+ if (new Set(section.entries.map((e) => e.key)).size !== section.entries.length)
185
+ return null;
186
+ }
187
+ return parsed;
188
+ }
189
+ /** The one text all three versions agree on after at most one side's edit: the changed side
190
+ * when only one changed, else the shared text, or null when both changed differently. */
191
+ function settleBlock(base, ours, theirs) {
192
+ if (ours === theirs || theirs === base)
193
+ return ours;
194
+ return ours === base ? theirs : null;
195
+ }
196
+ /** Merge one section's entries (see mergeBacklogEntries), or null on a same-entry conflict. */
197
+ function mergeSectionEntries(base, ours, theirs) {
198
+ const baseBy = new Map(base.map((e) => [e.key, e.text]));
199
+ const theirsBy = new Map(theirs.map((e) => [e.key, e.text]));
200
+ const oursKeys = new Set(ours.map((e) => e.key));
201
+ const result = [];
202
+ for (const { key, text: o } of ours) {
203
+ const b = baseBy.get(key);
204
+ const t = theirsBy.get(key);
205
+ if (b === undefined) {
206
+ if (t !== undefined && t !== o)
207
+ return null;
208
+ result.push({ key, text: o });
209
+ }
210
+ else if (t === undefined) {
211
+ if (o !== b)
212
+ return null; // main edited what the change deleted
213
+ }
214
+ else {
215
+ const settled = settleBlock(b, o, t);
216
+ if (settled === null)
217
+ return null;
218
+ result.push({ key, text: settled });
219
+ }
220
+ }
221
+ // An entry main deleted stays deleted, unless the change edited it.
222
+ for (const { key, text: b } of base) {
223
+ const t = theirsBy.get(key);
224
+ if (!oursKeys.has(key) && t !== undefined && t !== b)
225
+ return null;
226
+ }
227
+ for (let i = 0; i < theirs.length; i++) {
228
+ const added = theirs[i];
229
+ if (baseBy.has(added.key) || oursKeys.has(added.key))
230
+ continue;
231
+ let at = 0;
232
+ for (let j = i - 1; j >= 0; j--) {
233
+ const found = result.findIndex((e) => e.key === theirs[j].key);
234
+ if (found >= 0) {
235
+ at = found + 1;
236
+ break;
237
+ }
238
+ }
239
+ result.splice(at, 0, added);
240
+ }
241
+ return result;
242
+ }
243
+ /** Merge three versions of a backlog file entry by entry — `ours` is main, `theirs` the change —
244
+ * or null when that cannot be done without judgment: an open fence, a duplicate entry heading,
245
+ * differing `## ` heading sequences, a preamble or section header both sides changed, or the
246
+ * same entry edited on both sides (or edited by one side and deleted by the other). */
247
+ export function mergeBacklogEntries(base, ours, theirs) {
248
+ const [b, o, t] = [base, ours, theirs].map(parseBacklogBlocks);
249
+ if (!b || !o || !t)
250
+ return null;
251
+ const count = b.sections.length;
252
+ if (o.sections.length !== count || t.sections.length !== count)
253
+ return null;
254
+ const blocks = [];
255
+ const preamble = settleBlock(b.preamble, o.preamble, t.preamble);
256
+ if (preamble === null)
257
+ return null;
258
+ if (preamble !== "")
259
+ blocks.push(preamble);
260
+ for (let i = 0; i < count; i++) {
261
+ const [bs, os, ts] = [b.sections[i], o.sections[i], t.sections[i]];
262
+ const heading = (header) => header.split("\n")[0];
263
+ if (heading(bs.header) !== heading(os.header) || heading(bs.header) !== heading(ts.header)) {
264
+ return null;
265
+ }
266
+ const header = settleBlock(bs.header, os.header, ts.header);
267
+ const entries = mergeSectionEntries(bs.entries, os.entries, ts.entries);
268
+ if (header === null || entries === null)
269
+ return null;
270
+ blocks.push(header, ...entries.map((e) => e.text));
271
+ }
272
+ return `${blocks.join("\n\n")}\n`;
273
+ }
120
274
  /** Settle the insert-only conflicts among `files` in `wt` (plans/parallel-work-instances.md,
121
275
  * part 3/7): every conflicted repo-root backlog file (BACKLOG_FILES: PLANS.md, BUGS.md,
122
276
  * QUESTIONS.md and the two archive files) whose hunks all settle is rewritten without markers and
@@ -131,15 +285,29 @@ export async function resolveBacklogInsertConflicts(wt, files) {
131
285
  continue;
132
286
  }
133
287
  const p = path.join(wt, file);
134
- let text;
288
+ // lstat, not stat: a symlink among the conflicted files is never followed out of the
289
+ // worktree (INV-G3 — a symlink holds no text of its own and its target could point out
290
+ // of it, so what a change that adds one put there is the link, not the target). Leaving
291
+ // it to the pi resolver keeps the same policy the conflict-marker and secret-leak gates
292
+ // pin: the resolver, like those gates, must not read an arbitrary host file.
293
+ let st;
135
294
  try {
136
- text = fs.readFileSync(p, "utf8");
295
+ st = fs.lstatSync(p);
137
296
  }
138
297
  catch {
139
298
  remaining.push(file); // deleted side or unreadable path — leave it to the resolver
140
299
  continue;
141
300
  }
142
- const resolved = resolveInsertOnlyText(text);
301
+ if (!st.isFile()) {
302
+ remaining.push(file); // symlink, directory or gitlink — leave it to the resolver
303
+ continue;
304
+ }
305
+ const text = readTextOrNull(p);
306
+ if (text === null) {
307
+ remaining.push(file); // deleted side or unreadable path — leave it to the resolver
308
+ continue;
309
+ }
310
+ const resolved = resolveInsertOnlyText(text) ?? (await mergeStagedEntries(wt, file));
143
311
  if (resolved === null) {
144
312
  remaining.push(file);
145
313
  continue;
@@ -153,3 +321,15 @@ export async function resolveBacklogInsertConflicts(wt, files) {
153
321
  }
154
322
  return remaining;
155
323
  }
324
+ /** The entry-by-entry merge of `file`'s three index stages (1 base, 2 main, 3 the change), or
325
+ * null for an archive file, a stage missing on one side, or a merge needing judgment. */
326
+ async function mergeStagedEntries(wt, file) {
327
+ if (FINISHED_ARCHIVES.some((a) => a.archive === file))
328
+ return null;
329
+ const base = await gitTry(wt, "show", `:1:${file}`);
330
+ const ours = await gitTry(wt, "show", `:2:${file}`);
331
+ const theirs = await gitTry(wt, "show", `:3:${file}`);
332
+ if (base === null || ours === null || theirs === null)
333
+ return null;
334
+ return mergeBacklogEntries(base, ours, theirs);
335
+ }
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /** Two of the three halves of a merge (plans/merge-queue.md 5/5, PLANS.md land-queue speed
3
2
  * 2c): the per-change verify (verifyRequest) that merge-verify.ts's verify stage runs for every
4
3
  * queued change, and the merge (mergeApproved) its one merge slot runs over the approved ones — a
@@ -31,7 +30,7 @@ export async function verifyRequest(ctx, req, w) {
31
30
  purpose: "verify",
32
31
  ref: req.sha,
33
32
  signal: ctx.signal(),
34
- // The live view's pool budget, same rule as the tick path: a broken tumwater.json
33
+ // The live view's pool budget, same rule as the run path: a broken tumwater.json
35
34
  // must not error the verify before its reviewer runs.
36
35
  slotBudget: slotCount(ctx.config),
37
36
  });
@@ -108,8 +107,10 @@ async function verifyRequestIn(ctx, req, w, wt) {
108
107
  * Un-assemblable (a cherry-pick conflict, on the first stack or any prefix) → ABANDON the changes
109
108
  * not yet merged to one-at-a-time, in queue order, stopping at the first non-terminal outcome, each
110
109
  * through mergeApprovedChange — no second gate and no model run but mergeToMain's conflict
111
- * resolver. main is never left red: the only bytes this path ff's are a checked tip or per-change
112
- * merges re-verified in-lock whenever they differ from what their verify judged.
110
+ * resolver. main is not left red unchecked: the only bytes this path ff's are a checked tip or
111
+ * per-change merges re-verified in-lock whenever they differ from what their verify judged (the
112
+ * in-lock check is `check.mergeCommand` when set, and the main watch then runs the full check
113
+ * after the merge).
113
114
  *
114
115
  * An abort is observed between steps, not only by the pi runs it kills: before each
115
116
  * assembly-and-check attempt and before each one-change or fallback merge, so a stopping merge
@@ -136,6 +137,9 @@ export async function mergeApproved(ctx, approved, wiringFor) {
136
137
  runGatePi: (opts) => w.runGatePi(opts),
137
138
  foldUsage: w.foldUsage,
138
139
  signal: ctx.signal,
140
+ reviewHeld: ctx.reviewHeld,
141
+ reviewRetries: ctx.reviewRetries,
142
+ reviewRetryDelayMs: ctx.reviewRetryDelayMs,
139
143
  });
140
144
  let aborted = false;
141
145
  // Every finalized result goes through here: record it, then hand it to the caller's
@@ -230,7 +234,8 @@ export async function mergeApproved(ctx, approved, wiringFor) {
230
234
  break;
231
235
  }
232
236
  else if (outcome.kind === "blocked") {
233
- // The race was lost on every attempt — main is moving faster than a check completes.
237
+ // The race was lost on every attempt (main is moving faster than a check completes), or
238
+ // the primary checkout holds files the stack changes (found before the check).
234
239
  // Every change not yet merged keeps its ref with "merge_blocked": leftover recovery
235
240
  // re-merges each through its own gate + tryMerge, whose in-lock check cannot lose it.
236
241
  for (let s = front; s < entries.length; s++)
@@ -247,10 +252,11 @@ export async function mergeApproved(ctx, approved, wiringFor) {
247
252
  // outcome (the rest keep entry + ref for the next merge). Each request merges its approved
248
253
  // head (the synced pin its verify judged, not the bare pin) — no second gate, so no model run:
249
254
  // re-gating would rebase onto the main the earlier entries just moved, and the rewritten
250
- // sha misses the approved short-circuit. main is never left red — mergeToMain's in-lock
255
+ // sha misses the approved short-circuit. main is not left red unchecked — mergeToMain's in-lock
251
256
  // 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.
257
+ // judged (with check.mergeCommand set, by that cheaper command; the main watch runs the full
258
+ // check after the merge). Only the change being merged reads `merging`; the rest are back to
259
+ // awaiting their turn, and each one merged is done.
254
260
  for (let s = mergeDoneCount; s < approved.length; s++)
255
261
  report(s, "approved");
256
262
  for (let s = mergeDoneCount; s < approved.length; s++) {
@@ -0,0 +1,53 @@
1
+ /** Operator cancellation of a waiting merge (PLANS.md `tumwater queue --cancel <n>`), split
2
+ * out of merge-queue.ts so the queue stays the pure reader/writer it is: this module owns the
3
+ * destructive half of an unattempted entry — removing the queue file and deleting the role's
4
+ * merge pin so next-run leftover recovery cannot silently re-enqueue the change. It mirrors
5
+ * merge-requeue.ts (the other settled-entry editor living outside merge-queue.ts) and takes no
6
+ * lock: the queue is the inbox idiom (one file per entry, dropMerge tolerant of a concurrent
7
+ * drop) and the pin delete is value-guarded. */
8
+ import { queuedMergeFiles, dropMerge } from "./merge-queue.js";
9
+ import { mergeRefName } from "../paths.js";
10
+ import { deleteRefIfAt } from "../git/git.js";
11
+ import { logEventBestEffort } from "../events/events.js";
12
+ /** Remove one readable queue entry: delete that role's merge pin first (deleteRefIfAt, a no-op
13
+ * when the ref is gone or names a different sha, so a newer pin is never removed), then drop
14
+ * the queue file (ENOENT already a no-op), logging one `merge_cancelled` event under the role,
15
+ * and return the `cancelled` outcome naming what was removed. The single removal both
16
+ * `cancelQueuedMerge` (by position) and `cancelQueuedMergeBySha` (by sha) share, so the CLI and
17
+ * the dashboard's queue-cancel cannot drift on what cancelling means. */
18
+ async function removeQueuedEntry(root, target) {
19
+ await deleteRefIfAt(root, mergeRefName(target.entry.role), target.entry.sha);
20
+ dropMerge(target.file);
21
+ logEventBestEffort(root, {
22
+ source: target.entry.role,
23
+ type: "merge_cancelled",
24
+ commit: target.entry.sha,
25
+ summary: target.entry.summary,
26
+ });
27
+ return { status: "cancelled", role: target.entry.role, sha: target.entry.sha, summary: target.entry.summary };
28
+ }
29
+ /** Remove the Nth queued merge — 1-based, as `tumwater queue` numbers it — and delete that
30
+ * entry's role's merge pin, so next-run leftover recovery finds no pin and no branch-ahead
31
+ * commit and does not re-enqueue the change. `position` is resolved against
32
+ * `queuedMergeFiles` (the same readable, filename-ordered list the queue renderer shows), so
33
+ * the position matches the line `tumwater queue` printed. The pin is deleted first with
34
+ * deleteRefIfAt (a no-op when the ref is gone or names a different sha, so a newer pin is
35
+ * never removed), then the queue file is dropped (ENOENT already a no-op). Logs one
36
+ * `merge_cancelled` event under the role. Out-of-range changes nothing and returns the
37
+ * current readable depth. */
38
+ export async function cancelQueuedMerge(root, position) {
39
+ const files = queuedMergeFiles(root);
40
+ if (position < 1 || position > files.length)
41
+ return { status: "out-of-range", depth: files.length };
42
+ return removeQueuedEntry(root, files[position - 1]);
43
+ }
44
+ /** Remove the queued merge whose entry's pinned sha is `sha` (the browser twin of
45
+ * `tumwater queue --cancel <n>`, resolving by sha because the dashboard rows carry one). An
46
+ * entry whose sha matches none is `{ status: "gone" }` and changes nothing; otherwise it runs
47
+ * the same `removeQueuedEntry` the position variant uses. */
48
+ export async function cancelQueuedMergeBySha(root, sha) {
49
+ const target = queuedMergeFiles(root).find((q) => q.entry.sha.toLowerCase() === sha.toLowerCase());
50
+ if (!target)
51
+ return { status: "gone" };
52
+ return removeQueuedEntry(root, target);
53
+ }
@@ -1,16 +1,16 @@
1
- // vocabulary: plain
2
1
  import { deleteRef, headOf, patchId } from "../git/git.js";
3
2
  import { mergeRefName } from "../paths.js";
4
- import { recordReview } from "../tick/tick-apply.js";
5
- import { saveLoopState } from "../loop/loop-state.js";
3
+ import { recordReview } from "../run/run-apply.js";
4
+ import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
6
5
  import { unverifiedTreeOutcome } from "../build/build-check-events.js";
7
6
  import { checkFailureReasons, describeCheck, failureHeadline } from "../build/build-check-report.js";
7
+ import { onlyKnownMainFailures } from "../baseline/main-watch.js";
8
8
  import { mainTipVerdict } from "../baseline/main-red.js";
9
9
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
10
10
  import { mainRedNotMine } from "../text/phrases.js";
11
11
  import { normalizeClusterKey } from "../failure/failure-cluster.js";
12
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
13
+ /** Consecutive red in-lock merge checks of one patch (RunnerState.mergeCheckFailures) before
14
14
  * the merge is attributed instead of retried: the first red keeps the pin for one more
15
15
  * attempt — a load flake gets its retry, as the gate's pre-check gets one (PLANS.md land-queue
16
16
  * 1/3) — and the second is judged by main's own verdict at its tip (attributeRedCheck). Without
@@ -28,7 +28,7 @@ function pinKept(patch, count) {
28
28
  return patch === null || count < MERGE_CHECK_FAILURE_LIMIT;
29
29
  }
30
30
  /** Tally one deterministic failure of the same patch toward attribution: read the worktree's
31
- * head and its patch-id against main, carry LoopState.mergeCheckFailures's count across
31
+ * head and its patch-id against main, carry RunnerState.mergeCheckFailures's count across
32
32
  * re-merges of the identical patch (a clean rebase onto a moved main keeps the patch-id, so the
33
33
  * retries of one change add up while a different change starts fresh), and store the strike.
34
34
  * An unreadable patch-id cannot be matched, so pinKept never counts it toward the limit.
@@ -60,8 +60,9 @@ async function keepPinOrAttribute(ctx, wt, reason, attribute) {
60
60
  return attribute(head);
61
61
  }
62
62
  /** The terminal deterministic-reject sink shared by mergeBlocked and attributeRedCheck:
63
- * record the reject review, reset the unreviewed-failure streak, persist state, delete the
64
- * merge ref, and log review_rejected. The shared `_merge` checkout is left in place
63
+ * record the reject review, reset the unreviewed-failure streak, persist state best-effort
64
+ * (a failed save warns; the merge write-back saves the same state later), delete the merge
65
+ * ref, and log review_rejected, even on a full disk. The shared `_merge` checkout is left in place
65
66
  * (plans/worktree-pool.md, part 2c): it persists across merges like `_gate-main`, and a
66
67
  * rejection discards the pinned ref, not the checkout. Returns "rejected". review.ts's reject
67
68
  * path is a different sink — it resets the branch instead of deleting the ref — and keeps its
@@ -69,7 +70,7 @@ async function keepPinOrAttribute(ctx, wt, reason, attribute) {
69
70
  async function rejectChange(ctx, role, head, reasons, state) {
70
71
  recordReview(state, "reject", reasons, head);
71
72
  state.unreviewFailures = 0;
72
- saveLoopState(ctx.root, state);
73
+ saveRunnerStateBestEffort(ctx.root, state);
73
74
  await deleteRef(ctx.root, mergeRefName(role));
74
75
  logEventBestEffort(ctx.root, { source: role, type: "review_rejected", head, reasons });
75
76
  return "rejected";
@@ -82,6 +83,18 @@ async function rejectChange(ctx, role, head, reasons, state) {
82
83
  function firstCheckReason(red) {
83
84
  return checkFailureReasons(red.check, red.outcome)[0];
84
85
  }
86
+ /** Keep the pin when a red check fails only main's known failures (onlyKnownMainFailures): not
87
+ * the change's failure and not a strike, so recovery's re-merge — whose verifyMerge lets such a
88
+ * check through — lands it. Null when the red names a test outside the known set. */
89
+ function keepForKnownFailures(ctx, role, label, red, state) {
90
+ const known = onlyKnownMainFailures(ctx.root, red.outcome.failingTests);
91
+ if (!known)
92
+ return null;
93
+ state.lastError = `${label} failed only main's known failures, merge kept: ${known.join(", ")}`;
94
+ warnEventBestEffort(ctx.root, role, `${label} fails only main's known failures: ${known.join(", ")}`);
95
+ saveRunnerStateBestEffort(ctx.root, state);
96
+ return "merge_blocked";
97
+ }
85
98
  /** A merge blocked because its in-lock check went red on the rebased tree (merge-to-main.ts's
86
99
  * verifyMerge). The count is keyed by the patch-id; under MERGE_CHECK_FAILURE_LIMIT the
87
100
  * pin is kept (merge_blocked) for recovery's re-merge; at the limit the red is attributed like
@@ -95,6 +108,9 @@ export async function mergeCheckRed(ctx, role, wt, red) {
95
108
  ctx.state.lastError = `merge failed: ${firstCheckReason(red)}`;
96
109
  return "merge_blocked";
97
110
  }
111
+ const kept = keepForKnownFailures(ctx, role, "merge check", red, ctx.state);
112
+ if (kept)
113
+ return kept;
98
114
  const reason = firstCheckReason(red);
99
115
  let attributed = false;
100
116
  const result = await keepPinOrAttribute(ctx, wt, reason, (head) => {
@@ -103,7 +119,7 @@ export async function mergeCheckRed(ctx, role, wt, red) {
103
119
  });
104
120
  // The first red of a patch's in-lock check is exactly the retry that exists for load flakes,
105
121
  // 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
122
+ // only place this reason is written, is overwritten by the leftover-recovery run a second
107
123
  // later (BUGS.md 2026-10-14). Log it as a warning so the failure digest and a later audit can
108
124
  // name the flaky test; the second red at MERGE_CHECK_FAILURE_LIMIT is attributed through
109
125
  // review_rejected, whose reasons are already durable.
@@ -117,7 +133,7 @@ export async function mergeCheckRed(ctx, role, wt, red) {
117
133
  }
118
134
  /** Record a merge check that failed and then passed on its kept-pin retry as a flake, in the
119
135
  * 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
136
+ * doctor's `flaky checks` and the run prompt's flake note count it (PLANS.md 2026-10-16). Reads
121
137
  * the headline mergeCheckRed stored on the strike record only when its patch-id is the merged
122
138
  * pin's (a stale strike of a discarded pin must not be logged); a pin that was never red (or
123
139
  * only cross-check blocked) has none and logs nothing. Call before the strike is cleared. */
@@ -134,7 +150,7 @@ export function noteMergeRetryPassed(ctx, role, pinPatch) {
134
150
  * under the rebase, and a differently-rebased tree can pass the cross-check — but at the
135
151
  * limit the block is attributed to the change: rejected deterministically with the block
136
152
  * reason, the pin deleted, no model run. Without this a resolution that trips the same
137
- * cross-check on every re-resolve pays a fresh conflict-resolver run each tick, forever
153
+ * cross-check on every re-resolve pays a fresh conflict-resolver run each run, forever
138
154
  * (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). */
139
155
  export async function mergeBlocked(ctx, role, wt, blocked) {
140
156
  return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`merge blocked: ${blocked}`], ctx.state));
@@ -144,15 +160,18 @@ export async function mergeBlocked(ctx, role, wt, blocked) {
144
160
  * MERGE_CHECK_FAILURE_LIMIT — by the gate's rule: ask main's own
145
161
  * verdict at its current tip (mainTipVerdict — usually a cache hit, since every merge seeds
146
162
  * the SHA it moved main to). Main green → the change broke the check: rejected deterministically
147
- * — reasons in lastReview for the author's next tick, the strike count reset, ref deleted,
163
+ * — reasons in lastReview for the author's next run, the strike count reset, ref deleted,
148
164
  * review_rejected logged, no pi run. Main red → not this change's failure: "main_red" with the
149
- * ref kept and unreviewFailures untouched, so recovery re-merges it once main-red.ts's repair
150
- * turns main green. No verdict → reject, the safe default, and the reasons say so. The verdict
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. */
165
+ * ref kept and unreviewFailures untouched, so the orchestrator poll (requeueMainRedPin) re-queues
166
+ * it once main's head moves to one the watch has not found red, and leftover recovery holds it
167
+ * until then. No verdict → reject, the
168
+ * safe default, and the reasons say so. The verdict
169
+ * is persisted best-effort before it returns. A red whose headline matches a flake the gate or
170
+ * a merge retry recorded (isKnownFlake) is excused once per headline per pin
171
+ * (RunnerState.mergeFlakeExcused): "merge_blocked" with the ref kept, before main's verdict,
172
+ * because that verdict is usually a cached green that would reject a change over a flaky test
173
+ * (PLANS.md 2026-10-16, BUGS.md 2026-10-06). A repeat of the same headline is attributed as
174
+ * above. */
156
175
  export async function attributeRedCheck(ctx, role, head, label, red, state) {
157
176
  // An unverified red — a run that spanned a host sleep, the tree never judged — is not the
158
177
  // change's failure and not a strike: keep the ref for recovery's re-merge and name the sleep
@@ -160,22 +179,25 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
160
179
  // only live when the check produced a verdict.
161
180
  if (unverifiedTreeOutcome(red.outcome)) {
162
181
  state.lastError = `${label}: ${firstCheckReason(red)}`;
163
- saveLoopState(ctx.root, state);
182
+ saveRunnerStateBestEffort(ctx.root, state);
164
183
  return "merge_blocked";
165
184
  }
185
+ const kept = keepForKnownFailures(ctx, role, label, red, state);
186
+ if (kept)
187
+ return kept;
166
188
  const headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
167
189
  const key = normalizeClusterKey(headline);
168
190
  if (isKnownFlake(ctx.root, headline) && state.mergeFlakeExcused !== key) {
169
191
  state.mergeFlakeExcused = key;
170
192
  state.lastError = `${label} failed: known-flaky failure, merge kept: ${headline}`;
171
193
  warnEventBestEffort(ctx.root, role, `${label}: known-flaky failure, merge kept: ${headline}`);
172
- saveLoopState(ctx.root, state);
194
+ saveRunnerStateBestEffort(ctx.root, state);
173
195
  return "merge_blocked";
174
196
  }
175
197
  const main = await mainTipVerdict(ctx.root, role, ctx.mainBranch, ctx.config);
176
198
  if (main.status === "red") {
177
199
  state.lastError = `${label} failed: ${mainRedNotMine(main.sha)}`;
178
- saveLoopState(ctx.root, state);
200
+ saveRunnerStateBestEffort(ctx.root, state);
179
201
  return "main_red";
180
202
  }
181
203
  const reasons = checkFailureReasons(red.check, red.outcome);