tumwater 0.2.0 → 0.3.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 (446) 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 +42 -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 +4 -3
  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 +402 -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 +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -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 +1 -2
  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 +118 -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 +150 -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 +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,18 +1,10 @@
1
- import { REVIEW_RESUME_LIMIT, saveLoopState } from "../loop/loop-state.js";
2
- import { modelSelectorField, resolvedAgentConfig, reviewRunConfig } from "../config/config-views.js";
3
1
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
- import { git } from "../git/git-run.js";
5
- import { changeBaseRev, headOf, patchId } from "../git/git.js";
6
- import { aheadOfMainDiff, aheadOfMainFiles, revisionInterdiff } from "../git/git-diff.js";
2
+ import { headOf, patchId } from "../git/git.js";
3
+ import { aheadOfMainFiles } from "../git/git-diff.js";
7
4
  import { resetWorktreeToMain } from "../git/worktree.js";
8
- import { nativeLogPath, piLogPath } from "../paths.js";
9
- import { readPrinciples } from "../prompt/principles.js";
10
- import { buildReviewPrompt, buildReviewResumePrompt } from "../prompt/gate-prompts.js";
11
- import { agentSessionResumable } from "../agent/agent-backend.js";
12
- import { requestNoRerun, requestVerdict } from "./review-followup.js";
13
- import { parseVerdict } from "./review-verdict.js";
14
- import { recordReview } from "../tick/tick-apply.js";
15
- import { shortSha } from "../text/format.js";
5
+ import { recordReview } from "../run/run-apply.js";
6
+ import { baseRoleOf } from "../roles/runner-ids.js";
7
+ import { errorMessage } from "../text/text.js";
16
8
  import { firstReason } from "../text/phrases.js";
17
9
  import { isExemptDiff } from "./exemptions.js";
18
10
  import { falseFixReason } from "../verdict/fix-claim.js";
@@ -20,52 +12,72 @@ import { conflictMarkerReason } from "../merge/merge-git.js";
20
12
  import { protectedEditReason } from "./protected-edits.js";
21
13
  import { secretLeakReason } from "./secret-leaks.js";
22
14
  import { backlogStructureReason } from "../backlog/backlog-structure.js";
23
- import { suiteRerunWarning } from "./suite-rerun.js";
15
+ import { idleWithoutProviderLease } from "../concurrency/provider-lease.js";
24
16
  import { setMergeStage } from "../merge/merge-slot.js";
25
17
  import { gateBuildPrecheck } from "./review-precheck.js";
