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
@@ -0,0 +1,51 @@
1
+ /** The operator's deliberate acceptance of a pending post-merge review without running it
2
+ * (src/cli/cli-review-commands.ts `tumwater reviews --accept <n>`), split out of the CLI so
3
+ * the review queue stays a pure reader/editor and this module owns the destructive half —
4
+ * removing an unattempted entry on purpose. It is the sibling of `tumwater queue --cancel
5
+ * <n>` (src/merge/merge-cancel.ts): the queue's no-third-path gap that the at-risk alert's
6
+ * shared sentence names ("…or accept the commit unreviewed deliberately"). It reads the same
7
+ * FIFO list `readPostReviews` numbers 1..N and settles the entry by its commit, so the removed
8
+ * entry is the very one the printed position named. */
9
+ import { readPostReviews, settlePostReview } from "./post-merge-review.js";
10
+ import { warnEventBestEffort } from "../events/events.js";
11
+ import { shortSha } from "../text/format.js";
12
+ /** Remove one readable entry the operator accepts unreviewed: refuse it (writing nothing) when
13
+ * it carries `fixed` — the repair already merged, so accepting it unreviewed is meaningless
14
+ * and the false "accepted unreviewed" warning would mislead — otherwise settle it with no
15
+ * verdict (`settlePostReview`; its failed write already warns and never throws, INV-BT2),
16
+ * log one `warning` under the entry's role, and return the `accepted` outcome naming what was
17
+ * retired. A rejected entry waits on nothing, so it is accepted like any other pending entry.
18
+ * The single removal both `acceptPostReview` (by position) and `acceptPostReviewBySha`
19
+ * (by sha) share, so the CLI and the dashboard's post-review-accept cannot drift on what
20
+ * accepting means. */
21
+ function removeAcceptedEntry(root, entry) {
22
+ if (entry.fixed !== undefined)
23
+ return { status: "settled", commit: entry.commit };
24
+ settlePostReview(root, entry.commit, null);
25
+ const message = `post-merge review of ${shortSha(entry.commit)} accepted unreviewed at the operator's request — the commit stays on main unreviewed`; // vocabulary-ok: the accepted-unreviewed wording those reasons it is the operator's choice
26
+ warnEventBestEffort(root, entry.role, message);
27
+ return { status: "accepted", role: entry.role, commit: entry.commit, summary: entry.summary };
28
+ }
29
+ /** Retire the Nth pending post-merge review — 1-based, as `tumwater reviews` numbers it —
30
+ * accepting the commit unreviewed at the operator's request. `position` is resolved against
31
+ * `readPostReviews` (the same readable FIFO list the command renders), so the position matches
32
+ * the line `tumwater reviews` printed. An out-of-range position writes nothing and returns the
33
+ * current readable depth; otherwise it runs the same `removeAcceptedEntry` the sha variant
34
+ * shares, so the two never drift on what accepting means (see `removeAcceptedEntry` for the
35
+ * removal, the fixed-guard, the warning, and the never-throws guarantee). */
36
+ export function acceptPostReview(root, position) {
37
+ const entries = readPostReviews(root);
38
+ if (position < 1 || position > entries.length)
39
+ return { status: "out-of-range", depth: entries.length };
40
+ return removeAcceptedEntry(root, entries[position - 1]);
41
+ }
42
+ /** Remove the pending post-merge review whose entry's commit is `sha` (the browser twin of
43
+ * `tumwater reviews --accept <n>`, resolving by commit because the dashboard rows carry one).
44
+ * A commit that matches none is `{ status: "gone" }` and changes nothing; otherwise it runs the
45
+ * same `removeAcceptedEntry` the position variant shares. */
46
+ export function acceptPostReviewBySha(root, sha) {
47
+ const entry = readPostReviews(root).find((e) => e.commit.toLowerCase() === sha.toLowerCase());
48
+ if (!entry)
49
+ return { status: "gone" };
50
+ return removeAcceptedEntry(root, entry);
51
+ }
@@ -0,0 +1,112 @@
1
+ /** The renderer behind `tumwater reviews` — the operator-facing text for the post-merge review
2
+ * queue (.tumwater/state/post-review.json): one line per well-formed entry in the given order,
3
+ * plus a `post-merge review queue (N):` header. Pure function of the entry list — the optional
4
+ * `now` is the only clock input, and when it is a number and an entry carries `enqueuedAt` the
5
+ * line names the wait since the entry was first queued, exactly as the merge queue names its
6
+ * age — so a test can pin its output exactly. The `merge queue empty` convention
7
+ * (src/merge/merge-queue-render.ts) is reused for the empty case. */
8
+ import { roleLabel } from "../roles/role-labels.js";
9
+ import { shortSha } from "../text/format.js";
10
+ import { squash } from "../text/text.js";
11
+ import { filterKeepPositions } from "../collections.js";
12
+ import { plural } from "../text/phrases.js";
13
+ import { humanAgo } from "../text/datetime.js";
14
+ /** The one state every observer surface narrows on: `fixed` when the repair already merged
15
+ * (the terminal Forward-never-back 1a/6 record — naming both shas, it is not pending anything),
16
+ * `rejected` when the reviewer turned the commit back (empty `rejected` array still counts),
17
+ * `failing` when the review runs keep ending without a verdict (`failures` positive), else
18
+ * `waiting`. The precedence matches what postReviewStatusLabel already printed — an entry can
19
+ * never be both — so the status label and `tumwater reviews --status` share this one definition
20
+ * and cannot drift. */
21
+ export function postReviewState(entry) {
22
+ if (entry.fixed !== undefined)
23
+ return "fixed";
24
+ if (entry.rejected !== undefined)
25
+ return "rejected";
26
+ if (typeof entry.failures === "number" && entry.failures > 0)
27
+ return "failing";
28
+ return "waiting";
29
+ }
30
+ /** The states `tumwater reviews --status` accepts, in the order the filter's error message
31
+ * lists them (the terminal `fixed` last). `postReviewState` is the definition; this only names
32
+ * the vocabulary. */
33
+ export const POST_REVIEW_STATES = ["waiting", "failing", "rejected", "fixed"];
34
+ /** The bracketed status suffix one post-merge review entry carries on every observer surface
35
+ * (`tumwater reviews`, the dashboard's Post-merge reviews card, and the TUI Pending pane's
36
+ * review block): the reviewer's recorded rejected reasons when `rejected` is set; else the
37
+ * number of verdict-less review runs when `failures` is positive, naming the
38
+ * last failure's reason beside the count when `lastFailure` is a non-empty string; else `""`.
39
+ * Reasons are squashed to 60 as `tumwater reviews` has always printed them, and the failure
40
+ * detail to 80. Branches on `postReviewState` so the label and the `--status` filter share one
41
+ * state definition; extracted so the
42
+ * CLI, the dashboard and the TUI share one status wording and cannot drift. */
43
+ export function postReviewStatusLabel(entry) {
44
+ switch (postReviewState(entry)) {
45
+ case "fixed":
46
+ return `[fixed — the repair ${shortSha(entry.fixed)} already merged on main]`;
47
+ case "rejected":
48
+ return `[rejected — ${entry.rejected.map((r) => squash(r, 60)).join("; ")}]`;
49
+ case "failing": {
50
+ const failures = entry.failures; // postReviewState only says "failing" when failures > 0
51
+ if (typeof entry.lastFailure === "string" && entry.lastFailure.length > 0) {
52
+ return `[${plural(failures, "failed review run")} — last: ${squash(entry.lastFailure, 80)}]`;
53
+ }
54
+ return `[${plural(failures, "failed review run")}]`;
55
+ }
56
+ default:
57
+ return "";
58
+ }
59
+ }
60
+ /** Narrows a whole post-merge review list to the entries `keep` accepts, and returns
61
+ * the `scope` renderPostReviews takes: the kept roles, each kept entry's 1-based place in the
62
+ * whole list, and the whole list's entry count — plus, when a status filter is active, the
63
+ * `filter` text the header and no-match line print. The filtering-and-positioning step is the
64
+ * shared `filterKeepPositions` (src/collections.ts) that `scopeMergeQueue` (src/merge/
65
+ * merge-queue-render.ts) also uses, so `tumwater reviews --role` keeps the same whole-queue
66
+ * positions `--accept <n>` still addresses. */
67
+ export function scopePostReviews(entries, roles, keep, filter) {
68
+ const { kept, positions } = filterKeepPositions(entries, keep);
69
+ return {
70
+ entries: kept,
71
+ scope: { roles, positions, total: entries.length, ...(filter !== undefined ? { filter } : {}) },
72
+ };
73
+ }
74
+ /** The text `tumwater reviews` prints. A totally empty queue renders `post-merge review queue
75
+ * empty`; otherwise a header and one line per entry — position, role, short commit, and, when
76
+ * `now` and the entry's `enqueuedAt` are both numbers, the wait since the commit was queued
77
+ * (`<age> ago`, between the short commit and the summary) — then the squashed summary and the
78
+ * status suffix from `postReviewStatusLabel`, appended as `" · " + label` only when it is
79
+ * non-empty. Without a `now`, or for an entry with no `enqueuedAt`, the line is byte-identical
80
+ * to before, so an older `post-review.json` and every existing renderer test read unchanged.
81
+ * With the optional `scope` (`reviews --role`/`--status`: `entries` already holds only the kept
82
+ * entries', `positions` their 1-based places in the whole queue, `total` the whole queue's
83
+ * entry count, and `filter` the `--role`/`--status` filter text when one is active) each line
84
+ * keeps its whole-queue number and the header reads
85
+ * `post-merge review queue (<kept> of <total>, <filter>):`, where `<filter>` is `scope.filter`
86
+ * when set else `--role <ids>`. A scope that keeps nothing of a non-empty queue prints
87
+ * `no post-merge reviews matching <filter> (post-merge review queue holds <total>)` when a
88
+ * filter is set, else `no post-merge reviews for <ids> (…)`. Without `scope` and `now` the
89
+ * output is byte-identical to before. */
90
+ export function renderPostReviews(entries, scope, now) {
91
+ const total = scope ? scope.total : entries.length;
92
+ if (total === 0)
93
+ return "post-merge review queue empty";
94
+ if (scope && entries.length === 0) {
95
+ const reason = scope.filter !== undefined ? `matching ${scope.filter}` : `for ${scope.roles.join(",")}`;
96
+ return `no post-merge reviews ${reason} (post-merge review queue holds ${total})`;
97
+ }
98
+ const lines = [
99
+ scope
100
+ ? `post-merge review queue (${entries.length} of ${total}, ${scope.filter ?? `--role ${scope.roles.join(",")}`}):`
101
+ : `post-merge review queue (${plural(total, "entry", "entries")}):`,
102
+ ];
103
+ entries.forEach((entry, i) => {
104
+ const label = postReviewStatusLabel(entry);
105
+ const suffix = label === "" ? "" : ` · ${label}`;
106
+ const age = typeof now === "number" && typeof entry.enqueuedAt === "number"
107
+ ? ` · ${humanAgo(entry.enqueuedAt, now)}`
108
+ : "";
109
+ lines.push(` ${scope ? scope.positions[i] : i + 1}. ${roleLabel(entry.role)} ${shortSha(entry.commit)}${age} · ${squash(entry.summary, 80)}${suffix}`);
110
+ });
111
+ return lines.join("\n");
112
+ }
@@ -0,0 +1,239 @@
1
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
+ import { postReviewPath, postReviewWorktreePath, nativeLogPath, piLogPath, reviewRunSessionDir, reviewFixRef } from "../paths.js";
3
+ import { reviewedAfterMerge, reviewRunConfig } from "../config/config-views.js";
4
+ import { logEventBestEffort, TUMWATER_SOURCE, tryOrWarn } from "../events/events.js";
5
+ import { isJsonObject } from "../files/json-object.js";
6
+ import { git, gitCouldNotStart, gitTry } from "../git/git-run.js";
7
+ import { commitAll, deleteRef, setRef } from "../git/git.js";
8
+ import { ensureDetachedWorktree } from "../git/worktree.js";
9
+ import { useWorktree } from "../git/worktree-use.js";
10
+ import { readPrinciples } from "../prompt/principles.js";
11
+ import { buildReviewPrompt } from "../prompt/gate-prompts.js";
12
+ import { shortSha } from "../text/format.js";
13
+ import { errorMessage } from "../text/text.js";
14
+ import { revisionRoundOf } from "../git/commit-message.js";
15
+ import { REVISION_BACKSTOP } from "../runner/revision.js";
16
+ import { eventRole } from "../events/event-read.js";
17
+ import { baseRoleOf } from "../roles/runner-ids.js";
18
+ import { fixLine } from "../verdict/reply-contract.js";
19
+ import { parseVerdict } from "./review-verdict.js";
20
+ /** A commit's review is dropped, with a warning, after this many runs with no verdict. */
21
+ export const POST_REVIEW_FAILURE_LIMIT = 3;
22
+ /** The bar a day's post-merge rejections must reach before the `post-merge-churn` amber alert
23
+ * (src/ui/team-alerts.ts) fires: one post-merge rejection is the tiering's ordinary cost, two in
24
+ * a day is a role whose already-landed changes keep being turned back — the reviewer-fix signal
25
+ * for an operator to narrow `review.afterMerge` or raise the authoring bar. */
26
+ export const POST_MERGE_REJECT_WARN = 2;
27
+ /** How far back the post-merge-churn rule looks — one day, the 24 h event-window convention
28
+ * every event-reading alert uses (PRIMARY_OVERWRITE_WINDOW_MS sorts beside it). */
29
+ export const POST_MERGE_REJECT_WINDOW_MS = 24 * 60 * 60_000;
30
+ /** Group the `afterMerge: true` `review_rejected` events of `events` by base role — the same
31
+ * `e.type === "review_rejected" && e.afterMerge === true` test the report fold
32
+ * (src/report/report-fold.ts `postMergeRejectionsByRole`) applies, so the churn alert and the
33
+ * report cannot disagree on which rejections are post-merge. The key is the event's base role
34
+ * (`baseRoleOf(eventRole(ev))`), not the raw source: an event's `source` is the runner id
35
+ * (`bugfix-2`), and both the alert's remedy ("narrow `review.afterMerge`") and the scoped
36
+ * report are keyed by base role, so one base role's churn must read as one row. Pre-merge
37
+ * rejections and other event types contribute nothing. */
38
+ export function postMergeRejectionCounts(events) {
39
+ const byRole = new Map();
40
+ for (const ev of events) {
41
+ if (ev.type === "review_rejected" && ev.afterMerge === true) {
42
+ const role = baseRoleOf(eventRole(ev));
43
+ byRole.set(role, (byRole.get(role) ?? 0) + 1);
44
+ }
45
+ }
46
+ return [...byRole.entries()]
47
+ .map(([role, count]) => ({ role, count }))
48
+ .sort((a, b) => b.count - a.count || (a.role < b.role ? -1 : a.role > b.role ? 1 : 0));
49
+ }
50
+ /** The one home of the trigger and selection the `post-merge-churn` alert (src/ui/team-alerts.ts)
51
+ * and the doctor's `post-merge churn` row (src/doctor/doctor-project-checks.ts) share: whether
52
+ * the trailing `POST_MERGE_REJECT_WINDOW_MS` holds at least `POST_MERGE_REJECT_WARN` post-merge
53
+ * `review_rejected` events, and, when it does, which roles and how many each. null when the
54
+ * window holds fewer than the bar, or none — so the two surfaces cannot disagree on when churn
55
+ * fires. */
56
+ export function postMergeChurn(events) {
57
+ const roles = postMergeRejectionCounts(events);
58
+ const count = roles.reduce((n, r) => n + r.count, 0);
59
+ return count >= POST_MERGE_REJECT_WARN ? { count, roles } : null;
60
+ }
61
+ /** Whether `role`'s changes are reviewed after merging instead of before. One home for the
62
+ * membership test, config-views.ts's reviewedAfterMerge — the same predicate the review gate,
63
+ * the runner's `already_queued` recovery arm and the role view read. */
64
+ export function isAfterMergeRole(config, role) {
65
+ return reviewedAfterMerge(config, role);
66
+ }
67
+ /** The queued entries. A torn or hand-edited entry (not an object with a string commit, role and
68
+ * summary, or a `rejected` that is not an array — a non-string `lastFailure` is dropped like the
69
+ * other fields) is dropped, and the next write leaves it out. */
70
+ export function readPostReviews(root) {
71
+ const saved = readJsonFile(postReviewPath(root));
72
+ if (!Array.isArray(saved?.pending))
73
+ return [];
74
+ const entries = [];
75
+ for (const raw of saved.pending) {
76
+ if (!isJsonObject(raw))
77
+ continue;
78
+ const { commit, role, summary, body, failures, lastFailure, rejected, enqueuedAt, fixed } = raw;
79
+ if (typeof commit !== "string" || typeof role !== "string" || typeof summary !== "string")
80
+ continue;
81
+ if (rejected !== undefined && !Array.isArray(rejected))
82
+ continue;
83
+ const entry = { commit, role, summary };
84
+ if (typeof body === "string")
85
+ entry.body = body;
86
+ if (typeof failures === "number" && Number.isFinite(failures))
87
+ entry.failures = failures;
88
+ if (typeof lastFailure === "string")
89
+ entry.lastFailure = lastFailure;
90
+ if (Array.isArray(rejected))
91
+ entry.rejected = rejected.filter((r) => typeof r === "string");
92
+ if (typeof fixed === "string")
93
+ entry.fixed = fixed;
94
+ if (typeof enqueuedAt === "number" && Number.isFinite(enqueuedAt) && enqueuedAt >= 0)
95
+ entry.enqueuedAt = enqueuedAt;
96
+ entries.push(entry);
97
+ }
98
+ return entries;
99
+ }
100
+ function writePostReviews(root, pending) {
101
+ tryOrWarn(root, TUMWATER_SOURCE, "write post-review.json", () => writeJsonAtomic(postReviewPath(root), { pending }));
102
+ }
103
+ /** Queue the merged commit for its review. Never throws: a failed write warns and leaves the file
104
+ * as it was, so the commit goes unreviewed. Stamps `enqueuedAt` when the entry carries none —
105
+ * the file's existing stamp on a re-add of a known commit, else `now` (the default clock when the
106
+ * write path drives the drain) — so a re-queued entry keeps its original stamp and tests can pin
107
+ * a fixed instant. */
108
+ export function addPostReview(root, entry, now = Date.now()) {
109
+ const rest = readPostReviews(root);
110
+ const prior = rest.find((e) => e.commit === entry.commit)?.enqueuedAt;
111
+ const stamped = { ...entry, enqueuedAt: entry.enqueuedAt ?? prior ?? now };
112
+ writePostReviews(root, [...rest.filter((e) => e.commit !== entry.commit), stamped]);
113
+ }
114
+ /** Replace the entry for `commit` with `next`, or drop it when `next` is null. Never throws: a
115
+ * failed write warns and leaves the file as it was, so a later poll handles the entry again. */
116
+ export function settlePostReview(root, commit, next) {
117
+ const rest = readPostReviews(root).filter((e) => e.commit !== commit);
118
+ writePostReviews(root, next ? [...rest, next] : rest);
119
+ }
120
+ /** Cut a review finding's first reason short enough to ride a commit subject (the
121
+ * `tumwater(reviewer): Fix …` subject names it): trimmed of leading/trailing and repeated
122
+ * whitespace, clipped at a word boundary around 100 chars with an ellipsis. */
123
+ function clipFixReason(reason) {
124
+ const trimmed = reason.trim().replace(/\s+/g, " ");
125
+ if (trimmed.length <= 100)
126
+ return trimmed;
127
+ return `${trimmed.slice(0, 97).replace(/\s+\S*$/, "")}…`;
128
+ }
129
+ /** Commit the review worktree's edited paths as the one `tumwater(reviewer): Fix <short sha> —
130
+ * <first finding>` fix commit, made by the harness's own git call (never pi) — commitAll stages
131
+ * and commits everything, so however the reviewer edited a path, the single commit captures it
132
+ * on top of the reviewed commit C (the detached HEAD the worktree reset to); the caller advances
133
+ * the review-fix ref to the returned sha. The harness stamps the fix's body with the `Revision:`
134
+ * trailer one past the reviewed commit's round, so the next review of the fix reads its own round
135
+ * from the same place a rejection's hand-back reads it (Forward never back 2/6) and the self-fix
136
+ * chain counts toward the shared REVISION_BACKSTOP. Returns the new commit's full sha. */
137
+ async function commitReviewerFix(wt, commit, reasons, nextRound) {
138
+ const first = reasons[0] ? clipFixReason(reasons[0]) : "the reviewed change";
139
+ const subject = `tumwater(reviewer): Fix ${shortSha(commit)} — ${first}`;
140
+ const body = reasons.map((r, i) => `${i + 1}. ${r}`).join("\n");
141
+ const withRound = body ? `${body}\nRevision: ${nextRound}` : `Revision: ${nextRound}`;
142
+ return commitAll(wt, `${subject}\n\n${withRound}`);
143
+ }
144
+ /** Review the merged commit C: the usual reviewer prompt over `git diff C^ C`, run in a detached
145
+ * checkout of C. Logs `review_start` and `review_verdict` / `review_rejected` with
146
+ * `afterMerge: true`. A diff git fails to produce is a failed review with no reviewer run: read
147
+ * as empty, the reviewer approved a commit it never saw. Throws only when git could not start
148
+ * (gitCouldNotStart), which says nothing about C, so the caller keeps the entry as it was for
149
+ * the next poll instead of counting a failed run toward dropping it. */
150
+ export async function reviewMergedCommit(root, config, entry, runGatePi, signal) {
151
+ const { commit, role } = entry;
152
+ try {
153
+ const reviewCfg = reviewRunConfig(config);
154
+ const startedAt = Date.now();
155
+ const fixRef = reviewFixRef(commit);
156
+ // The whole review — the detached checkout, the reviewer run, and (on a repair) the status
157
+ // read, the fix commit and its ref — runs under one hold of the post-review worktree
158
+ // (plans/disk-floor.md part 2/4), so nothing can reset or reuse the shared worktree between
159
+ // the reviewer's edit and the commit that captures it.
160
+ return await useWorktree(root, postReviewWorktreePath(root), async () => {
161
+ // The review-fix ref tracks the reviewed commit C (and later the reviewer's fix commit), so
162
+ // a repair survives as a durable reachable ref the pin housekeeping sees. The ref is reset
163
+ // to C at every review and ensureDetachedWorktree hard-resets the worktree to C, so a
164
+ // leftover ref or edit from an earlier review cannot change what is reviewed. Checking the
165
+ // worktree out at C itself (not the ref) keeps the "git could not start" semantics of the
166
+ // detached checkout it replaces: a review whose git cannot start throws gitCouldNotStart.
167
+ const wt = await ensureDetachedWorktree(root, postReviewWorktreePath(root), commit);
168
+ await setRef(root, fixRef, commit);
169
+ const diff = await git(wt, "diff", `${commit}^`, commit);
170
+ logEventBestEffort(root, { source: role, type: "review_start", head: commit, afterMerge: true });
171
+ const pi = await runGatePi({
172
+ cwd: wt,
173
+ kind: "gate",
174
+ prompt: buildReviewPrompt(diff, entry.summary, entry.body, readPrinciples(root), undefined, `${commit}^`, undefined, undefined, undefined, true),
175
+ config: reviewCfg,
176
+ sessionDir: reviewRunSessionDir(root, role, `post-${shortSha(commit)}`),
177
+ sessionName: `tumwater-review-${role}-post-${shortSha(commit)}`,
178
+ rawLogFile: piLogPath(root, role),
179
+ nativeLogFile: nativeLogPath(root, role, reviewCfg.agent ?? "pi"),
180
+ label: "review",
181
+ signal,
182
+ });
183
+ const verdict = pi.ok && !pi.aborted ? parseVerdict(pi.verdictText ?? "") : null;
184
+ if (!verdict) {
185
+ await deleteRef(root, fixRef);
186
+ return { kind: "failed", detail: pi.errorMessage ?? "no parseable VERDICT line in the reviewer's reply" };
187
+ }
188
+ const durationMs = Date.now() - startedAt;
189
+ // One record for the reviewed commit C whatever way the review ends — reject or approve —
190
+ // so the fix-forward landing does not lose the verdict that motivated it.
191
+ if (verdict.verdict === "reject") {
192
+ logEventBestEffort(root, { source: role, type: "review_rejected", head: commit, reasons: verdict.reasons, durationMs, afterMerge: true });
193
+ }
194
+ else {
195
+ logEventBestEffort(root, { source: role, type: "review_verdict", head: commit, reason: verdict.reasons[0], durationMs, afterMerge: true });
196
+ }
197
+ // A rejecting review whose reply carries a FIX: line and whose worktree holds an edit
198
+ // survives as one fix commit on the review ref, kept for merge-post-review.ts to land on
199
+ // main through the ordinary change path. A clean worktree or a reply with no FIX line keeps
200
+ // today's outcomes and deletes the ref.
201
+ const paths = pi.ok && !pi.aborted ? fixLine(pi.verdictText ?? "") : null;
202
+ if (paths && verdict.verdict === "reject" && (await gitTry(wt, "status", "--porcelain"))?.trim().length) {
203
+ // The self-fix chain's round is one past the reviewed commit's `Revision:` trailer; at
204
+ // REVISION_BACKSTOP the reviewer stops fixing and records the surviving findings — the
205
+ // same terminal stop as a rejection's exhaustion, one shared counter, never a second
206
+ // one (Forward never back 2/6). The answered plan is deliberately NOT held here: it was
207
+ // already archived to PLANS-DONE.md when its implementation landed, so it is no longer
208
+ // under `## Planned` and there is nothing left to hold or re-pick.
209
+ const reviewedMessage = (await gitTry(root, "log", "-1", "--format=%B", commit)) ?? "";
210
+ const nextRound = (revisionRoundOf(reviewedMessage) ?? 0) + 1;
211
+ if (nextRound >= REVISION_BACKSTOP) {
212
+ await deleteRef(root, fixRef); // the chain stops here; nothing to land
213
+ logEventBestEffort(root, {
214
+ source: entry.role,
215
+ type: "revision",
216
+ action: "exhausted",
217
+ round: nextRound,
218
+ sha: commit,
219
+ });
220
+ return { kind: "exhausted", reasons: verdict.reasons };
221
+ }
222
+ const fix = await commitReviewerFix(wt, commit, verdict.reasons, nextRound);
223
+ // Advance the review-fix ref from C to the new fix commit, so the repair is reachable and
224
+ // the merge-post-review landing (which checks the fix out by this sha) finds it.
225
+ await setRef(root, fixRef, fix);
226
+ return { kind: "fixed", fix, reasons: verdict.reasons };
227
+ }
228
+ await deleteRef(root, fixRef);
229
+ if (verdict.verdict === "reject")
230
+ return { kind: "reject", reasons: verdict.reasons };
231
+ return { kind: "approve", reasons: verdict.reasons };
232
+ });
233
+ }
234
+ catch (err) {
235
+ if (gitCouldNotStart(err))
236
+ throw err;
237
+ return { kind: "failed", detail: errorMessage(err) };
238
+ }
239
+ }
@@ -1,23 +1,25 @@
1
- // vocabulary: plain
2
1
  import path from "node:path";
