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
@@ -1,395 +0,0 @@
1
- import { APPROVED_PATCH_IDS_MAX, REVIEW_RESUME_LIMIT, saveRunnerStateBestEffort } from "../runner/runner-state.js";
2
- import { modelSelectorField, resolvedBackendConfig, reviewRunConfig } from "../config/config-views.js";
3
- import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
- import { git } from "../git/git-run.js";
5
- import { changeBaseRev, headOf } from "../git/git.js";
6
- import { aheadOfMainDiff, revisionInterdiff } from "../git/git-diff.js";
7
- import { resetWorktreeToMain } from "../git/worktree.js";
8
- import { nativeLogPath, piLogPath } from "../paths.js";
9
- import { readPrinciples } from "../prompt/principles.js";
10
- import { buildReviewPrompt, buildReviewResumePrompt } from "../prompt/gate-prompts.js";
11
- import { backendSessionResumable } from "../agent/agent-backend.js";
12
- import { requestNoRerun, requestVerdict } from "./review-followup.js";
13
- import { parseVerdict } from "./review-verdict.js";
14
- import { recordReview } from "../run/run-apply.js";
15
- import { shortSha } from "../text/format.js";
16
- import { suiteRerunWarning } from "./suite-rerun.js";
17
- import { setMergeStage } from "../merge/merge-slot.js";
18
- /** Attach the review gate's pi runs onto a partial GateResult — `run` plus the optional
19
- * verdict-recovery follow-up (`followUpRun`) and no-re-run nudge (`nudgeRun`) fields, each
20
- * present only when its turn actually ran. Spelled once so every return site of
21
- * reviewAheadOfMain that carries runs shares one shape for the usage-folding caller
22
- * (merge-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
23
- * spread per site that a new run kind would have to update everywhere. */
24
- export function withRuns(r, run, followUp, nudge) {
25
- return {
26
- ...r,
27
- run,
28
- ...(followUp ? { followUpRun: followUp } : {}),
29
- ...(nudge ? { nudgeRun: nudge } : {}),
30
- };
31
- }
32
- /** Put the worktree back on the exact head the gate reviewed: the reviewer's only output is its
33
- * verdict, so a commit it made must never reach the merge pin or the next re-review. */
34
- export async function restoreReviewedHead(wt, head, root, role) {
35
- const now = await headOf(wt, "HEAD").catch(() => null);
36
- if (now && now !== head) {
37
- warnEventBestEffort(root, role, `the reviewer moved HEAD to ${shortSha(now)} during review; restored the reviewed head ${shortSha(head)}`);
38
- }
39
- await git(wt, "reset", "--hard", head);
40
- }
41
- /** Consecutive failed reviews of one branch HEAD after which the leftover is discarded with
42
- * a warning: a misconfigured reviewer model must not be able to wedge a role into re-reviewing
43
- * the same commit forever. */
44
- export const REVIEW_FAILURE_LIMIT = 3;
45
- function combineSignals(a, b) {
46
- return a ? AbortSignal.any([a, b]) : b;
47
- }
48
- /** The reviewer run: resume-vs-fresh session choice, prompt, run, follow-up turns, verdict parse
49
- * and the approve/reject bookkeeping. Called by reviewAttempt once the deterministic gate has
50
- * passed the change on to a reviewer. */
51
- export async function runReviewer(ctx, state, input) {
52
- const { root, role, wt, mainBranch, config } = ctx;
53
- const { head, reviewedPatchId, overlap, knownMainFailures, retry, summary, commitBody, highFriction } = input;
54
- const { reject, keep, settleFailedCheck } = input;
55
- // The reviewer's model selector (plans/model-tiers.md "Observability"): resolved from the
56
- // same config the run below gets, so the event names what the reviewer actually runs on —
57
- // the budget fallback included. Omitted when no model is configured (pi's own default).
58
- const reviewCfg = reviewRunConfig(config);
59
- // The team claude config dir (plans/notes-command.md "Keeping funes out of team runs"), so
60
- // a claude review's resume guard looks under the same dir the review run writes its transcript
61
- // to — a team that set one must resume under it, not the default ~/.claude.
62
- const claudeConfigDir = resolvedBackendConfig(config, "claude").configDir;
63
- // A progressing timeout left this run's session for the next attempt (RunnerState.reviewResume):
64
- // resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
65
- // the directory still holds a gate session to `--continue`. Otherwise the stale marker is
66
- // cleared and this attempt reviews the change from scratch.
67
- const priorResume = state.reviewResume;
68
- const resuming = priorResume !== undefined &&
69
- priorResume.patchId === reviewedPatchId &&
70
- priorResume.attempts < REVIEW_RESUME_LIMIT &&
71
- backendSessionResumable(priorResume.sessionDir, {
72
- kind: "gate",
73
- agent: reviewCfg.agent ?? "pi",
74
- }, claudeConfigDir);
75
- if (resuming) {
76
- // Count the resume before the run: a crash must not let the same session be resumed
77
- // forever without ever spending the budget.
78
- priorResume.attempts += 1;
79
- saveRunnerStateBestEffort(root, state);
80
- }
81
- else {
82
- state.reviewResume = undefined;
83
- }
84
- // The directory this run actually uses: a resumed attempt continues the timed-out run's
85
- // session, a fresh one its own. Both the run and its follow-up turns must target this dir —
86
- // a follow-up that used ctx.sessionDir while the review resumed another dir would
87
- // `--continue` a session that never held this review.
88
- const reviewSessionDir = resuming ? state.reviewResume.sessionDir : ctx.sessionDir;
89
- // The follow-up turns (verdict recovery, no-re-run nudge) continue the just-finished review,
90
- // so they must read the same directory. A fresh run's dir is already ctx's, so only a resume
91
- // needs the override.
92
- const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
93
- logEventBestEffort(root, {
94
- source: role,
95
- type: "review_start",
96
- head,
97
- ...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
98
- ...(resuming ? { resumed: true } : {}),
99
- ...modelSelectorField(reviewCfg),
100
- });
101
- // Persist the phase BEFORE the run so a dashboard mid-review shows "reviewing" and a crash
102
- // mid-review is distinguishable from a crash mid-author-run on resume (stray edits are the
103
- // reviewer's then, not the author's). Cleared at run end alongside `running`.
104
- state.phase = "review";
105
- saveRunnerStateBestEffort(root, state);
106
- // Freeze the revision the diff is measured from once, before the run: main moves while a
107
- // merge is approved, and a reviewer comparing against the live `main` ref reads another role's
108
- // just-merged work as this change reverting it (BUGS.md 2026-10-06). The prompt names this SHA
109
- // and tells the reviewer to compare against it instead.
110
- const base = await changeBaseRev(wt, mainBranch);
111
- const diff = await aheadOfMainDiff(wt, mainBranch);
112
- // A revision's re-review gets the prior review's objections and the interdiff between the two
113
- // versions (plans/revise-rejected.md part 2/2), so it checks what the revision had to resolve
114
- // instead of starting from no memory. A missing prior object yields an empty interdiff, never
115
- // a throw (revisionInterdiff).
116
- const priorReview = ctx.priorReview
117
- ? {
118
- round: ctx.revisionRound ?? 1,
119
- reasons: ctx.priorReview.reasons,
120
- interdiff: await revisionInterdiff(wt, mainBranch, ctx.priorReview.sha, head),
121
- }
122
- : undefined;
123
- // The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
124
- // merge on local hardware is a team-level cost an operator must be able to see.
125
- const reviewStartedAt = Date.now();
126
- // The reviewer's started tool calls: the no-re-run rule always stands (tumwater runs the
127
- // check), so the suite-rerun tripwire below always applies.
128
- const reviewerCalls = [];
129
- // The merge cell switches to the reviewer run's live detail (no-op outside a queued
130
- // merge). runPi writes the run's `tumwater_run` label line before spawning pi, and that
131
- // line resets the gate progress accumulator, so the cell never shows a previous run's counts.
132
- setMergeStage(root, role, overlap ? "reviewing-check" : "reviewing");
133
- // A check that already went red has aborted the overlap: no reviewer run starts, the verdict
134
- // below is the gate's alone.
135
- if (overlap?.abort.signal.aborted)
136
- return settleFailedCheck(overlap, await overlap.gate);
137
- // The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
138
- // against the actual diff is exactly the adversarial angle (recovery merges reconstruct
139
- // them from the pinned commit's message).
140
- const pi = await ctx.runGatePi({
141
- cwd: wt,
142
- kind: "gate",
143
- prompt: resuming
144
- ? buildReviewResumePrompt(base, state.reviewResume.patchId)
145
- : buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, base, undefined, priorReview, knownMainFailures),
146
- // The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
147
- // run's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
148
- // path below — commit kept, no strike — and re-merges through the author's next run
149
- // instead of holding the merge queue for a whole authoring run.
150
- config: reviewCfg,
151
- // This run's own session directory (per-run, keyed by run and pinned sha): a follow-up
152
- // turn, or a later attempt that resumes this review, continues exactly this run instead
153
- // of whichever review wrote last. A fresh review carries no --continue — the reviewer
154
- // must not inherit the author's context — and old files are collected by the age-based
155
- // prune at orchestrator start.
156
- sessionDir: reviewSessionDir,
157
- ...(resuming ? { continueSession: true } : {}),
158
- sessionName: `tumwater-review-${role}-${ctx.runNumber}`,
159
- rawLogFile: piLogPath(root, role),
160
- nativeLogFile: nativeLogPath(root, role, reviewCfg.agent ?? "pi"),
161
- // Label this run in the shared transcript (src/ui/transcript.ts renders it as
162
- // `── review @ <ts> ──`) so an operator can tell reviewer runs from author runs.
163
- label: "review",
164
- signal: overlap ? combineSignals(ctx.signal, overlap.abort.signal) : ctx.signal,
165
- // A stalled tool call during review hangs the gate just like one during authoring —
166
- // name it in the event feed while the quiet watchdog still counts down.
167
- onToolCallStalled: (message) => warnEventBestEffort(root, role, message),
168
- onToolCallStart: (toolName, args) => {
169
- reviewerCalls.push({ toolName, args });
170
- },
171
- // A gate run that read an image its model cannot see is logged, not killed (plans/
172
- // image-input.md "Detecting a blind read"): the reviewer keeps working from what it CAN
173
- // see, and the image_blind line names the file, the model, and this run so the operator
174
- // knows the diff was judged under a blind read.
175
- onBlindRead: (path, action) => logEventBestEffort(root, {
176
- source: role,
177
- type: "image_blind",
178
- run: ctx.runNumber,
179
- path,
180
- model: reviewCfg.model,
181
- action,
182
- }),
183
- });
184
- // A reviewer told the pre-check passed that re-ran the full suite anyway held the merge
185
- // slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
186
- // scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
187
- // included — the run happened either way; the verdict itself is not touched.
188
- const rerun = suiteRerunWarning(reviewerCalls);
189
- if (rerun)
190
- warnEventBestEffort(root, role, rerun);
191
- // Join the overlapped check (Fast merge part 12): a gate that did not pass settles the attempt;
192
- // a green gate lets the reviewer's outcome stand, and a reviewer that finished first waited
193
- // here for the gate.
194
- if (overlap) {
195
- if (!overlap.done)
196
- setMergeStage(root, role, "build-check");
197
- const pre = await overlap.gate;
198
- if (pre.resolved)
199
- return settleFailedCheck(overlap, pre, pi);
200
- retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
201
- }
202
- const verifiedHead = retry.pre?.verifiedHead;
203
- if (pi.aborted) {
204
- // Shutdown mid-review: fail closed — the commit stays on the branch and the
205
- // resumed/following run re-reviews it via the combined ahead-of-main diff. Any resume
206
- // marker from an earlier attempt is dropped: an abort says nothing about the diff, and the
207
- // next attempt starts clean rather than inheriting a half-counted session.
208
- state.reviewResume = undefined;
209
- return keep(withRuns({ decision: "failed", aborted: true }, pi, null, null));
210
- }
211
- // The bounded nudge (BUGS.md 2026-10-02): the reviewer broke the no-re-run rule despite a
212
- // green pre-check, so one turn on its own session names the call and asks it to finish
213
- // without re-running. A repeat on the nudge's own tool calls fails the review — strike-free,
214
- // commit kept, re-reviewed under the normal path — instead of paying a third suite run.
215
- let nudge = null;
216
- let nudgeRepeat = null;
217
- if (rerun && pi.ok) {
218
- const nudgeCalls = [];
219
- nudge = await requestNoRerun(followupCtx, rerun, nudgeCalls);
220
- if (nudge?.aborted) {
221
- return keep(withRuns({ decision: "failed", aborted: true }, pi, null, nudge));
222
- }
223
- nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
224
- if (nudgeRepeat) {
225
- warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the merge re-reviews it without a third suite run`);
226
- }
227
- }
228
- let verdict = parseVerdict(pi.verdictText ?? "");
229
- // The nudge turn's reply can carry the verdict the review run lost — use it before spending
230
- // any other recovery turn, but never on a repeat (the repeat fails the review regardless).
231
- if (!verdict && nudge && !nudgeRepeat) {
232
- const recovered = parseVerdict(nudge.verdictText ?? "");
233
- if (recovered) {
234
- verdict = recovered;
235
- warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with the no-re-run nudge turn on its session");
236
- }
237
- }
238
- // A run that FAILED (`ok` false: transport error, failed spawn, timeout) produced no reply,
239
- // so no follow-up is attempted — it is evidence about the backend, and the strike-free
240
- // branch below keeps it that way (BUGS.md 2026-09-20). Only a run that completed and
241
- // replied without a parseable VERDICT earns the recovery turn: that is evidence about the
242
- // reviewer's output format, not about the diff, and it is as recoverable as the author
243
- // side's missing SUMMARY (BUGS.md 2026-09-29). The nudge turn is the bounded follow-up when
244
- // the reviewer broke the no-re-run rule — one turn total, so a spent nudge never earns the
245
- // verdict-recovery turn on top.
246
- let followUp = null;
247
- if (!verdict && pi.ok && !nudge) {
248
- followUp = await requestVerdict(followupCtx);
249
- if (followUp?.aborted)
250
- return keep(withRuns({ decision: "failed", aborted: true }, pi, followUp, nudge));
251
- const recovered = followUp ? parseVerdict(followUp.verdictText ?? "") : null;
252
- if (recovered) {
253
- verdict = recovered;
254
- warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with a follow-up turn on its own session");
255
- }
256
- }
257
- // A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
258
- // turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
259
- // commit stays for the next merge's re-review, the discard counter never advances, and no
260
- // rejection is recorded against the author's diff.
261
- if (nudgeRepeat) {
262
- const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the merge re-reviews it under the normal path.`;
263
- logEventBestEffort(root, { source: role, type: "review_failed", head, message, attempt: retry.attempt, durationMs: Date.now() - reviewStartedAt });
264
- state.reviewResume = undefined;
265
- recordReview(state, "failed", [message], head);
266
- return keep(withRuns({ decision: "failed", detail: message }, pi, followUp, nudge));
267
- }
268
- if (!verdict) {
269
- return settleUnparsedReview(ctx, state, input, {
270
- pi, followUp, nudge, resuming, reviewSessionDir, reviewStartedAt, reviewCfg, claudeConfigDir,
271
- });
272
- }
273
- if (verdict.verdict === "reject") {
274
- const rejected = await reject(verdict.reasons, Date.now() - reviewStartedAt);
275
- return withRuns({ ...rejected }, pi, followUp, nudge);
276
- }
277
- // Approve: record the reviewed HEAD and discard any stray working-tree edits the reviewer
278
- // made while reading around — its only output channel is the verdict. The patch-id keys the
279
- // approval to the diff it judged; unreadable, it is not remembered.
280
- state.lastApprovedHead = head;
281
- if (reviewedPatchId !== undefined) {
282
- const kept = (state.approvedPatchIds ?? []).filter((id) => id !== reviewedPatchId);
283
- state.approvedPatchIds = [...kept, reviewedPatchId].slice(-APPROVED_PATCH_IDS_MAX);
284
- }
285
- state.reviewResume = undefined;
286
- recordReview(state, "approve", verdict.reasons, head);
287
- state.unreviewFailures = 0;
288
- await restoreReviewedHead(wt, head, root, role);
289
- logEventBestEffort(root, {
290
- source: role,
291
- type: "review_verdict",
292
- head,
293
- reason: verdict.reasons[0],
294
- durationMs: Date.now() - reviewStartedAt,
295
- });
296
- return withRuns({ decision: "approved", verifiedHead }, pi, followUp, nudge);
297
- }
298
- /** Settle a review that produced no verdict: a dead backend keeps the commit (strike-free, retried
299
- * in place while retries last), a reply that stayed unparseable counts a strike per HEAD and
300
- * discards the leftover at REVIEW_FAILURE_LIMIT. */
301
- async function settleUnparsedReview(ctx, state, input, outcome) {
302
- const { root, role, wt, mainBranch } = ctx;
303
- const { head, retry, reviewedPatchId, keep } = input;
304
- const { pi, followUp, nudge, resuming, reviewSessionDir, reviewStartedAt, reviewCfg, claudeConfigDir } = outcome;
305
- const followUpError = followUp && !followUp.ok ? followUp.errorMessage : undefined;
306
- // The nudge turn's own death is backend evidence, exactly like the verdict-recovery
307
- // follow-up's: a nudge that died with a transport error is the backend failing after the
308
- // reviewer judged nothing — about the world, never about the diff — so the strike-free
309
- // branch below must see it, and its error names the failure.
310
- const nudgeError = nudge && !nudge.ok ? nudge.errorMessage : undefined;
311
- // A progressing timeout of the REVIEW run itself (never a follow-up or nudge turn, both of
312
- // which run only after pi ok) leaves the session for the next attempt while the patch is
313
- // identifiable and the resume budget lasts. That is what makes the recorded failure honest:
314
- // while a resume remains the next attempt continues this review, and once the budget is
315
- // spent or the failure cannot resume, it reviews the change from scratch (BUGS.md
316
- // 2026-09-30). A string rewrite of runPi's exact suffix, so any other cause — and the plain
317
- // no-progress timeout, which promises nothing — passes through untouched.
318
- const resumeAttempts = resuming ? state.reviewResume?.attempts ?? 0 : 0;
319
- const resumeRemains = !pi.ok &&
320
- !pi.aborted &&
321
- pi.timedOutProgressing &&
322
- reviewedPatchId !== undefined &&
323
- resumeAttempts < REVIEW_RESUME_LIMIT &&
324
- // The promise must be one the next attempt can keep: only a dir whose backend still
325
- // holds a gate session to resume earns the marker (and the "continues this review"
326
- // wording). Without this a backend that leaves no session file would record a resume the
327
- // next attempt silently declines, reviewing from scratch anyway.
328
- backendSessionResumable(reviewSessionDir, {
329
- kind: "gate",
330
- agent: reviewCfg.agent ?? "pi",
331
- }, claudeConfigDir);
332
- const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
333
- ? " — the commit is kept; the next attempt continues this review"
334
- : " — the commit is kept; the next attempt reviews it from scratch");
335
- const message = reviewTimeoutRewrite(followUpError ??
336
- nudgeError ??
337
- pi.errorMessage ??
338
- `no parseable VERDICT line in the reviewer's reply${nudge
339
- ? ", even after the no-re-run nudge turn on its session"
340
- : followUp
341
- ? ", even after a follow-up turn on its session"
342
- : ""}`);
343
- logEventBestEffort(root, { source: role, type: "review_failed", head, message, attempt: retry.attempt, durationMs: Date.now() - reviewStartedAt });
344
- // A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
345
- // for the next run's re-review and do not advance the per-HEAD discard counter. That
346
- // holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
347
- // follow-up (BUGS.md 2026-09-29), and for the no-re-run nudge turn (BUGS.md 2026-10-02):
348
- // each one's death is the backend failing after the reviewer had already judged nothing —
349
- // evidence about the world, never about the diff.
350
- if (!pi.ok || (followUp && !followUp.ok) || (nudge && !nudge.ok)) {
351
- if (resumeRemains) {
352
- // Record the session this run used (the resumed directory, or this fresh run's own),
353
- // so the next attempt `--continue`s exactly it.
354
- state.reviewResume = {
355
- patchId: reviewedPatchId,
356
- sessionDir: reviewSessionDir,
357
- attempts: resumeAttempts,
358
- at: Date.now(),
359
- };
360
- saveRunnerStateBestEffort(root, state);
361
- }
362
- else {
363
- state.reviewResume = undefined;
364
- }
365
- const configError = pi.configError || Boolean(followUp?.configError) || Boolean(nudge?.configError);
366
- // Retried in place (reviewAheadOfMain): the failure is about the world, so the verify tries
367
- // the reviewer again before the attempt ends. A configuration error can never succeed.
368
- if (retry.retriesLeft > 0 && !configError) {
369
- return keep(withRuns({ decision: "failed", detail: message, retryable: true }, pi, followUp, nudge));
370
- }
371
- recordReview(state, "failed", [message], head);
372
- return keep(withRuns({ decision: "failed", detail: message, ...(configError ? { configError: true } : {}) }, pi, followUp, nudge));
373
- }
374
- // Consecutive failures of THIS HEAD only: a new commit (new HEAD) starts fresh. Read
375
- // *before* overwriting lastReview with this failure.
376
- // Retry before counting: the strike below is per verify's final outcome, not per run.
377
- if (retry.retriesLeft > 0) {
378
- state.reviewResume = undefined;
379
- return keep(withRuns({ decision: "failed", detail: message, retryable: true }, pi, followUp, nudge));
380
- }
381
- const prev = state.lastReview;
382
- const sameHead = prev?.verdict === "failed" && prev.head === head;
383
- state.reviewResume = undefined;
384
- state.unreviewFailures = (sameHead ? (state.unreviewFailures ?? 0) : 0) + 1;
385
- recordReview(state, "failed", [message], head);
386
- let discarded = false;
387
- if ((state.unreviewFailures ?? 0) >= REVIEW_FAILURE_LIMIT) {
388
- await resetWorktreeToMain(wt, mainBranch);
389
- state.unreviewFailures = 0; // The HEAD is gone; nothing left to count against.
390
- discarded = true;
391
- warnEventBestEffort(root, role, `discarding unreviewed leftover after ${REVIEW_FAILURE_LIMIT} failed reviews (${shortSha(head)})`);
392
- }
393
- const failed = { ...withRuns({ decision: "failed", detail: message }, pi, followUp, nudge), ...(discarded ? { discarded: true } : {}) };
394
- return discarded ? failed : keep(failed);
395
- }
@@ -1,76 +0,0 @@
1
- /** Consecutive provider-class failures that trip a fallback episode. */
2
- export const MODEL_FALLBACK_FAILURES = 3;
3
- /** The first probe cooldown after a trip; a failed probe doubles it. */
4
- export const MODEL_FALLBACK_COOLDOWN_MS = 5 * 60_000;
5
- /** The ceiling a repeatedly-failing probe's cooldown doubles toward. */
6
- export const MODEL_FALLBACK_MAX_COOLDOWN_MS = 30 * 60_000;
7
- /** True only while the role is in a fallback episode whose probe is still in the future — the
8
- * runs that use the fallback pair. */
9
- export function modelFallbackActive(state, now) {
10
- return state !== undefined && state.since > 0 && now < state.probeAt;
11
- }
12
- /** True only while the role is in a fallback episode whose cooldown has elapsed: the next
13
- * run uses the primary as the probe. */
14
- export function modelFallbackProbe(state, now) {
15
- return state !== undefined && state.since > 0 && now >= state.probeAt;
16
- }
17
- /** Classify one authoring agent run's verdict about the primary (src/run/run-usage.ts records the
18
- * same flags for the team-wide hold). An agent run that was aborted, quiet-killed, or timed out
19
- * — or that read an image its model cannot see (the runner killed it, plans/image-input.md) — is
20
- * inconclusive even when it also reports provider evidence: the harness killed it, so it is
21
- * no verdict about the primary's health. */
22
- export function modelFallbackVerdict(pi) {
23
- if (pi.aborted || pi.quietKilled || pi.timedOut || pi.imageBlind)
24
- return "inconclusive";
25
- if (!pi.ok && (pi.transientRateLimit || pi.transientBackend))
26
- return "provider-failure";
27
- return "answered";
28
- }
29
- /** Fold one authoring agent run's verdict into the role's fallback state, returning the next state
30
- * or undefined for "running the primary" (no episode).
31
- *
32
- * - Not in fallback, provider failure: increment the consecutive count; the
33
- * MODEL_FALLBACK_FAILURES-th trips the episode.
34
- * - Not in fallback, an answering agent run: clear the running count (undefined).
35
- * - Not in fallback, an inconclusive agent run: unchanged — a harness kill is not evidence the
36
- * primary recovered, so the running count survives it.
37
- * - In fallback, a fallback run (`probe` false): unchanged — its outcome neither returns
38
- * the role to primary nor re-trips it.
39
- * - In fallback, a probe run: an inconclusive probe is unchanged (retried on the next run);
40
- * an answering probe clears the episode; a provider-class failed probe doubles the cooldown
41
- * (capped) and schedules the next one.
42
- */
43
- export function recordModelFallback(state, outcome) {
44
- const { verdict, probe, now, reason } = outcome;
45
- if (state !== undefined && state.since > 0) {
46
- if (!probe)
47
- return state;
48
- if (verdict === "inconclusive")
49
- return state;
50
- if (verdict === "answered")
51
- return undefined;
52
- const cooldownMs = Math.min(state.cooldownMs * 2, MODEL_FALLBACK_MAX_COOLDOWN_MS);
53
- return { ...state, cooldownMs, probeAt: now + cooldownMs, reason: reason || state.reason };
54
- }
55
- if (verdict === "inconclusive")
56
- return state;
57
- if (verdict === "answered")
58
- return undefined;
59
- const failures = (state?.failures ?? 0) + 1;
60
- if (failures >= MODEL_FALLBACK_FAILURES) {
61
- return {
62
- failures: 0,
63
- since: now,
64
- probeAt: now + MODEL_FALLBACK_COOLDOWN_MS,
65
- cooldownMs: MODEL_FALLBACK_COOLDOWN_MS,
66
- reason: reason || "backend failure",
67
- };
68
- }
69
- return { failures, since: 0, probeAt: 0, cooldownMs: 0, reason };
70
- }
71
- /** The reason label one agent run's provider evidence yields: "rate-limit" for a 429, else the
72
- * backend-failure kind. Exported so the runner's evidence assembly and tests share one
73
- * spelling. */
74
- export function providerFailureReason(rateLimit, kind) {
75
- return rateLimit ? "rate-limit" : (kind ?? "backend failure");
76
- }
@@ -1,66 +0,0 @@
1
- import { logEventBestEffort } from "../events/events.js";
2
- import { modelFallbackVerdict, providerFailureReason, recordModelFallback } from "./model-fallback.js";
3
- /** Fold one AUTHORING agent run's verdict into the role's model-fallback state
4
- * (src/runner/model-fallback.ts) and emit the episode's started/ended events. Only the agent run
5
- * that actually happened is evidence: a run that never invoked pi (a skipped director
6
- * inbox, a recovered leftover, a merge-side `main_red` pin) returns from runOnce before this is
7
- * reached, and modelFallbackVerdict reports an aborted/killed/timed-out agent run as
8
- * inconclusive — so neither can end an episode without the primary having answered.
9
- * A role whose tier resolves to no fallback pair never trips, and an episode persisted from a
10
- * config that HAD a pair is cleared when a live edit removes it (or sets it to "pause"), so a
11
- * stale "on fallback" mark cannot outlive the config that created it. Persists on any state
12
- * change; the run's own finalize save carries it too. */
13
- export function foldModelFallbackPhase(ctx, fallbackCtx, pi) {
14
- const s = ctx.state;
15
- const prior = s.modelFallback;
16
- const now = Date.now();
17
- const verdict = modelFallbackVerdict(pi);
18
- const reason = providerFailureReason(pi.transientRateLimit, pi.backendKind);
19
- const wasIn = prior !== undefined && prior.since > 0;
20
- if (fallbackCtx.fallback === null) {
21
- // No fallback pair resolves any more: the feature is off for this role. End a persisted
22
- // episode (the primary just ran, so this run IS the return), and drop any pre-trip count
23
- // so a later config starts from zero. A killed/aborted/timed-out agent run is no verdict, so it
24
- // leaves the episode in place exactly as it does on a configured fallback.
25
- if (prior === undefined)
26
- return;
27
- if (wasIn && verdict === "inconclusive")
28
- return;
29
- emitFallbackEnded(ctx, prior, fallbackCtx.primary, now, wasIn);
30
- s.modelFallback = undefined;
31
- ctx.save();
32
- return;
33
- }
34
- const next = recordModelFallback(prior, { verdict, probe: fallbackCtx.probe, now, reason });
35
- const willBeIn = next !== undefined && next.since > 0;
36
- if (!wasIn && willBeIn) {
37
- logEventBestEffort(ctx.root, {
38
- source: ctx.role,
39
- type: "model_fallback_started",
40
- provider: fallbackCtx.fallback.provider,
41
- model: fallbackCtx.fallback.model,
42
- reason: next?.reason,
43
- });
44
- }
45
- else if (wasIn && !willBeIn && prior !== undefined) {
46
- emitFallbackEnded(ctx, prior, fallbackCtx.primary, now, wasIn);
47
- }
48
- if (next !== prior) {
49
- s.modelFallback = next;
50
- ctx.save();
51
- }
52
- }
53
- /** Log the model_fallback_ended event for an episode that was active. Split out so the two
54
- * ending paths — a successful probe and a config that dropped the pair — name the primary
55
- * pair and duration the same way. A pre-trip state (no episode) has no ended event. */
56
- function emitFallbackEnded(ctx, prior, primary, now, wasIn) {
57
- if (!wasIn)
58
- return;
59
- logEventBestEffort(ctx.root, {
60
- source: ctx.role,
61
- type: "model_fallback_ended",
62
- provider: primary.provider,
63
- model: primary.model,
64
- durationMs: now - (prior.since || now),
65
- });
66
- }
@@ -1,24 +0,0 @@
1
- import { loadConfigSafe } from "../../config/config.js";
2
- import { createTeamConfigKeyEditor } from "./tui-editor-config.js";
3
- /** The Ctrl+Y claude subscription-window reserve editor on the Activity view's shared prompt
4
- * line — the terminal twin of the dashboard Settings `Claude window reserve
5
- * (backends.claude.windowReserve)` field and `tumwater config set claudeWindowReserve`. Enter
6
- * writes the top-level `claudeWindowReserve`
7
- * through the shared setConfigKey/unsetConfigKey family, so validateConfig's claudeWindowReserve
8
- * block owns the value's truth. The team scope is the only scope, so the editor takes the line
9
- * on the Activity view (view 0) alone; a History pane keeps Ctrl+Y for its per-role Summary
10
- * toggle, which runs after this arm. */
11
- export function createClaudeReserveEditor(session) {
12
- return createTeamConfigKeyEditor(session, {
13
- mode: "claudeReserve",
14
- openCtrl: "y",
15
- configKey: "claudeWindowReserve",
16
- prefill: (s) => {
17
- const existing = loadConfigSafe(s.root).config?.claudeWindowReserve;
18
- return existing !== undefined ? JSON.stringify(existing) : "";
19
- },
20
- openFlash: "edit the team's claude subscription-window reserve (backends.claude.windowReserve, a JSON object like {\"seven_day\":0.8}; empty clears): Enter to save, Esc to cancel",
21
- clearedFlash: "claude window reserve cleared",
22
- setFlash: (text) => `claude window reserve set to ${text}`,
23
- });
24
- }
@@ -1,27 +0,0 @@
1
- import { loadConfigSafe } from "../../config/config.js";
2
- import { createTeamConfigKeyEditor } from "./tui-editor-config.js";
3
- /** The Ctrl+P provider-concurrency-caps editor on the Activity view's shared prompt line — the
4
- * terminal twin of the dashboard Settings `Provider concurrency caps (JSON)` field and
5
- * `tumwater config set maxConcurrentPerProvider`, completing the run-concurrency knob set
6
- * `tumwater doctor`'s checkProviderContention points at. Enter writes the top-level
7
- * `maxConcurrentPerProvider` through the shared setConfigKey/unsetConfigKey family the CLI and
8
- * the dashboard use — so setConfigKey JSON-parses the typed text and validateConfig's map block
9
- * owns the value's truth — and Esc, Ctrl+P again, and Ctrl+T cancel by restoring the prompt
10
- * draft. Mutually exclusive with the other editors on the one prompt line via the session's
11
- * held registry. The Team scope is the only scope, so the editor takes the line on the Activity
12
- * view (view 0) alone; a role's Transcript keeps Ctrl+P for its pause editor, which runs after
13
- * this arm. */
14
- export function createProviderCapsEditor(session) {
15
- return createTeamConfigKeyEditor(session, {
16
- mode: "providerCaps",
17
- openCtrl: "p",
18
- configKey: "maxConcurrentPerProvider",
19
- prefill: (s) => {
20
- const existing = loadConfigSafe(s.root).config?.maxConcurrentPerProvider;
21
- return existing !== undefined ? JSON.stringify(existing) : "";
22
- },
23
- openFlash: "edit the team's provider concurrency caps (maxConcurrentPerProvider, a JSON object like {\"anthropic\":2}; empty clears): Enter to save, Esc to cancel",
24
- clearedFlash: "provider concurrency caps cleared",
25
- setFlash: (text) => `provider concurrency caps set to ${text}`,
26
- });
27
- }
File without changes
File without changes