26
- /** Attach the review gate's pi runs onto a partial GateResult — `run` plus the optional
27
- * verdict-recovery follow-up (`followUpRun`) and no-re-run nudge (`nudgeRun`) fields, each
28
- * present only when its turn actually ran. Spelled once so every return site of
29
- * reviewAheadOfMain that carries runs shares one shape for the usage-folding caller
30
- * (merge-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
31
- * spread per site that a new run kind would have to update everywhere. */
32
- function withRuns(r, run, followUp, nudge) {
33
- return {
34
- ...r,
35
- run,
36
- ...(followUp ? { followUpRun: followUp } : {}),
37
- ...(nudge ? { nudgeRun: nudge } : {}),
38
- };
39
- }
40
- /** Put the worktree back on the exact head the gate reviewed: the reviewer's only output is its
41
- * verdict, so a commit it made must never reach the merge pin or the next re-review. */
42
- async function restoreReviewedHead(wt, head, root, role) {
43
- const now = await headOf(wt, "HEAD").catch(() => null);
44
- if (now && now !== head) {
45
- warnEventBestEffort(root, role, `the reviewer moved HEAD to ${shortSha(now)} during review; restored the reviewed head ${shortSha(head)}`);
18
+ export { REVIEW_FAILURE_LIMIT } from "./review-run.js";
19
+ import { restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
20
+ /** In-place retries of a failed reviewer run within one verify, after the first run. */
21
+ const REVIEW_RETRIES = 2;
22
+ /** The pause before each retry (ReviewContext.retryDelayMs overrides it in tests). */
23
+ const REVIEW_RETRY_DELAY_MS = 30_000;
24
+ /** Overrides the pause for tests that drive a whole runner or orchestrator, where no context
25
+ * reaches the review. */
26
+ const REVIEW_RETRY_DELAY_ENV = "TUMWATER_REVIEW_RETRY_DELAY_MS";
27
+ /** A second pool slot for the gate check, or null when the context cannot lease one, none is free
28
+ * right now, or the lease failed (the check then runs first, in the change's own slot). */
29
+ async function leaseCheckSlot(ctx, head) {
30
+ if (!ctx.leaseCheckSlot)
31
+ return null;
32
+ try {
33
+ return await ctx.leaseCheckSlot(head);
34
+ }
35
+ catch (err) {
36
+ warnEventBestEffort(ctx.root, ctx.role, `no second workspace for the gate check (${errorMessage(err)}); running it before the review`);
37
+ return null;
46
38
  }
47
- await git(wt, "reset", "--hard", head);
48
39
  }
49
- /** Consecutive failed reviews of one branch HEAD after which the leftover is discarded with
50
- * a warning: a misconfigured reviewer model must not be able to wedge a loop into re-reviewing
51
- * the same commit forever. */
52
- export const REVIEW_FAILURE_LIMIT = 3;
40
+ /** Start the gate check in `lease`'s checkout of `head`. A rejection is recorded, not applied:
41
+ * resetting the change's branch is left to reviewAttempt, after the reviewer has stopped. */
42
+ function startOverlappedCheck(ctx, state, head, lease) {
43
+ const overlap = { abort: new AbortController(), done: false };
44
+ const reject = async (reasons) => {
45
+ overlap.rejectedReasons = reasons;
46
+ return { decision: "rejected", detail: firstReason(reasons) };
47
+ };
48
+ overlap.gate = gateBuildPrecheck({ ...ctx, wt: lease.dir }, state, head, reject)
49
+ .then((pre) => {
50
+ overlap.done = true;
51
+ if (pre.resolved)
52
+ overlap.abort.abort();
53
+ else
54
+ setMergeStage(ctx.root, ctx.role, "reviewing");
55
+ return pre;
56
+ }, (err) => {
57
+ overlap.done = true;
58
+ overlap.abort.abort();
59
+ throw err;
60
+ })
61
+ .finally(() => lease.release());
62
+ overlap.gate.catch(() => { }); // surfaced where reviewAttempt awaits it
63
+ return overlap;
64
+ }
53
65
  /** Run the adversarial review gate over everything ahead of main in `wt` and update `state`
54
- * accordingly. Shared by the tick path (after commitAll) and leftover recovery
66
+ * accordingly. Shared by the run path (after commitAll) and leftover recovery
55
67
  * (leftover.ts's recoverLeftover) — every path
56
68
  * that can move a commit into main routes through here, so no crash or abort path smuggles
57
69
  * unreviewed work in:
58
70
  * - gate disabled, or an already-approved HEAD, or an exempt (doc-only) diff → merge as-is;
59
- * - a new HEAD carrying the last approved patch (lastApprovedPatchId) → approved after the
71
+ * - a new HEAD carrying an approved patch (approvedPatchIds) → approved after the
60
72
  * build pre-check, with no reviewer run;
61
73
  * - approve → record lastApprovedHead and its patch-id, and discard the reviewer's stray
62
74
  * working-tree edits (its only output channel is the verdict);
63
75
  * - reject → reset the branch to main, record reasons in state.lastReview (injected into the
64
- * role's next tick prompt), log review_rejected;
76
+ * role's next run prompt), log review_rejected;
65
77
  * - fail with an unparseable reply (the reviewer ran to completion but emitted no VERDICT) →
66
78
  * one capped follow-up turn on the reviewer's own session asks for the missing line
67
79
  * (mirroring requestSummary — BUGS.md 2026-09-29); only a follow-up that also yields
68
- * nothing counts the strike. Leave the commit on the branch for the next tick's re-review,
80
+ * nothing counts the strike. Leave the commit on the branch for the next run's re-review,
69
81
  * counting consecutive failures per HEAD; past REVIEW_FAILURE_LIMIT discard the leftover
70
82
  * with a warning.
71
83
  * - fail because the run itself failed (transport/spawn/timeout, pi.ok false) → leave the
@@ -77,7 +89,7 @@ export const REVIEW_FAILURE_LIMIT = 3;
77
89
  * `highFriction` marks a change whose authoring run burned more than the configured turn/time
78
90
  * thresholds (plans/refusal-and-thrash.md): the flag rides along in the review prompt so the
79
91
  * reviewer applies extra scrutiny to whether the work should exist at all. */
80
- export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFriction) {
92
+ async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retry) {
81
93
  const { root, role, wt, mainBranch, config } = ctx;
82
94
  if (!config.review.enabled) {
83
95
  state.reviewResume = undefined;
@@ -86,7 +98,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
86
98
  const head = await headOf(wt, "HEAD");
87
99
  // The one reject path: every rejection of this HEAD — deterministic build-check failure or
88
100
  // model verdict — shares this bookkeeping. Record lastReview (injected into the role's next
89
- // tick prompt), reset the failure count (a verdict about this HEAD is a successful review
101
+ // run prompt), reset the failure count (a verdict about this HEAD is a successful review
90
102
  // either way), discard the branch by resetting it to main, and log review_rejected.
91
103
  // Callers attach `run` when a pi run was consumed.
92
104
  const reject = async (reasons, durationMs) => {
@@ -103,6 +115,22 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
103
115
  });
104
116
  return { decision: "rejected", detail: firstReason(reasons) };
105
117
  };
118
+ // Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
119
+ const keep = async (r) => {
120
+ await restoreReviewedHead(wt, head, root, role);
121
+ return r;
122
+ };
123
+ // Settle the attempt on an overlapped check that did not pass: the sequential pre-check's own
124
+ // outcome, with the reviewer's run (when one started) dropped and no verdict recorded. A
125
+ // rejection is applied now, after the reviewer stopped touching the tree; every other outcome
126
+ // keeps the change at the head the gate reviewed.
127
+ const settleFailedCheck = async (o, pre, pi) => {
128
+ state.reviewResume = undefined;
129
+ const settled = o.rejectedReasons
130
+ ? await reject(o.rejectedReasons)
131
+ : await keep(pre.resolved ?? { decision: "failed", detail: "the gate check did not complete" });
132
+ return pi ? withRuns(settled, pi, null, null) : settled;
133
+ };
106
134
  // Already reviewed this exact HEAD (e.g. a merge_blocked retry): do not burn another run.
107
135
  if (state.lastApprovedHead === head) {
108
136
  state.reviewResume = undefined;
@@ -130,7 +158,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
130
158
  // Fixed must have its fix's symbols on this tree, or it records code that does not
131
159
  // exist (BUGS.md 2026-09-22 — 9cea8c3 merged a Fix paragraph naming runScriptGroup
132
160
  // and signalTree with no source behind it). A deterministic rejection, like the
133
- // pre-check below: no pi run consumed, reasons injected into the author's next tick.
161
+ // pre-check below: no pi run consumed, reasons injected into the author's next run.
134
162
  const falseFix = await falseFixReason(wt, mainBranch, files);
135
163
  if (falseFix)
136
164
  return reject([falseFix]);
@@ -154,336 +182,103 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
154
182
  // Deterministic build pre-check — after BOTH early returns above (an md-only diff cannot
155
183
  // break the build) and before any reviewer run or the phase/event that would show
156
184
  // "reviewing": a deterministic rejection never shows as reviewing on the dashboards. Both
157
- // gate callers (the tick path and leftover.ts's recoverLeftover) get it for free. Its run,
185
+ // gate callers (the run path and leftover.ts's recoverLeftover) get it for free. Its run,
158
186
  // flake re-run, and attribution live in review-precheck.ts (the check run itself lives in
159
187
  // runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved result
160
188
  // here is returned verbatim and a pass hands the verified head to the merge path below.
161
189
  // The pre-check sets the merge cell's stage (a no-op outside a queued merge) —
162
190
  // "build-check" while it runs, "reviewing" when the reviewer run starts below.
163
- const pre = await gateBuildPrecheck(ctx, state, head, reject);
164
- if (pre.resolved) {
165
- state.reviewResume = undefined;
166
- return pre.resolved;
167
- }
168
- const { verifiedHead, verifiedByHarness } = pre;
191
+ // A retried attempt keeps the first attempt's passing pre-check (a green, or a red naming
192
+ // only main's known failures): the tree is the same head.
169
193
  // A review judges a diff, not a sha: a head carrying exactly the patch this role's last
170
194
  // approval judged (the approved head cleanly rebased onto a moved main) reuses that verdict
171
195
  // instead of paying a second reviewer run. Only the model review is reused — the pre-check
172
- // above has just run on this tree (a failure rejected it there), and its verifiedHead rides
173
- // on the result as for any approval.
196
+ // has just run on this tree (a failure rejected it there), and its verifiedHead rides on the
197
+ // result as for any approval.
174
198
  const reviewedPatchId = (await patchId(wt, mainBranch, head)) ?? undefined;
175
- if (state.lastApprovedPatchId !== undefined) {
176
- if (reviewedPatchId === state.lastApprovedPatchId) {
177
- state.reviewResume = undefined;
178
- state.lastApprovedHead = head;
179
- return { decision: "approved", verifiedHead };
180
- }
181
- }
182
- // The reviewer's model selector (plans/model-tiers.md "Observability"): resolved from the
183
- // same config the run below gets, so the event names what the reviewer actually runs on —
184
- // the budget fallback included. Omitted when no model is configured (pi's own default).
185
- const reviewCfg = reviewRunConfig(config);
186
- // The team claude config dir (plans/notes-command.md "Keeping funes out of team runs"), so
187
- // a claude review's resume guard looks under the same dir the review run writes its transcript
188
- // to — a team that set one must resume under it, not the default ~/.claude.
189
- const claudeConfigDir = resolvedAgentConfig(config, "claude").configDir;
190
- // A progressing timeout left this run's session for the next attempt (LoopState.reviewResume):
191
- // resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
192
- // the directory still holds a gate session to `--continue`. Otherwise the stale marker is
193
- // cleared and this attempt reviews the change from scratch.
194
- const priorResume = state.reviewResume;
195
- const resuming = priorResume !== undefined &&
196
- priorResume.patchId === reviewedPatchId &&
197
- priorResume.attempts < REVIEW_RESUME_LIMIT &&
198
- agentSessionResumable(priorResume.sessionDir, {
199
- kind: "gate",
200
- agent: reviewCfg.agent ?? "pi",
201
- }, claudeConfigDir);
202
- if (resuming) {
203
- // Count the resume before the run: a crash must not let the same session be resumed
204
- // forever without ever spending the budget.
205
- priorResume.attempts += 1;
206
- saveLoopState(root, state);
207
- }
208
- else {
209
- state.reviewResume = undefined;
210
- }
211
- // The directory this run actually uses: a resumed attempt continues the timed-out run's
212
- // session, a fresh one its own. Both the run and its follow-up turns must target this dir —
213
- // a follow-up that used ctx.sessionDir while the review resumed another dir would
214
- // `--continue` a session that never held this review.
215
- const reviewSessionDir = resuming ? state.reviewResume.sessionDir : ctx.sessionDir;
216
- // The follow-up turns (verdict recovery, no-re-run nudge) continue the just-finished review,
217
- // so they must read the same directory. A fresh run's dir is already ctx's, so only a resume
218
- // needs the override.
219
- const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
220
- logEventBestEffort(root, {
221
- source: role,
222
- type: "review_start",
223
- head,
224
- ...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
225
- ...(resuming ? { resumed: true } : {}),
226
- ...modelSelectorField(reviewCfg),
227
- });
228
- // Persist the phase BEFORE the run so a dashboard mid-review shows "reviewing" and a crash
229
- // mid-review is distinguishable from a crash mid-author-run on resume (stray edits are the
230
- // reviewer's then, not the author's). Cleared at tick end alongside `running`.
231
- state.phase = "review";
232
- saveLoopState(root, state);
233
- // Freeze the revision the diff is measured from once, before the run: main moves while a
234
- // merge is approved, and a reviewer comparing against the live `main` ref reads another loop's
235
- // just-merged work as this change reverting it (BUGS.md 2026-10-06). The prompt names this SHA
236
- // and tells the reviewer to compare against it instead.
237
- const base = await changeBaseRev(wt, mainBranch);
238
- const diff = await aheadOfMainDiff(wt, mainBranch);
239
- // A revision's re-review gets the prior review's objections and the interdiff between the two
240
- // versions (plans/revise-rejected.md part 2/2), so it checks what the revision had to resolve
241
- // instead of starting from no memory. A missing prior object yields an empty interdiff, never
242
- // a throw (revisionInterdiff).
243
- const priorReview = ctx.priorReview
244
- ? {
245
- round: ctx.revisionRound ?? 1,
246
- reasons: ctx.priorReview.reasons,
247
- interdiff: await revisionInterdiff(wt, mainBranch, ctx.priorReview.sha, head),
248
- }
249
- : undefined;
250
- // The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
251
- // merge on local hardware is a team-level cost an operator must be able to see.
252
- const reviewStartedAt = Date.now();
253
- // The reviewer's started tool calls, collected only when the no-re-run rule stands (a verified
254
- // pre-check) — the one case the suite-rerun tripwire below can fire.
255
- const reviewerCalls = [];
256
- // The merge cell switches to the reviewer run's live detail (no-op outside a queued
257
- // merge). runPi writes the run's `tumwater_run` label line before spawning pi, and that
258
- // line resets the gate progress accumulator, so the cell never shows a previous run's counts.
259
- setMergeStage(root, role, "reviewing");
260
- // The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
261
- // against the actual diff is exactly the adversarial angle (recovery merges reconstruct
262
- // them from the pinned commit's message).
263
- const pi = await ctx.runGatePi({
264
- cwd: wt,
265
- kind: "gate",
266
- prompt: resuming
267
- ? buildReviewResumePrompt(base, state.reviewResume.patchId)
268
- : buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, verifiedByHarness, base, undefined, priorReview),
269
- // The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
270
- // tick's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
271
- // path below — commit kept, no strike — and re-merges through the author's next tick
272
- // instead of holding the merge queue for a whole authoring tick.
273
- config: reviewCfg,
274
- // This run's own session directory (per-run, keyed by tick and pinned sha): a follow-up
275
- // turn, or a later attempt that resumes this review, continues exactly this run instead
276
- // of whichever review wrote last. A fresh review carries no --continue — the reviewer
277
- // must not inherit the author's context — and old files are collected by the age-based
278
- // prune at orchestrator start.
279
- sessionDir: reviewSessionDir,
280
- ...(resuming ? { continueSession: true } : {}),
281
- sessionName: `tumwater-review-${role}-${ctx.tick}`,
282
- rawLogFile: piLogPath(root, role),
283
- nativeLogFile: nativeLogPath(root, role, reviewCfg.agent ?? "pi"),
284
- // Label this run in the shared transcript (src/ui/transcript.ts renders it as
285
- // `── review @ <ts> ──`) so an operator can tell reviewer runs from author ticks.
286
- label: "review",
287
- signal: ctx.signal,
288
- // A stalled tool call during review hangs the gate just like one during authoring —
289
- // name it in the event feed while the quiet watchdog still counts down.
290
- onToolCallStalled: (message) => warnEventBestEffort(root, role, message),
291
- onToolCallStart: verifiedByHarness
292
- ? (toolName, args) => {
293
- reviewerCalls.push({ toolName, args });
199
+ const patchApproved = reviewedPatchId !== undefined && state.approvedPatchIds?.includes(reviewedPatchId) === true;
200
+ // The check runs alongside the reviewer when a second pool slot is free right now (Fast merge
201
+ // part 12): it starts here, in its own checkout of this head, while the reviewer run below
202
+ // starts in the change's own slot. An already-approved patch has no reviewer to overlap, and
203
+ // with no free slot (or no way to lease one) the check runs first, as it always did.
204
+ const afterMerge = (config.review.afterMerge ?? []).includes(baseRoleOf(role));
205
+ let overlap;
206
+ if (!retry.pre) {
207
+ const lease = patchApproved || afterMerge ? null : await leaseCheckSlot(ctx, head);
208
+ if (lease)
209
+ overlap = startOverlappedCheck(ctx, state, head, lease);
210
+ else {
211
+ const pre = await gateBuildPrecheck(ctx, state, head, reject);
212
+ if (pre.resolved) {
213
+ state.reviewResume = undefined;
214
+ return pre.resolved;
294
215
  }
295
- : undefined,
296
- // A gate run that read an image its model cannot see is logged, not killed (plans/
297
- // image-input.md "Detecting a blind read"): the reviewer keeps working from what it CAN
298
- // see, and the image_blind line names the file, the model, and this tick so the operator
299
- // knows the diff was judged under a blind read.
300
- onBlindRead: (path, action) => logEventBestEffort(root, {
301
- source: role,
302
- type: "image_blind",
303
- run: ctx.tick,
304
- path,
305
- model: reviewCfg.model,
306
- action,
307
- }),
308
- });
309
- // A reviewer told the pre-check passed that re-ran the full suite anyway held the merge
310
- // slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
311
- // scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
312
- // included — the run happened either way; the verdict itself is not touched.
313
- const rerun = suiteRerunWarning(reviewerCalls);
314
- if (rerun)
315
- warnEventBestEffort(root, role, rerun);
316
- // Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
317
- const keep = async (r) => {
318
- await restoreReviewedHead(wt, head, root, role);
319
- return r;
320
- };
321
- if (pi.aborted) {
322
- // Shutdown mid-review: fail closed — the commit stays on the branch and the
323
- // resumed/following tick re-reviews it via the combined ahead-of-main diff. Any resume
324
- // marker from an earlier attempt is dropped: an abort says nothing about the diff, and the
325
- // next attempt starts clean rather than inheriting a half-counted session.
326
- state.reviewResume = undefined;
327
- return keep(withRuns({ decision: "failed", aborted: true }, pi, null, null));
328
- }
329
- // The bounded nudge (BUGS.md 2026-10-02): the reviewer broke the no-re-run rule despite a
330
- // green pre-check, so one turn on its own session names the call and asks it to finish
331
- // without re-running. A repeat on the nudge's own tool calls fails the review — strike-free,
332
- // commit kept, re-reviewed under the normal path — instead of paying a third suite run.
333
- let nudge = null;
334
- let nudgeRepeat = null;
335
- if (rerun && pi.ok) {
336
- const nudgeCalls = [];
337
- nudge = await requestNoRerun(followupCtx, rerun, nudgeCalls);
338
- if (nudge?.aborted) {
339
- return keep(withRuns({ decision: "failed", aborted: true }, pi, null, nudge));
340
- }
341
- nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
342
- if (nudgeRepeat) {
343
- warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the merge re-reviews it without a third suite run`);
344
- }
345
- }
346
- let verdict = parseVerdict(pi.verdictText ?? "");
347
- // The nudge turn's reply can carry the verdict the review run lost — use it before spending
348
- // any other recovery turn, but never on a repeat (the repeat fails the review regardless).
349
- if (!verdict && nudge && !nudgeRepeat) {
350
- const recovered = parseVerdict(nudge.verdictText ?? "");
351
- if (recovered) {
352
- verdict = recovered;
353
- warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with the no-re-run nudge turn on its session");
216
+ retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
354
217
  }
355
218
  }
356
- // A run that FAILED (`ok` false: transport error, failed spawn, timeout) produced no reply,
357
- // so no follow-up is attempted — it is evidence about the backend, and the strike-free
358
- // branch below keeps it that way (BUGS.md 2026-09-20). Only a run that completed and
359
- // replied without a parseable VERDICT earns the recovery turn: that is evidence about the
360
- // reviewer's output format, not about the diff, and it is as recoverable as the author
361
- // side's missing SUMMARY (BUGS.md 2026-09-29). The nudge turn is the bounded follow-up when
362
- // the reviewer broke the no-re-run rule — one turn total, so a spent nudge never earns the
363
- // verdict-recovery turn on top.
364
- let followUp = null;
365
- if (!verdict && pi.ok && !nudge) {
366
- followUp = await requestVerdict(followupCtx);
367
- if (followUp?.aborted)
368
- return keep(withRuns({ decision: "failed", aborted: true }, pi, followUp, nudge));
369
- const recovered = followUp ? parseVerdict(followUp.verdictText ?? "") : null;
370
- if (recovered) {
371
- verdict = recovered;
372
- warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with a follow-up turn on its own session");
373
- }
374
- }
375
- // A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
376
- // turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
377
- // commit stays for the next merge's re-review, the discard counter never advances, and no
378
- // rejection is recorded against the author's diff.
379
- if (nudgeRepeat) {
380
- const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the merge re-reviews it under the normal path.`;
381
- logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
219
+ const knownMainFailures = retry.pre?.knownMainFailures;
220
+ // A role listed in review.afterMerge merges on its gate alone: the deterministic rejects and the
221
+ // pre-check above still ran, and the merged commit is reviewed after it lands
222
+ // (reviewMergedCommit, run by the merge pipeline's post-review drain).
223
+ if (afterMerge && retry.pre) {
382
224
  state.reviewResume = undefined;
383
- recordReview(state, "failed", [message], head);
384
- return keep(withRuns({ decision: "failed", detail: message }, pi, followUp, nudge));
225
+ state.lastApprovedHead = head;
226
+ return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
385
227
  }
386
- if (!verdict) {
387
- const followUpError = followUp && !followUp.ok ? followUp.errorMessage : undefined;
388
- // The nudge turn's own death is backend evidence, exactly like the verdict-recovery
389
- // follow-up's: a nudge that died with a transport error is the backend failing after the
390
- // reviewer judged nothing — about the world, never about the diff — so the strike-free
391
- // branch below must see it, and its error names the failure.
392
- const nudgeError = nudge && !nudge.ok ? nudge.errorMessage : undefined;
393
- // A progressing timeout of the REVIEW run itself (never a follow-up or nudge turn, both of
394
- // which run only after pi ok) leaves the session for the next attempt while the patch is
395
- // identifiable and the resume budget lasts. That is what makes the recorded failure honest:
396
- // while a resume remains the next attempt continues this review, and once the budget is
397
- // spent or the failure cannot resume, it reviews the change from scratch (BUGS.md
398
- // 2026-09-30). A string rewrite of runPi's exact suffix, so any other cause — and the plain
399
- // no-progress timeout, which promises nothing — passes through untouched.
400
- const resumeAttempts = resuming ? state.reviewResume?.attempts ?? 0 : 0;
401
- const resumeRemains = !pi.ok &&
402
- !pi.aborted &&
403
- pi.timedOutProgressing &&
404
- reviewedPatchId !== undefined &&
405
- resumeAttempts < REVIEW_RESUME_LIMIT &&
406
- // The promise must be one the next attempt can keep: only a dir whose backend still
407
- // holds a gate session to resume earns the marker (and the "continues this review"
408
- // wording). Without this a backend that leaves no session file would record a resume the
409
- // next attempt silently declines, reviewing from scratch anyway.
410
- agentSessionResumable(reviewSessionDir, {
411
- kind: "gate",
412
- agent: reviewCfg.agent ?? "pi",
413
- }, claudeConfigDir);
414
- const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
415
- ? " — the commit is kept; the next attempt continues this review"
416
- : " — the commit is kept; the next attempt reviews it from scratch");
417
- const message = reviewTimeoutRewrite(followUpError ??
418
- nudgeError ??
419
- pi.errorMessage ??
420
- `no parseable VERDICT line in the reviewer's reply${nudge
421
- ? ", even after the no-re-run nudge turn on its session"
422
- : followUp
423
- ? ", even after a follow-up turn on its session"
424
- : ""}`);
425
- logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
426
- // A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
427
- // for the next tick's re-review and do not advance the per-HEAD discard counter. That
428
- // holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
429
- // follow-up (BUGS.md 2026-09-29), and for the no-re-run nudge turn (BUGS.md 2026-10-02):
430
- // each one's death is the backend failing after the reviewer had already judged nothing —
431
- // evidence about the world, never about the diff.
432
- if (!pi.ok || (followUp && !followUp.ok) || (nudge && !nudge.ok)) {
433
- if (resumeRemains) {
434
- // Record the session this run used (the resumed directory, or this fresh run's own),
435
- // so the next attempt `--continue`s exactly it.
436
- state.reviewResume = {
437
- patchId: reviewedPatchId,
438
- sessionDir: reviewSessionDir,
439
- attempts: resumeAttempts,
440
- at: Date.now(),
441
- };
442
- saveLoopState(root, state);
443
- }
444
- else {
445
- state.reviewResume = undefined;
446
- }
447
- recordReview(state, "failed", [message], head);
448
- const configError = pi.configError || Boolean(followUp?.configError) || Boolean(nudge?.configError);
449
- return keep(withRuns({ decision: "failed", detail: message, ...(configError ? { configError: true } : {}) }, pi, followUp, nudge));
450
- }
451
- // Consecutive failures of THIS HEAD only: a new commit (new HEAD) starts fresh. Read
452
- // *before* overwriting lastReview with this failure.
453
- const prev = state.lastReview;
454
- const sameHead = prev?.verdict === "failed" && prev.head === head;
228
+ if (patchApproved && retry.pre) {
455
229
  state.reviewResume = undefined;
456
- state.unreviewFailures = (sameHead ? (state.unreviewFailures ?? 0) : 0) + 1;
457
- recordReview(state, "failed", [message], head);
458
- let discarded = false;
459
- if ((state.unreviewFailures ?? 0) >= REVIEW_FAILURE_LIMIT) {
460
- await resetWorktreeToMain(wt, mainBranch);
461
- state.unreviewFailures = 0; // The HEAD is gone; nothing left to count against.
462
- discarded = true;
463
- warnEventBestEffort(root, role, `discarding unreviewed leftover after ${REVIEW_FAILURE_LIMIT} failed reviews (${shortSha(head)})`);
464
- }
465
- const failed = { ...withRuns({ decision: "failed", detail: message }, pi, followUp, nudge), ...(discarded ? { discarded: true } : {}) };
466
- return discarded ? failed : keep(failed);
230
+ state.lastApprovedHead = head;
231
+ return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
467
232
  }
468
- if (verdict.verdict === "reject") {
469
- const rejected = await reject(verdict.reasons, Date.now() - reviewStartedAt);
470
- return withRuns({ ...rejected }, pi, followUp, nudge);
233
+ return runReviewer(ctx, state, {
234
+ head, overlap, knownMainFailures, retry, summary, commitBody, highFriction,
235
+ reviewedPatchId, reject, keep, settleFailedCheck,
236
+ });
237
+ }
238
+ /** Review the change ahead of main, retrying a failed reviewer run in place (twice, 30 s apart)
239
+ * before the attempt ends `failed`: a run that timed out, was killed, died in transport or at
240
+ * the provider, or stated no VERDICT after its recovery turn. A resumable session is resumed,
241
+ * otherwise the retry starts fresh (reviewAttempt). A configuration error and an abort are not
242
+ * retried. While the reviewer's provider is held the retry waits for the hold to lift and
243
+ * costs nothing. Each pause runs without the caller's team permit (idleWithoutProviderLease):
244
+ * a verification hands its lease back while it waits and takes it back before the next attempt.
245
+ * See reviewAttempt for the rest of the gate's contract. */
246
+ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFriction) {
247
+ const retry = { attempt: 1, retriesLeft: ctx.retries ?? REVIEW_RETRIES };
248
+ const delayMs = ctx.retryDelayMs ?? Number(process.env[REVIEW_RETRY_DELAY_ENV] ?? REVIEW_RETRY_DELAY_MS);
249
+ for (;;) {
250
+ const result = await reviewAttempt(ctx, state, summary, commitBody, highFriction, retry);
251
+ if (!result.retryable)
252
+ return result;
253
+ for (const run of [result.run, result.followUpRun, result.nudgeRun])
254
+ if (run)
255
+ ctx.foldUsage?.(run);
256
+ const held = ctx.reviewHeld?.() ?? false;
257
+ warnEventBestEffort(ctx.root, ctx.role, `the reviewer run failed (${result.detail ?? "no detail"}) — ${held ? "waiting for the provider hold to lift, then " : ""}retrying the review in place`);
258
+ // The pause holds no team permit: a verification's lease is handed back while it waits and
259
+ // re-taken before the next attempt, so a reviewer that keeps failing does not starve the roles.
260
+ await idleWithoutProviderLease(async () => {
261
+ await sleepUnlessAborted(delayMs, ctx.signal);
262
+ while (ctx.reviewHeld?.() && !ctx.signal?.aborted)
263
+ await sleepUnlessAborted(Math.min(delayMs, 5000), ctx.signal);
264
+ }, ctx.signal);
265
+ if (ctx.signal?.aborted)
266
+ return { decision: "failed", aborted: true };
267
+ if (!held)
268
+ retry.retriesLeft -= 1;
269
+ retry.attempt += 1;
471
270
  }
472
- // Approve: record the reviewed HEAD and discard any stray working-tree edits the reviewer
473
- // made while reading around — its only output channel is the verdict. The patch-id keys the
474
- // approval to the diff it judged; unreadable, it clears any older one (no reuse at all).
475
- state.lastApprovedHead = head;
476
- state.lastApprovedPatchId = reviewedPatchId;
477
- state.reviewResume = undefined;
478
- recordReview(state, "approve", verdict.reasons, head);
479
- state.unreviewFailures = 0;
480
- await restoreReviewedHead(wt, head, root, role);
481
- logEventBestEffort(root, {
482
- source: role,
483
- type: "review_verdict",
484
- head,
485
- reason: verdict.reasons[0],
486
- durationMs: Date.now() - reviewStartedAt,
271
+ }
272
+ function sleepUnlessAborted(ms, signal) {
273
+ return new Promise((resolve) => {
274
+ if (signal?.aborted)
275
+ return resolve();
276
+ const done = () => {
277
+ clearTimeout(timer);
278
+ signal?.removeEventListener("abort", done);
279
+ resolve();
280
+ };
281
+ const timer = setTimeout(done, ms);
282
+ signal?.addEventListener("abort", done, { once: true });
487
283
  });
488
- return withRuns({ decision: "approved", verifiedHead }, pi, followUp, nudge);
489
284
  }