tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -42,12 +42,19 @@
42
42
  * heading.
43
43
  * - A `path:line` location (`status-roles.ts:66`) was checked as a symbol, and no file is named
44
44
  * that. A location now claims its file, a bare line reference (`:368`) claims nothing, and a
45
- * bare file name is backed by a file of that name anywhere in the walked source dirs. */
45
+ * bare file name is backed by a file of that name anywhere in the walked source dirs.
46
+ *
47
+ * Another false positive (BUGS.md 2026-10-29): a commit id was checked as a symbol, and no code
48
+ * file ever spells one. A dropped post-merge review is filed as a bug whose whole fix is a review
49
+ * of the commit, so the Fix paragraph cites `17c0bbbc…` and nothing else — and six bugfix runners
50
+ * resubmitted that close 29 times in five hours, each rejected the same way. A commit id is now
51
+ * backed when it names a commit in the repository (withoutKnownCommits); a made-up one is still
52
+ * rejected. */
46
53
  import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
47
54
  import { extname, join } from "node:path";
48
55
  import { readTextOrNull, statOrNull, walkFiles } from "../files/files.js";
49
56
  import { fenceTracker, finishedArchiveOf, parseEntryDetails, sectionLines, trailingParenthetical, } from "../backlog/backlog-md.js";
50
- import { changeBaseRev, fileContentAt } from "../git/git.js";
57
+ import { changeBaseRev, commitExists, fileContentAt } from "../git/git.js";
51
58
  import { collapseWhitespace, truncate } from "../text/text.js";
52
59
  /** How much of a one-line record a rejection quotes: the record is one line by construction but
53
60
  * can run to several hundred characters, and the reason rides in a one-line event. */
@@ -263,12 +270,13 @@ function sourceHaystackSignature(root) {
263
270
  sigs.sort();
264
271
  return sigs.join("|");
265
272
  }
266
- /** Every file under the source dirs the haystack reads (src/, test/, scripts/), node_modules and
267
- * dist trees skipped at any depth. */
273
+ /** Every file under the tree the haystack reads — the whole root, so a project whose code lives
274
+ * outside src//test//scripts/ (tools/, tests/, lib/, …) backs its symbols like a src/ one — with
275
+ * build output, vendored code, VCS and harness state skipped at any depth: node_modules, dist,
276
+ * .git and .tumwater. */
268
277
  function walkSourceFiles(root, visit) {
269
- const skipDirs = new Set(["node_modules", "dist"]);
270
- for (const dir of ["src", "test", "scripts"])
271
- walkFiles(join(root, dir), visit, skipDirs);
278
+ const skipDirs = new Set(["node_modules", "dist", ".git", ".tumwater"]);
279
+ walkFiles(root, visit, skipDirs);
272
280
  }
273
281
  /** A slash-free name with a file extension: `status-roles.ts`, not `src/a.ts` or `signalTree`. */
274
282
  const FILE_NAME_RE = /^[^/]+\.[A-Za-z][\w-]*$/;
@@ -292,28 +300,42 @@ export function unbackedSymbols(root, symbols, haystack) {
292
300
  return !fileNames.has(s);
293
301
  });
294
302
  }
303
+ /** A commit id as a Fix paragraph cites one: 7 to 40 lowercase hex digits, optionally cut short
304
+ * with a trailing ellipsis (`3c1c1c6176...`). Group 1 is the id itself. */
305
+ const COMMIT_REF_RE = /^([0-9a-f]{7,40})(?:\.\.\.|…)?$/;
306
+ /** Whether a Fix-paragraph span is shaped like a commit id: a citation of history rather than a
307
+ * name code could hold. The doctor's synchronous scan cannot ask git, so it leaves these out of a
308
+ * record's names; the gate checks them with withoutKnownCommits. */
309
+ export function isCommitRef(span) {
310
+ return COMMIT_REF_RE.test(span);
311
+ }
312
+ /** `missing` without the commit ids that name a commit in the repository at `wt`. No code file
313
+ * ever spells a commit id, so the haystack could never back one: a Fix paragraph citing the
314
+ * commit it reviewed or landed in read as a phantom fix. An id that names no commit stays
315
+ * missing, so an invented one is still rejected. */
316
+ export async function withoutKnownCommits(wt, missing) {
317
+ const out = [];
318
+ for (const name of missing) {
319
+ const sha = COMMIT_REF_RE.exec(name)?.[1];
320
+ if (sha === undefined || !(await commitExists(wt, sha)))
321
+ out.push(name);
322
+ }
323
+ return out;
324
+ }
295
325
  /** The unbacked names for a one-line message: all of them when 3 or fewer, otherwise the
296
326
  * first 3 and an ellipsis — a doctor check or gate message has room for one line. */