3
2
  import { promptBlockOf } from "../brief/brief.js";
4
3
  import { readTextOrNull } from "../files/files.js";
5
4
  import { changeBaseRev, fileContentAt } from "../git/git.js";
6
5
  import { briefCandidates } from "../paths.js";
7
- import { baseRoleOf } from "../roles/loop-ids.js";
8
- /** The brief files that may carry the initial prompt block: the basenames of `briefCandidates`. */
6
+ import { baseRoleOf } from "../roles/runner-ids.js";
7
+ /** The brief files that may carry the brief text block: the basenames of `briefCandidates`. */
9
8
  const BRIEF_FILES = briefCandidates("").map((p) => path.basename(p));
10
9
  /** The roles that may edit PRINCIPLES.md. */
11
10
  const PRINCIPLES_ROLES = new Set(["director", "steward"]);
12
- /** The deterministic gate/stage finding for a change that edits what the loop prompts forbid
13
- * editing: the initial prompt block of a brief file (changed, removed, or the file deleted —
14
- * a block that only moves passes), or PRINCIPLES.md from a role other than director or
15
- * steward. undefined when neither happened. Reads the worktree and the change's merge-base
16
- * like backlogStructureReason; a missing file reads as empty and never throws. */
17
- export async function protectedEditReason(wt, mainBranch, files, role) {
11
+ /** The deterministic gate/stage finding for a change that edits the brief text block of a brief
12
+ * file (changed, removed, or the file deleted — a block that only moves passes); the role-
13
+ * independent half of protectedEditReason, split out so the main watch still runs it when it
14
+ * cannot name the tip's merging role (an unknown role is no basis for calling a PRINCIPLES.md
15
+ * edit unauthorized, but the brief block forbids its edit for every role alike): a brief edit
16
+ * could otherwise land on main un-watched ("Every commit is verified on main, part 1/3").
17
+ * undefined when no brief-file change touches the block. Reads the worktree and the change's
18
+ * baseRev (a commit or branch) like backlogStructureReason; a missing file reads as empty and
19
+ * never throws. */
20
+ export async function briefEditReason(wt, baseRev, files) {
18
21
  const briefs = BRIEF_FILES.filter((f) => files.includes(f));
19
22
  if (briefs.length > 0) {
20
- const baseRev = await changeBaseRev(wt, mainBranch);
21
23
  for (const file of briefs) {
22
24
  const base = promptBlockOf(await fileContentAt(wt, baseRev, file));
23
25
  if (base === null)
@@ -27,6 +29,18 @@ export async function protectedEditReason(wt, mainBranch, files, role) {
27
29
  return `${file} changes the brief between the tumwater:prompt markers — only you edit it (tumwater brief); restore the block`;
28
30
  }
29
31
  }
32
+ return undefined;
33
+ }
34
+ /** The deterministic gate/stage finding for a change that edits what the role prompts forbid
35
+ * editing: the brief text block of a brief file (changed, removed, or the file deleted —
36
+ * a block that only moves passes), or PRINCIPLES.md from a role other than director or
37
+ * steward. undefined when neither happened. Reads the worktree and the change's merge-base
38
+ * like backlogStructureReason; a missing file reads as empty and never throws. */
39
+ export async function protectedEditReason(wt, mainBranch, files, role) {
40
+ const baseRev = await changeBaseRev(wt, mainBranch);
41
+ const brief = await briefEditReason(wt, baseRev, files);
42
+ if (brief !== undefined)
43
+ return brief;
30
44
  if (files.includes("PRINCIPLES.md") && !PRINCIPLES_ROLES.has(baseRoleOf(role)))
31
45
  return `PRINCIPLES.md is edited by the ${role} role — only the director and steward roles may edit it; record an objection in PLANS.md instead and revert the edit`;
32
46
  return undefined;
@@ -0,0 +1,65 @@
1
+ /** The read-only render of the review gate's flake quarantine for the `tumwater
2
+ * quarantine` command. The quarantine list (.tumwater/state/quarantine.json) is the tests
3
+ * that failed then passed on the same tree; the doctor's `flaky checks` row and the
4
+ * selfcheck digest share the underlying quarantineWarning, and this module renders the full
5
+ * list — every entry oldest first with its age, flake count, last sighting and filed bug —
6
+ * so an operator can see which tests the gate is excusing and why. Pure over its inputs
7
+ * (no I/O, no clock): every branch has its own unit tests. */
8
+ import { plural } from "../text/phrases.js";
9
+ import { squash } from "../text/text.js";
10
+ import { humanAgo } from "../text/datetime.js";
11
+ import { quarantineWarning } from "../baseline/main-guardrails.js";
12
+ /** The quarantine as sorted, positioned rows, oldest first (firstSeen ascending, the order
13
+ * quarantineWarning names its oldest), ties by test name, numbered 1..N. `bug` passes
14
+ * through with `""` staying `""`. */
15
+ export function quarantineRows(entries) {
16
+ return [...entries]
17
+ .sort((a, b) => a.firstSeen - b.firstSeen || (a.test < b.test ? -1 : a.test > b.test ? 1 : 0))
18
+ .map((e, i) => ({
19
+ position: i + 1,
20
+ test: e.test,
21
+ firstSeen: e.firstSeen,
22
+ lastSeen: e.lastSeen,
23
+ count: e.count,
24
+ bug: e.bug,
25
+ }));
26
+ }
27
+ /** The command's text for `rows`: an empty list prints the empty line (the merge queue empty
28
+ * convention), otherwise a `flake quarantine: N tests` header, the shared quarantineWarning
29
+ * phrase on its own line when one fires (so the command and the doctor's row agree), then one
30
+ * numbered line per entry naming its test, first-seen and last-seen age, flake count and the
31
+ * BUGS.md heading filed for it (or "not filed"). */
32
+ export function renderQuarantine(rows, now) {
33
+ if (rows.length === 0)
34
+ return "flake quarantine is empty";
35
+ const lines = [`flake quarantine: ${plural(rows.length, "test")}`];
36
+ const warning = quarantineWarning(rows, now);
37
+ if (warning !== null)
38
+ lines.push(warning);
39
+ for (const r of rows) {
40
+ const filed = r.bug ? ` — filed: ${squash(r.bug, 80)}` : " — not filed";
41
+ lines.push(`${r.position}. ${squash(r.test, 80)} — first seen ${humanAgo(r.firstSeen, now)}, ` +
42
+ `last seen ${humanAgo(r.lastSeen, now)}, ${plural(r.count, "flake")}${filed}`);
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+ /** The Markdown section the Failures surfaces (the dashboard's Failures view and the TUI's
47
+ * Failures pane) append below the failure digest — the read twin of the amber quarantine
48
+ * alert, whose `See failures` action lands there. It reads the same `quarantineRows` rule as
49
+ * `tumwater quarantine` (and shares its `quarantineWarning` phrase when one fires), so the CLI
50
+ * and the always-on surfaces cannot disagree. Empty for an empty list, so a window with a
51
+ * digest but a clean quarantine adds nothing. Pure over its inputs (no I/O, no clock). */
52
+ export function renderQuarantineSection(rows, now) {
53
+ if (rows.length === 0)
54
+ return "";
55
+ const lines = ["## Flake quarantine", ""];
56
+ const warning = quarantineWarning(rows, now);
57
+ if (warning !== null)
58
+ lines.push(warning, "");
59
+ for (const r of rows) {
60
+ const filed = r.bug ? `filed: ${squash(r.bug, 80)}` : "not filed";
61
+ lines.push(`- ${squash(r.test, 80)} — first seen ${humanAgo(r.firstSeen, now)}, ` +
62
+ `last seen ${humanAgo(r.lastSeen, now)}, ${plural(r.count, "flake")} — ${filed}`);
63
+ }
64
+ return lines.join("\n");
65
+ }
@@ -1,11 +1,11 @@
1
- import { agentSessionResumable } from "../agent/agent-backend.js";
2
- import { reviewRunConfig, resolvedAgentConfig } from "../config/config-views.js";
1
+ import { backendSessionResumable } from "../agent/agent-backend.js";
2
+ import { reviewRunConfig, resolvedBackendConfig } from "../config/config-views.js";
3
3
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
4
  import { nativeLogPath, piLogPath } from "../paths.js";
5
5
  import { buildNoRerunPrompt, buildVerdictRequestPrompt } from "../prompt/gate-prompts.js";
6
6
  import { cappedRequestTimeouts } from "../request-timeouts.js";
7
7
  /** Hard caps on the VERDICT follow-up turn: it should take one short reply on a warm session,
8
- * so it never gets the review run's own budget (mirrors LoopPi's SUMMARY-request caps). */
8
+ * so it never gets the review run's own budget (mirrors RunnerPi's SUMMARY-request caps). */
9
9
  const VERDICT_REQUEST_TIMEOUT_S = 900;
10
10
  const VERDICT_REQUEST_QUIET_S = 300;
11
11
  /** One bounded follow-up turn on the reviewer's just-finished session: the shared session
@@ -19,7 +19,7 @@ async function runFollowupTurn(ctx, opts) {
19
19
  // backend's own resumable rule keeps a newer line of another kind from being the session a
20
20
  // pi continue would actually reach; claude resumes its recorded id (looked up under the
21
21
  // team's config dir, so a team that set one continues under it).
22
- if (!agentSessionResumable(dir, { kind: "gate", agent: cfg.agent ?? "pi" }, resolvedAgentConfig(ctx.config, "claude").configDir)) {
22
+ if (!backendSessionResumable(dir, { kind: "gate", agent: cfg.agent ?? "pi" }, resolvedBackendConfig(ctx.config, "claude").configDir)) {
23
23
  return null;
24
24
  }
25
25
  return ctx.runGatePi({
@@ -46,7 +46,7 @@ async function runFollowupTurn(ctx, opts) {
46
46
  onBlindRead: (path, action) => logEventBestEffort(ctx.root, {
47
47
  source: ctx.role,
48
48
  type: "image_blind",
49
- run: ctx.tick,
49
+ run: ctx.runNumber,
50
50
  path,
51
51
  model: cfg.model,
52
52
  action,
@@ -55,14 +55,14 @@ async function runFollowupTurn(ctx, opts) {
55
55
  });
56
56
  }
57
57
  /** Ask the reviewer's own session (--continue) for the missing VERDICT line: one tightly
58
- * bounded turn on the just-finished review's session, mirroring LoopPi.requestSummary on the
58
+ * bounded turn on the just-finished review's session, mirroring RunnerPi.requestSummary on the
59
59
  * author side (BUGS.md 2026-09-29). Null when there is no session to continue — the caller
60
60
  * then counts the strike exactly as before. The run is returned even when it failed so the
61
61
  * caller can honor a shutdown abort and fold the spend. */
62
62
  export async function requestVerdict(ctx) {
63
63
  return runFollowupTurn(ctx, {
64
64
  prompt: buildVerdictRequestPrompt(),
65
- sessionName: `tumwater-review-${ctx.role}-${ctx.tick}-verdict`,
65
+ sessionName: `tumwater-review-${ctx.role}-${ctx.runNumber}-verdict`,
66
66
  label: "review-verdict",
67
67
  });
68
68
  }
@@ -76,7 +76,7 @@ export async function requestVerdict(ctx) {
76
76
  export async function requestNoRerun(ctx, rerun, calls) {
77
77
  return runFollowupTurn(ctx, {
78
78
  prompt: buildNoRerunPrompt(rerun),
79
- sessionName: `tumwater-review-${ctx.role}-${ctx.tick}-rerun`,
79
+ sessionName: `tumwater-review-${ctx.role}-${ctx.runNumber}-rerun`,
80
80
  label: "review-rerun",
81
81
  // Collected unconditionally: a nudge only runs after a detected rerun, so the no-re-run
82
82
  // rule stood and the repeat check below needs every call the nudge started.