297
327
  export function missingSymbolNames(missing) {
298
328
  return missing.length <= 3 ? missing.join(", ") : `${missing.slice(0, 3).join(", ")}…`;
299
329
  }
300
- /** The first false-fix claim in an md-only diff that touches BUGS.md, or undefined when the
301
- * diff backs its Fixed transitions (or makes none). `files` is the ahead-of-main path list
302
- * of the diff merging on `mainBranch`; the worktree `wt` holds the tree being merged. */
303
- export async function falseFixReason(wt, mainBranch, files) {
304
- // Fixed entries live in BUGS.md's own section and in its archive, BUGS-FIXED.md
305
- // (plans/backlog-archive.md): tumwater moves a fixed bug there right before the commit, so a
306
- // fix claim reaches the gate as an archive edit, and an archived entry whose body is intact
307
- // is no claim at all. Both sides of the comparison read both files.
308
- if (!files.includes("BUGS.md") && !files.includes(BUGS_ARCHIVE))
309
- return undefined;
310
- // A deleted, unreadable, or non-regular (symlinked) file reads as empty: there is no Fixed
311
- // transition to verify, so this check passes and lets the gate's sibling structural check
312
- // (backlog-structure.ts) report the dropped section. Reading a symlink raw would pull an
313
- // outside host file's `## Fixed` heading into this gate's verdict (the same lstat policy the
314
- // backlog-conflict resolver and backlogStructureReason pin under INV-G3), so a head is read
315
- // only when lstat shows a regular file; ENOENT and symlinks both read as empty.
316
- const heads = ["BUGS.md", BUGS_ARCHIVE].map((f) => {
330
+ /** The `## Fixed` heads of BUGS.md and its archive as read under the lstat policy: a deleted,
331
+ * unreadable, or non-regular (symlinked) file reads as empty — there is no Fixed transition to
332
+ * verify, so the gate's sibling structural check (backlog-structure.ts) reports the dropped
333
+ * section instead. Reading a symlink raw would pull an outside host file's `## Fixed` heading
334
+ * into the gate's verdict (the same lstat policy the backlog-conflict resolver and
335
+ * backlogStructureReason pin under INV-G3), so a head is read only when lstat shows a regular
336
+ * file; ENOENT and symlinks both read as empty. */
337
+ function readFixedHeads(wt) {
338
+ return ["BUGS.md", BUGS_ARCHIVE].map((f) => {
317
339
  const full = join(wt, f);
318
340
  try {
319
341
  if (!lstatSync(full).isFile())
@@ -324,40 +346,40 @@ export async function falseFixReason(wt, mainBranch, files) {
324
346
  }
325
347
  return readTextOrNull(full) ?? "";
326
348
  });
327
- if (!heads.some((head) => head.includes("## Fixed")))
328
- return undefined;
329
- // The diff's own base, not main's tip: the gate diffs `mainBranch...HEAD` against the
330
- // merge-base, and the entry's Open/Fixed state lives in the tree the change builds on —
331
- // pre-batch main can still carry the record as (falsely) Fixed and make an Open→Fixed
332
- // restoration in a stacked batch compare as already done (BUGS.md 2026-09-23).
349
+ }
350
+ /** The base Fixed section bodies the diff's own merge-base holds, keyed by normalized heading.
351
+ * The base is the diff's own merge-base, not main's tip: the gate diffs `mainBranch...HEAD`
352
+ * against the merge-base, and the entry's Open/Fixed state lives in the tree the change builds
353
+ * on — pre-batch main can still carry the record as (falsely) Fixed and make an Open→Fixed
354
+ * restoration in a stacked batch compare as already done (BUGS.md 2026-09-23). Every base body
355
+ * under a heading counts: two distinct entries can share one normalized heading, and keeping
356
+ * only the last made the other read as rewritten (BUGS.md 2026-10-23). */
357
+ async function baseFixedBodies(wt, mainBranch) {
333
358
  const baseRev = await changeBaseRev(wt, mainBranch);
334
359
  const bases = [await fileContentAt(wt, baseRev, "BUGS.md"), await fileContentAt(wt, baseRev, BUGS_ARCHIVE)];
335
- // An already-Fixed entry is skipped only when its body is unchanged: an md-only edit that
336
- // rewrites an existing Fixed record's narrative must face the symbol check too (BUGS.md
337
- // 2026-09-23 — the in-place narrative rewrite is how a second phantom merge evaded it).
338
- // Every base body under a heading counts: two distinct entries can share one normalized
339
- // heading, and keeping only the last made the other read as rewritten (BUGS.md 2026-10-23).
340
- const baseBodies = new Map();
360
+ const bodies = new Map();
341
361
  for (const e of bases.flatMap((base) => fixedEntries(base))) {
342
362
  const key = normalizeFixedHeading(e.title);
343
- const bodies = baseBodies.get(key) ?? new Set();
344
- bodies.add(e.body);
345
- baseBodies.set(key, bodies);
363
+ const known = bodies.get(key) ?? new Set();
364
+ known.add(e.body);
365
+ bodies.set(key, known);
346
366
  }
347
- // Built lazily, only once a Fixed record's body actually changed: the haystack walks and
348
- // reads the entire source tree (~5 MB here), and the common BUGS.md edit moves nothing to
349
- // Fixed — an Open bug added, a narrative touched elsewhere — so the gate and the in-lock
350
- // recheck each paid that walk for a check that never reached a symbol.
351
- let haystack;
352
- const sections = heads.map((head) => fixedSection(head));
353
- const entries = sections.flatMap((section) => section.entries);
354
- const records = sections.flatMap((section) => section.records);
367
+ return { bases, bodies };
368
+ }
369
+ /** A changed full Fixed entry's rejection, or undefined when every changed entry backs its
370
+ * symbols. An already-Fixed entry is skipped only when its body is unchanged: an md-only edit
371
+ * that rewrites an existing Fixed record's narrative must face the symbol check too (BUGS.md
372
+ * 2026-09-23 — the in-place narrative rewrite is how a second phantom merge evaded it). The
373
+ * haystack is built lazily — only once a Fixed record's body actually changed — because it
374
+ * walks and reads the entire source tree (~5 MB here), and the common BUGS.md edit moves
375
+ * nothing to Fixed: an Open bug added, a narrative touched elsewhere, so the gate and the
376
+ * in-lock recheck each paid that walk for a check that never reached a symbol. */
377
+ async function movedEntryReason(wt, entries, bodies, getHaystack) {
355
378
  for (const { title: heading, body } of entries) {
356
- const knownBodies = baseBodies.get(normalizeFixedHeading(heading));
379
+ const knownBodies = bodies.get(normalizeFixedHeading(heading));
357
380
  if (knownBodies?.has(body))
358
381
  continue;
359
- haystack ??= sourceHaystack(wt);
360
- const missing = unbackedSymbols(wt, fixSymbols(body), haystack);
382
+ const missing = await withoutKnownCommits(wt, unbackedSymbols(wt, fixSymbols(body), getHaystack()));
361
383
  if (missing.length === 0)
362
384
  continue;
363
385
  const names = missingSymbolNames(missing);
@@ -372,10 +394,15 @@ export async function falseFixReason(wt, mainBranch, files) {
372
394
  `paragraph now names code that exists nowhere on this tree: ${names} — include that ` +
373
395
  `code in the same commit, or leave the entry's text as it was`;
374
396
  }
375
- // One-line records are compressions of entries the base already holds as Fixed, and history
376
- // keeps a renamed symbol's old name, so a record the diff adds or edits may carry forward any
377
- // name the base's Fixed section already records. A name it introduces must exist on the tree:
378
- // a bug moved straight from Open into a one-line record faces the same check as a full entry.
397
+ return undefined;
398
+ }
399
+ /** An added or edited one-line Fixed record's rejection, or undefined when each record either
400
+ * reproduces a base line or carries only names the base already holds. One-line records are
401
+ * compressions of entries the base already holds as Fixed, and history keeps a renamed symbol's
402
+ * old name, so a record the diff adds or edits may carry forward any name the base's Fixed
403
+ * section already records. A name it introduces must exist on the tree: a bug moved straight
404
+ * from Open into a one-line record faces the same check as a full entry. */
405
+ async function addedRecordReason(wt, records, bases, getHaystack) {
379
406
  const baseLines = bases.flatMap((base) => sectionLines(base, "Fixed")).map((line) => line.trim());
380
407
  const seen = new Set(baseLines);
381
408
  const baseText = baseLines.join("\n");
@@ -385,8 +412,7 @@ export async function falseFixReason(wt, mainBranch, files) {
385
412
  const introduced = fixSymbols(record).filter((symbol) => !baseText.includes(symbol));
386
413
  if (introduced.length === 0)
387
414
  continue;
388
- haystack ??= sourceHaystack(wt);
389
- const missing = unbackedSymbols(wt, introduced, haystack);
415
+ const missing = await withoutKnownCommits(wt, unbackedSymbols(wt, introduced, getHaystack()));
390
416
  if (missing.length === 0)
391
417
  continue;
392
418
  return (`md-only BUGS.md edit adds the one-line Fixed record "${truncate(record, RECORD_QUOTE_MAX)}", ` +
@@ -396,3 +422,28 @@ export async function falseFixReason(wt, mainBranch, files) {
396
422
  }
397
423
  return undefined;
398
424
  }
425
+ /** The first false-fix claim in an md-only diff that touches BUGS.md, or undefined when the
426
+ * diff backs its Fixed transitions (or makes none). `files` is the ahead-of-main path list
427
+ * of the diff merging on `mainBranch`; the worktree `wt` holds the tree being merged. */
428
+ export async function falseFixReason(wt, mainBranch, files) {
429
+ // Fixed entries live in BUGS.md's own section and in its archive, BUGS-FIXED.md
430
+ // (plans/backlog-archive.md): tumwater moves a fixed bug there right before the commit, so a
431
+ // fix claim reaches the gate as an archive edit, and an archived entry whose body is intact
432
+ // is no claim at all. Both sides of the comparison read both files.
433
+ if (!files.includes("BUGS.md") && !files.includes(BUGS_ARCHIVE))
434
+ return undefined;
435
+ const heads = readFixedHeads(wt);
436
+ if (!heads.some((head) => head.includes("## Fixed")))
437
+ return undefined;
438
+ const { bases, bodies } = await baseFixedBodies(wt, mainBranch);
439
+ const sections = heads.map((head) => fixedSection(head));
440
+ const entries = sections.flatMap((section) => section.entries);
441
+ const records = sections.flatMap((section) => section.records);
442
+ // Built lazily, shared by both checks, only once a Fixed record's body actually changed: the
443
+ // haystack walks and reads the entire source tree (~5 MB here), and the common BUGS.md edit
444
+ // moves nothing to Fixed — an Open bug added, a narrative touched elsewhere — so the gate and
445
+ // the in-lock recheck each paid that walk for a check that never reached a symbol.
446
+ let haystack;
447
+ const getHaystack = () => (haystack ??= sourceHaystack(wt));
448
+ return (await movedEntryReason(wt, entries, bodies, getHaystack)) ?? (await addedRecordReason(wt, records, bases, getHaystack));
449
+ }
@@ -106,7 +106,7 @@ async function stripBlockedPlanRefusalNotes(ctx, wt) {
106
106
  // and a plain writeFileSync that a kill interrupted would commit a truncated file,
107
107
  // permanently dropping the plan entries past the cut point. writeTextAtomic is the
108
108
  // harness's one write-to-temp-then-rename entry point — the same one backlog-write.ts
109
- // and question-answer.ts use for the markdown backlog files.
109
+ // and backlog-question-answer.ts use for the markdown backlog files.
110
110
  try {
111
111
  writeTextAtomic(plansFile, md);
112
112
  for (const title of dropped) {
@@ -74,10 +74,9 @@ export const REPLY_ENDINGS = `- End with exactly ONE of these three endings:
74
74
  ${SUMMARY_BLOCK.replace(/^ {2}/gm, " ")}`;
75
75
  /** The review gate's closing rule as stated to pi: the two accepted verdict forms and the
76
76
  * numbered-reasons line that must follow them. buildReviewPrompt (gate-prompts.ts) ends its
77
- * rules list with this, and the gate's two same-session follow-ups (buildVerdictRequestPrompt,
78
- * buildNoRerunPrompt) re-state it — one home, so the advertised pair is edited in one place
79
- * instead of drifting between the three prompts. Indentation is the caller's: the review prompt
80
- * indents both lines, the two follow-ups indent only the VERDICT line. The machine detector for
77
+ * rules list with this — one home, so the advertised pair is edited in one place
78
+ * instead of drifting between the prompts. Indentation is the caller's: the review prompt
79
+ * indents both lines. The machine detector for
81
80
  * these forms lives beside it (VERDICT_LINE_SOURCE below); gate-prompts.test.ts pins the forms
82
81
  * buildReviewPrompt advertises against what parseVerdict accepts. */
83
82
  export const VERDICT_ENDING = `VERDICT: approve or VERDICT: reject
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tumwater",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Opinionated autonomous development harness built on pi. You write the initial prompt; role-driven loops build the project.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,10 +37,10 @@
37
37
  "build": "rm -rf dist && tsc && node scripts/stamp-build.mjs",
38
38
  "prepack": "npm run build",
39
39
  "lint": "eslint . --cache",
40
- "test": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js",
41
- "test:gate": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js --affected",
42
- "test:e2e": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js e2e",
43
- "test:coverage": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js --coverage",
40
+ "test": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js",
41
+ "test:gate": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node scripts/lint-changed.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js --affected --budget=30",
42
+ "test:e2e": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js e2e",
43
+ "test:coverage": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js --coverage",
44
44
  "smoke:claude": "node scripts/smoke-claude.mjs",
45
45
  "smoke:opencode": "node scripts/smoke-opencode.mjs",
46
46
  "smoke:codex": "node scripts/smoke-codex.mjs",
@@ -1,105 +0,0 @@
1
- /** The AsyncLocalStorage store that lets a run's lease follow its runs across providers
2
- * (plans/provider-concurrency.md §5): a lease rides here for the length of a run, a verify task,
3
- * or a resolver run, and `RunnerPi.runWithTransientRetry` re-keys it to the provider each run
4
- * actually uses. The one choke point every run a role makes (authoring, gate, merge and resolver)
5
- * passes through, so a budget handback or a model-failure switch that moves a run in flight to
6
- * another provider moves the lease with it instead of leaving it parked on the key it was admitted
7
- * on. The check-permit.ts `holdingPermit` store is the pattern: nothing here is a separate permit,
8
- * only the single lease the run path already holds.
9
- *
10
- * The store carries the `piDefault` (the caller's cached readPiDefaultModel result) beside the
11
- * lease so `ensureProviderLease` can resolve a bare no-model pi seam's key exactly as the
12
- * admission/review seam did — a run whose key were computed without it would re-key a default
13
- * team's run off pi's own provider. It also carries the semaphore and, optionally, wait
14
- * hooks, so a re-key that has to park on a full target key reports its wait the way a parked
15
- * resolver does (merge-verify.ts's providerWaitStage). */
16
- import { AsyncLocalStorage } from "node:async_hooks";
17
- import { seamConcurrencyKey } from "../config/config-views.js";
18
- const providerLeaseStore = new AsyncLocalStorage();
19
- /** Run `run` with `lease` in the provider-lease store, so any run it makes re-keys that lease to
20
- * the provider it actually uses (ensureProviderLease). `opts.piDefault` is the caller's cached pi
21
- * default model, so a bare no-model seam resolves to the same key the admission or review seam
22
- * used; `opts.hooks` (the verify: providerWaitStage) hears a re-key wait, if there is one. The same
23
- * lease object is released by the caller after the store is unwrapped — a re-key mutates it in
24
- * place, so release() frees whatever key the run path ended on. */
25
- export function withProviderLease(lease, semaphore, run, opts = {}) {
26
- return providerLeaseStore.run({ lease, semaphore, piDefault: opts.piDefault, hooks: opts.hooks }, run);
27
- }
28
- /** Re-key the calling lease to the provider `cfg` runs on, if it is not there yet. No-op when
29
- * the current async context carries no lease (a direct RunnerPi unit test, `tumwater history`) or
30
- * when the run is already on the lease's key. When it does re-key and `hooks` are present and
31
- * the target key is at its cap, the hooks hear `waiting` before the lease parks and `granted`
32
- * once the key frees — a re-key never waits on the team total (it already holds a team
33
- * permit), only on the target key's headroom. */
34
- export async function ensureProviderLease(cfg) {
35
- const ctx = providerLeaseStore.getStore();
36
- if (!ctx)
37
- return;
38
- const key = seamConcurrencyKey(cfg, ctx.piDefault);
39
- if (ctx.lease.key === key)
40
- return;
41
- // The leased currently holds `old` (or nothing); the target key's in-use does not include it,
42
- // so the key being at its cap is exactly whether rekey() will park.
43
- const parks = ctx.hooks !== undefined && keyAtCap(ctx, key);
44
- if (parks)
45
- ctx.hooks.waiting();
46
- await ctx.lease.rekey(key);
47
- if (parks)
48
- ctx.hooks.granted();
49
- }
50
- /** Whether `key` is at its configured cap right now (an absent or uncapped key is not) — the
51
- * same test semaphore.drive uses to withhold a re-key grant, decided from the live snapshot so
52
- * a re-key wait can be announced before the lease parks (providerWaitStage). */
53
- function keyAtCap(ctx, key) {
54
- if (key === undefined)
55
- return false;
56
- const entry = ctx.semaphore.snapshot().byKey[key];
57
- return entry?.limit !== undefined && entry.inUse >= entry.limit;
58
- }
59
- /** The pool-slot wait rule (plans/provider-concurrency.md §5 "No deadlock"): a lease never holds a
60
- * provider key while it waits for a worktree pool slot. Called by leaseSlot just before it parks on
61
- * a busy pool; it re-keys the calling lease to no key (which never waits), so a run or verify
62
- * waiting for a slot does not hold — and cannot deadlock — a provider slot whose in-use a re-key
63
- * waiter is waiting to drain. No-op without a lease in context. */
64
- export async function dropProviderLeaseKey() {
65
- const ctx = providerLeaseStore.getStore();
66
- if (!ctx)
67
- return;
68
- await ctx.lease.rekey(undefined);
69
- }
70
- /** Run `wait`, an idle stretch inside the calling lease's work, with the lease's team permit and
71
- * key slot handed back (SemaphoreLease.pause), so a parked run can use them meanwhile, then take
72
- * them back (resume) before returning. A failed review's in-place retry pause is the caller: with
73
- * one team permit, a reviewer that kept failing held it through two 30 s pauses and starved every
74
- * other role. Once `signal` has fired, nothing is taken back and the caller's release() settles a
75
- * still-paused lease. No-op without a lease in context. */
76
- export async function idleWithoutProviderLease(wait, signal) {
77
- const ctx = providerLeaseStore.getStore();
78
- if (!ctx)
79
- return wait();
80
- ctx.lease.pause();
81
- try {
82
- await wait();
83
- }
84
- finally {
85
- if (!signal?.aborted)
86
- await untilAborted(ctx.lease.resume(), signal);
87
- }
88
- }
89
- /** `p`, or an earlier return once `signal` fires: a resume parked on a full team must not keep an
90
- * aborted verification waiting for a permit it no longer needs. */
91
- async function untilAborted(p, signal) {
92
- if (!signal)
93
- return p;
94
- let onAbort = () => { };
95
- const aborted = new Promise((resolve) => {
96
- onAbort = resolve;
97
- signal.addEventListener("abort", onAbort, { once: true });
98
- });
99
- try {
100
- await Promise.race([p, aborted]);
101
- }
102
- finally {
103
- signal.removeEventListener("abort", onAbort);
104
- }
105
- }
@@ -1,87 +0,0 @@
1
- import { backendSessionResumable } from "../agent/agent-backend.js";
2
- import { reviewRunConfig, resolvedBackendConfig } from "../config/config-views.js";
3
- import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
- import { nativeLogPath, piLogPath } from "../paths.js";
5
- import { buildNoRerunPrompt, buildVerdictRequestPrompt } from "../prompt/gate-prompts.js";
6
- import { cappedRequestTimeouts } from "../request-timeouts.js";
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 RunnerPi's SUMMARY-request caps). */
9
- const VERDICT_REQUEST_TIMEOUT_S = 900;
10
- const VERDICT_REQUEST_QUIET_S = 300;
11
- /** One bounded follow-up turn on the reviewer's just-finished session: the shared session
12
- * guard, capped budgets, and runGatePi wiring behind requestVerdict and requestNoRerun —
13
- * exactly two call sites, differing only in prompt, session name, label, and (the nudge only)
14
- * a started-tool-call collector. Null when there is no session to continue. */
15
- async function runFollowupTurn(ctx, opts) {
16
- const dir = ctx.sessionDir;
17
- const cfg = reviewRunConfig(ctx.config);
18
- // The verdict/re-run follow-up continues the review session the gate just wrote. The
19
- // backend's own resumable rule keeps a newer line of another kind from being the session a
20
- // pi continue would actually reach; claude resumes its recorded id (looked up under the
21
- // team's config dir, so a team that set one continues under it).
22
- if (!backendSessionResumable(dir, { kind: "gate", agent: cfg.agent ?? "pi" }, resolvedBackendConfig(ctx.config, "claude").configDir)) {
23
- return null;
24
- }
25
- return ctx.runGatePi({
26
- cwd: ctx.wt,
27
- kind: "gate",
28
- prompt: opts.prompt,
29
- config: {
30
- ...cfg,
31
- ...cappedRequestTimeouts(cfg, VERDICT_REQUEST_TIMEOUT_S, VERDICT_REQUEST_QUIET_S),
32
- },
33
- sessionDir: dir,
34
- // The whole point: continue the just-finished review's session, which already holds
35
- // everything the reviewer read and concluded.
36
- continueSession: true,
37
- sessionName: opts.sessionName,
38
- rawLogFile: piLogPath(ctx.root, ctx.role),
39
- nativeLogFile: nativeLogPath(ctx.root, ctx.role, cfg.agent ?? "pi"),
40
- label: opts.label,
41
- signal: ctx.signal,
42
- onToolCallStalled: (message) => warnEventBestEffort(ctx.root, ctx.role, message),
43
- // A gate run that read an image its model cannot see is logged, not killed (plans/
44
- // image-input.md "Detecting a blind read") — same shape as the main review run in
45
- // review.ts.
46
- onBlindRead: (path, action) => logEventBestEffort(ctx.root, {
47
- source: ctx.role,
48
- type: "image_blind",
49
- run: ctx.runNumber,
50
- path,
51
- model: cfg.model,
52
- action,
53
- }),
54
- ...(opts.onToolCallStart ? { onToolCallStart: opts.onToolCallStart } : {}),
55
- });
56
- }
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 RunnerPi.requestSummary on the
59
- * author side (BUGS.md 2026-09-29). Null when there is no session to continue — the caller
60
- * then counts the strike exactly as before. The run is returned even when it failed so the
61
- * caller can honor a shutdown abort and fold the spend. */
62
- export async function requestVerdict(ctx) {
63
- return runFollowupTurn(ctx, {
64
- prompt: buildVerdictRequestPrompt(),
65
- sessionName: `tumwater-review-${ctx.role}-${ctx.runNumber}-verdict`,
66
- label: "review-verdict",
67
- });
68
- }
69
- /** The suite-rerun nudge (BUGS.md 2026-10-02): the reviewer broke the no-re-run rule, so one
70
- * tightly bounded turn on the just-finished review's session names the exact tool call and
71
- * asks it to finish without re-running. Mirrors requestVerdict (same caps); `calls` collects
72
- * the nudge turn's own started tool calls so the caller can detect a repeat. Null when there
73
- * is no session to continue — the caller then has no repeat evidence, only the warning. The
74
- * run is returned even when it failed so the caller can honor a shutdown abort and fold the
75
- * spend. */
76
- export async function requestNoRerun(ctx, rerun, calls) {
77
- return runFollowupTurn(ctx, {
78
- prompt: buildNoRerunPrompt(rerun),
79
- sessionName: `tumwater-review-${ctx.role}-${ctx.runNumber}-rerun`,
80
- label: "review-rerun",
81
- // Collected unconditionally: a nudge only runs after a detected rerun, so the no-re-run
82
- // rule stood and the repeat check below needs every call the nudge started.
83
- onToolCallStart: (toolName, args) => {
84
- calls.push({ toolName, args });
85
- },
86
- });
87
- }