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
@@ -2,23 +2,20 @@
2
2
  * event-and-verdict half (src/gates/budget-gates.ts) so the two halves each have a plain
3
3
  * function boundary and their own tests (the same split stage-path-checks.ts made of
4
4
  * stage-check.ts). This module owns everything pollBudgetGate does from its `onFallback` seam
5
- * through the end of the function: the seam/backends walk and used-up window moves
6
- * (`windowOut`, `movedBackends`, `windowMove`, `windowTiers`, `windowResumedBackends`,
7
- * `windowResumed`), the motion keys (`resolvedKey`, `movedKey`, `fromKey`), the two view
8
- * derivations (`fallbackConfig`/`directorConfig`), the `movedTiers`/`applyFallbackModel`
9
- * application, `chainInfo`, and the `pairs` handback list. Its one entry reads the inputs
10
- * pollBudgetGate's first half already resolved and returns the view fields `BudgetGatePoll`
11
- * carries, so pollBudgetGate keeps its call signature and src/gates/gate-polls.ts's
12
- * destructure of it is unchanged. The plan references the moved comments carry (part 5b/8,
13
- * part 5c/8, 6a/7) hold here. */
5
+ * through the end of the function: the seam/backends walk, the motion key (`resolvedKey`,
6
+ * `fromKey`), the fallback view derivation (`fallbackConfig`), `chainInfo`, and the `pairs`
7
+ * handback list. It no longer moves any tier for a used-up subscription window (part 5d/6): only
8
+ * a spent `maxDailyCostUsd` triggers a move, so the view's one job is the cap's free-seam
9
+ * install. Its one entry reads the inputs pollBudgetGate's first half already resolved and
10
+ * returns the view fields `BudgetGatePoll` carries, so pollBudgetGate keeps its call signature
11
+ * and src/gates/gate-polls.ts's destructure of it is unchanged. The plan references the moved
12
+ * comments carry (part 5b/8, part 5c/8, 6a/7) hold here. */
14
13
  import { MODEL_TIERS, } from "../config/config-schema.js";
15
14
  import { modelPairName, pairDailyCost } from "../budget/budget.js";
16
15
  import { fallbackServingPair, } from "../budget/fallback-breaker.js";
17
- import { applyFallbackModel, seamsInUse, tierChainSteps, tierFallbackLabels, } from "../config/config-fallbacks.js";
18
- import { DIRECTOR_ROLE } from "../roles/roles.js";
19
- import { enabledRoleIds } from "../config/config.js";
20
- import { formatModelSelector, seamAgent } from "../config/model-selector.js";
21
- import { configForRole, isTierName, reviewConfig, roleSeamTier, withModelOverrides, } from "../config/config-views.js";
16
+ import { applyFallbackModel, tierChainSteps, } from "../config/config-fallbacks.js";
17
+ import { seamAgent } from "../config/config-model-selector.js";
18
+ import { withModelOverrides } from "../config/config-views.js";
22
19
  import { piPairFree, seamFree } from "../pi/pi-models.js";
23
20
  import { dayAt } from "../text/datetime.js";
24
21
  /** Can the account not take extra usage: some window is rejected, or the newest non-reset
@@ -47,225 +44,76 @@ export function subscriptionBackendOf(x, accounts) {
47
44
  const account = accounts(id);
48
45
  return account?.billing === "subscription" ? id : null;
49
46
  }
50
- /** Resolve the window move: whether the fallback holds, which subscription backends' used-up
51
- * windows move, and the motion keys the view re-derives on. Read by every phase that needs
52
- * the move to decide what to install. Returns the move's facts; `movedKey` stays local. */
53
- function resolveWindowMove(a) {
54
- const { config: liveConfig, accounts, windowOutFor, priceResolved, pairNames, reached, } = a;
55
- // While the fallback holds, roles run under the derived view — the free pair installed
56
- // top-level and every per-role/reviewer model override dropped, so no seam can reach a priced
57
- // model. A breaker-demoted fallback keeps the view too: its gate is `paused`, but a run parked
58
- // in the semaphore when it tripped, the half-open probe, and the merges must still run on the
59
- // free pair — a demotion must never promote them to the priced model the cap already spent.
60
- const onFallback = reached && pairNames.length > 0;
61
- // A used-up window moves the subscription jobs of the backend whose window stands to their
62
- // tier's fallback below the cap too; with no fallback to move to, nothing changes and that
63
- // backend's hold applies. Every backend with a subscription seam in the config counts
64
- // (seamsInUse enumerates them: every enabled role, the reviewer, the resolver, the director,
65
- // and every tier fallback step), so a used-up window with no seam on that backend (or no
66
- // fallback) moves nothing.
67
- const seamBackends = new Set();
68
- for (const seam of seamsInUse(liveConfig)) {
69
- if (seam.agent === undefined)
70
- continue;
71
- const acc = accounts(seam.agent);
72
- if (acc?.billing === "subscription")
73
- seamBackends.add(seam.agent);
74
- }
75
- // The used-up windows behind this poll's move, keyed by backend: a backend counts only when
76
- // a subscription seam of it is in the config, its own window is used up, and there is a
77
- // fallback pair to move to. `windowOut` names only those backends; a healthy or absent
78
- // backend's window is untouched.
79
- const windowOut = {};
80
- const movedBackends = new Set();
81
- for (const backend of seamBackends) {
82
- const win = windowOutFor(backend);
83
- if (win !== null && pairNames.length > 0) {
84
- windowOut[backend] = win;
85
- movedBackends.add(backend);
86
- }
87
- }
88
- const windowMove = movedBackends.size > 0;
89
- // The resolved pairs are part of the key: a priced step crossing its own daily cap changes
90
- // the resolution without a config or window change, and the view must follow it.
91
- const resolvedKey = MODEL_TIERS.map((t) => {
92
- const rp = priceResolved[t].pair;
93
- return rp ? modelPairName(rp) : "-";
94
- }).join(",");
95
- // The set of window-moved backends is part of the key too (not just the `w` flag): a second
96
- // backend's window can come or go while the move still stands, and the view must re-derive
97
- // for the roles it newly blocks or frees.
98
- const movedKey = [...movedBackends].sort().join("+");
99
- const fromKey = `${windowMove ? "w" : ""}${reached ? "c" : ""}|${movedKey}|${resolvedKey}`;
100
- return { onFallback, windowOut, movedBackends, windowMove, resolvedKey, fromKey };
101
- }
102
- /** Install `state.fallbackConfig` when the fallback or a window move changed the view key,
103
- * including the pinned-role and pinned-reviewer escapes. Read by the pinned loops. */
104
- function installFallbackConfig(a, o) {
105
- const { state, config: liveConfig, accounts, providers, catalog, priceResolved, reached } = a;
106
- const { onFallback, windowMove, windowOut, movedBackends, fromKey } = o;
107
- if ((onFallback || windowMove) && (state.from !== liveConfig || state.fromKey !== fromKey)) {
47
+ /** Install `state.fallbackConfig` when the fallback changed the view key: at the cap, the free
48
+ * pair installed top-level and every per-role/reviewer model override dropped, so no seam can
49
+ * reach a priced model. A breaker-demoted fallback keeps the view too: its gate is `paused`,
50
+ * but a run parked in the semaphore when it tripped, the half-open probe, and the merges must
51
+ * still run on the free pair — a demotion must never promote them to the priced model the cap
52
+ * already spent. */
53
+ function installFallbackConfig(a, fromKey) {
54
+ const { state, config: liveConfig, providers, catalog, accounts, priceResolved, reached } = a;
55
+ if (reached && (state.from !== liveConfig || state.fromKey !== fromKey)) {
108
56
  state.from = liveConfig;
109
57
  state.fromKey = fromKey;
110
- // A job keeps its own model while that model is available: below the cap every seam but a
111
- // subscription one whose own backend's window is used up; at the cap it must also be free.
112
- const seamBlocked = (seam) => {
113
- const b = subscriptionBackendOf(seam, accounts);
114
- return b !== null && windowOut[b] !== undefined;
115
- };
116
- const available = (seam) => !seamBlocked(seam) && (!reached || seamFree(providers, seam, accounts, catalog));
117
- const view = applyFallbackModel(liveConfig, priceResolved, available);
118
- // A role pinned to a subscription backend under a tier of another backend has no tier pair
119
- // to fall onto — dropping its override would put it on the tier's priced primary — so it
120
- // keeps its pin and that backend's hold stops it, as before the window move.
121
- if (movedBackends.size > 0) {
122
- for (const role of enabledRoleIds(liveConfig)) {
123
- const tier = roleSeamTier(liveConfig, role);
124
- const pin = liveConfig.roles[role];
125
- const roleBackend = subscriptionBackendOf(configForRole(liveConfig, role), accounts);
126
- const tierBackend = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
127
- if (pin && roleBackend !== null && roleBackend !== tierBackend)
128
- view.roles[role] = pin;
129
- }
130
- // A reviewer pinned to a subscription backend under such a tier would likewise end up on
131
- // the tier's priced primary; it goes to the tier's fallback pair instead (a held reviewer
132
- // would park the roles a pre-merge review gates).
133
- const rm = liveConfig.review.model;
134
- const reviewTier = isTierName(rm) ? rm : "strong";
135
- const reviewPair = priceResolved[reviewTier].pair;
136
- const reviewBackend = subscriptionBackendOf(reviewConfig(liveConfig), accounts);
137
- const reviewTierBackend = subscriptionBackendOf(withModelOverrides(liveConfig, reviewTier, {}), accounts);
138
- // Only the reviewer's own backend's window counts: a reviewer on a healthy backend must
139
- // stay put even while another backend's window moves its tier.
140
- if (reviewPair &&
141
- reviewBackend !== null &&
142
- windowOut[reviewBackend] !== undefined &&
143
- reviewBackend !== reviewTierBackend)
144
- view.review = {
145
- ...view.review,
146
- backend: reviewPair.backend ?? "pi",
147
- provider: undefined,
148
- model: formatModelSelector({
149
- provider: reviewPair.backend === undefined ? reviewPair.provider : undefined,
150
- model: reviewPair.model ?? "",
151
- thinking: reviewPair.thinking,
152
- }),
153
- thinking: undefined,
154
- };
155
- }
156
- state.fallbackConfig = view;
157
- }
158
- }
159
- /** The tiers the window move put on a fallback pair: those whose own seam is a subscription
160
- * backend whose window stands. */
161
- function deriveWindowTiers(a, windowOut, windowMove) {
162
- const { config: liveConfig, accounts, priceResolved } = a;
163
- // The tiers the window move put on a fallback pair: those whose own seam is a subscription
164
- // backend whose window stands.
165
- const windowTiers = {};
166
- if (windowMove) {
167
- const moved = new Set();
168
- for (const tier of MODEL_TIERS) {
169
- const b = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
170
- if (b !== null && windowOut[b] !== undefined)
171
- moved.add(tier);
172
- }
173
- const labels = tierFallbackLabels(priceResolved);
174
- for (const tier of MODEL_TIERS)
175
- if (moved.has(tier) && labels[tier] !== undefined)
176
- windowTiers[tier] = labels[tier];
177
- }
178
- return windowTiers;
179
- }
180
- /** Install the director's `state.directorFrom`/`directorKey`/`directorView` and return whether
181
- * the director moved. */
182
- function installDirectorFallback(a, resolvedKey, windowOut) {
183
- const { state, config: liveConfig, accounts, pairNames, directorWindowOutFor, priceResolved } = a;
184
- // The director follows availability, not the cap: while the window move stands and its own
185
- // model job is a subscription backend, its tier's resolved pair replaces the live config (only
186
- // subscription jobs of a used-up window are unavailable, so a priced director primary keeps
187
- // its model over the cap). With no fallback (windowMove false) it stays on the live config and
188
- // the scheduler holds it on that backend's hold.
189
- const directorBackend = subscriptionBackendOf(configForRole(liveConfig, DIRECTOR_ROLE), accounts);
190
- const directorMoved = directorBackend !== null && pairNames.length > 0 && directorWindowOutFor(directorBackend) !== null;
191
- if (!directorMoved)
192
- state.directorFrom = null;
193
- else if (state.directorFrom !== liveConfig || state.directorKey !== resolvedKey) {
194
- state.directorFrom = liveConfig;
195
- state.directorKey = resolvedKey;
196
- state.directorView = applyFallbackModel(liveConfig, priceResolved, (seam) => {
197
- const b = subscriptionBackendOf(seam, accounts);
198
- return b === null || windowOut[b] === undefined;
199
- });
58
+ // At the cap only free seams are available (a subscription claude seam is free money), so
59
+ // every role/reviewer override that resolves to a priced seam drops to the fallback pair.
60
+ const available = (seam) => seamFree(providers, seam, accounts, catalog);
61
+ state.fallbackConfig = applyFallbackModel(liveConfig, priceResolved, available);
200
62
  }
201
- return directorMoved;
202
63
  }
203
- /** Fold `state.prevWindowMoved` into the resumed-backends signal and record the new move. */
204
- function deriveWindowResumed(a, movedBackends) {
205
- const { state } = a;
206
- const windowResumedBackends = [...state.prevWindowMoved].filter((b) => !movedBackends.has(b));
207
- const windowResumed = windowResumedBackends.length > 0;
208
- state.prevWindowMoved = movedBackends;
209
- return { windowResumed, windowResumedBackends };
210
- }
211
- /** The tiers `applyFallbackModel` moved: those whose own seam fails the `available` test above.
212
- * Feeds the `chain` block's `moved` set. */
213
- function deriveMovedTiers(a, windowOut, windowMove) {
214
- const { config: liveConfig, accounts, providers, catalog, reached } = a;
64
+ /** The tiers `applyFallbackModel` moved at the cap: those whose own seam fails the `available`
65
+ * test above (a priced seam under a spent cap). Feeds the `chain` block's `moved` set. */
66
+ function deriveMovedTiers(a) {
67
+ const { config: liveConfig, providers, accounts, catalog, reached } = a;
215
68
  const movedTiers = new Set();
216
- // The tiers `applyFallbackModel` moved: those whose own seam fails the `available` test above.
217
69
  for (const tier of MODEL_TIERS) {
218
70
  const seam = withModelOverrides(liveConfig, tier, {});
219
- const b = subscriptionBackendOf(seam, accounts);
220
- const out = (windowMove && b !== null && windowOut[b] !== undefined) || (reached && !seamFree(providers, seam, accounts, catalog));
221
- if (out)
71
+ if (reached && !seamFree(providers, seam, accounts, catalog))
222
72
  movedTiers.add(tier);
223
73
  }
224
74
  return movedTiers;
225
75
  }
226
76
  /** Derive the fallback view for one poll from the verdict half's already-resolved inputs
227
- * (`priceResolved`, `servingResolved`, `chainPairs`, `pairNames`) and the `reached` flag
228
- * and the shared reads (`state`, `config`, `accounts`, `windowOutFor`/`directorWindowOutFor`,
229
- * `providers`, `catalog`), writing the cross-poll view state (`from`/`fromKey`/`fallbackConfig`,
230
- * `directorFrom`/`directorKey`/`directorView`, `prevWindowMoved`) and returning the view fields
231
- * pollBudgetGate hands back. */
77
+ * (`priceResolved`, `servingResolved`, `chainPairs`, `pairNames`) and the `reached` flag and
78
+ * the shared reads (`state`, `config`, `accounts`, `providers`, `catalog`), writing the
79
+ * cross-poll view state (`from`/`fromKey`/`fallbackConfig`) and returning the view fields
80
+ * pollBudgetGate hands back. The director always keeps the live config (part 5d/6): with the
81
+ * window move gone no input moves it, so `directorConfig` is the live config by construction. */
232
82
  export function budgetFallbackView(a) {
233
- const { state, config: liveConfig, now, states, providers, catalog, accounts, windowOutFor, priceResolved, servingResolved, chainPairs, } = a;
234
- const { onFallback, windowOut, movedBackends, windowMove, resolvedKey, fromKey } = resolveWindowMove(a);
235
- installFallbackConfig(a, { onFallback, windowMove, windowOut, movedBackends, fromKey });
236
- const windowTiers = deriveWindowTiers(a, windowOut, windowMove);
237
- const directorMoved = installDirectorFallback(a, resolvedKey, windowOut);
238
- const { windowResumed, windowResumedBackends } = deriveWindowResumed(a, movedBackends);
239
- const movedTiers = deriveMovedTiers(a, windowOut, windowMove);
83
+ const { state, config: liveConfig, reached, priceResolved } = a;
84
+ const onFallback = reached && a.pairNames.length > 0;
85
+ // The resolved pairs are part of the key: a priced step crossing its own daily cap changes
86
+ // the resolution without a config or window change, and the view must follow it.
87
+ const resolvedKey = MODEL_TIERS.map((t) => {
88
+ const rp = priceResolved[t].pair;
89
+ return rp ? modelPairName(rp) : "-";
90
+ }).join(",");
91
+ const fromKey = `${reached ? "c" : ""}|${resolvedKey}`;
92
+ installFallbackConfig(a, fromKey);
93
+ const movedTiers = deriveMovedTiers(a);
240
94
  const chain = chainInfo({
241
95
  config: liveConfig,
242
- resolved: servingResolved,
96
+ resolved: a.servingResolved,
243
97
  moved: movedTiers,
244
98
  breakers: state.breakers,
245
- states,
246
- now,
247
- windowOutFor,
248
- accounts,
249
- isPriced: (p) => !piPairFree(providers, p, accounts, catalog),
99
+ states: a.states,
100
+ now: a.now,
101
+ accounts: a.accounts,
102
+ isPriced: (p) => !piPairFree(a.providers, p, a.accounts, a.catalog),
250
103
  });
251
104
  return {
252
105
  onFallback,
253
106
  chain,
254
- roleConfig: onFallback || windowMove ? state.fallbackConfig : liveConfig,
255
- pairs: distinctPairs(priceResolved, chainPairs),
256
- windowOut,
257
- windowTiers,
258
- windowResumed,
259
- windowResumedBackends,
260
- directorConfig: directorMoved ? state.directorView : liveConfig,
107
+ roleConfig: onFallback ? state.fallbackConfig : liveConfig,
108
+ pairs: distinctPairs(priceResolved, a.chainPairs),
109
+ directorConfig: liveConfig,
261
110
  };
262
111
  }
263
112
  /** The `chain` block: for each tier this poll moved off its primary, the step it runs on and
264
113
  * why the position before it cannot run (plans/fallback-chain.md "Events and surfaces").
265
114
  * `moved` names the tiers the poll moved; a tier the gate pauses (`pair: null`) gets no entry.
266
115
  * The reason is read from the position just before the active step — the previous chain step,
267
- * or the tier's primary at step 0 — in the order demoted, step-cap, window/reserve, overage,
268
- * cap. */
116
+ * or the tier's primary at step 0 — in the order demoted, step-cap, overage, cap. */
269
117
  export function chainInfo(a) {
270
118
  const out = {};
271
119
  const steps = tierChainSteps(a.config);
@@ -274,47 +122,56 @@ export function chainInfo(a) {
274
122
  const r = a.resolved[tier];
275
123
  if (!a.moved.has(tier) || r.pair === null)
276
124
  continue;
277
- const own = steps.filter((s) => s.tier === (r.from ?? tier)).map((s) => s.pair);
278
- const idx = r.step ?? 0;
279
- const prior = idx > 0 ? own[idx - 1] : null;
280
- // The backend whose used-up window this tier's chain reads for the window/reserve reason:
281
- // the prior step's own backend when a prior step is subscription, else the primary's.
282
- const priorSub = prior ? subscriptionBackendOf(prior, a.accounts) : null;
283
- const primarySub = subscriptionBackendOf(withModelOverrides(a.config, tier, {}), a.accounts);
284
- const windowBackend = (prior !== null ? priorSub : null) ?? primarySub;
285
- const info = { pair: modelPairName(r.pair), step: idx + 1, of: own.length, reason: "cap" };
286
- const priorName = prior ? modelPairName(prior) : "";
287
- if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
288
- info.reason = "demoted";
289
- }
290
- else if (prior?.maxDailyCostUsd !== undefined &&
291
- pairDailyCost(a.states, priorName, a.now) >= prior.maxDailyCostUsd) {
292
- info.reason = "step-cap";
293
- info.returnsAt = midnight;
294
- }
295
- else if (windowBackend !== null && a.windowOutFor(windowBackend) !== null) {
296
- const win = a.windowOutFor(windowBackend);
297
- info.reason = win.reserve ? "reserve" : "window";
298
- info.returnsAt = win.resetsAt;
299
- }
300
- else if (prior &&
301
- subscriptionBackendOf(prior, a.accounts) !== null &&
302
- prior.extraUsage &&
303
- a.accounts("claude") &&
304
- claudeOverageRefused(a.accounts("claude"))) {
305
- info.reason = "overage";
306
- }
307
- else {
308
- info.returnsAt = midnight;
309
- }
310
- if (r.pair.maxDailyCostUsd !== undefined && a.isPriced(r.pair)) {
311
- info.spentUsd = pairDailyCost(a.states, modelPairName(r.pair), a.now);
312
- info.capUsd = r.pair.maxDailyCostUsd;
313
- }
125
+ const position = movedTierPosition(steps, tier, r);
126
+ const info = {
127
+ pair: modelPairName(r.pair),
128
+ step: position.step,
129
+ of: position.of,
130
+ ...movedTierReason(a, position.prior, position.priorName, midnight),
131
+ };
132
+ Object.assign(info, movedTierSpend(a, r.pair));
314
133
  out[tier] = info;
315
134
  }
316
135
  return out;
317
136
  }
137
+ /** The step a moved tier runs on and the pair just before it: the tier's own chain steps
138
+ * (`steps.filter((s) => s.tier === (r.from ?? tier))`) resolved to their pairs, positioned at
139
+ * `r.step ?? 0`, with the previous step's pair (or null at step 0). Answers "which step does
140
+ * this moved tier run on, and which pair precedes it?" (plans/fallback-chain.md "Events and
141
+ * surfaces"). */
142
+ function movedTierPosition(steps, tier, r) {
143
+ const own = steps.filter((s) => s.tier === (r.from ?? tier)).map((s) => s.pair);
144
+ const idx = r.step ?? 0;
145
+ const prior = idx > 0 ? own[idx - 1] : null;
146
+ return { step: idx + 1, of: own.length, prior, priorName: prior ? modelPairName(prior) : "" };
147
+ }
148
+ /** Why the position before the active step cannot run, in INV-BN1's fixed order (demoted,
149
+ * step-cap, overage, cap). A demoted or overage result carries no `returnsAt`; a step-cap or
150
+ * cap result pins it to `midnight` (INV-BN2: local midnight only for step-cap and cap).
151
+ * Answers "why can't the position before the active step run, and when does it come back?". */
152
+ function movedTierReason(a, prior, priorName, midnight) {
153
+ if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
154
+ return { reason: "demoted" };
155
+ }
156
+ if (prior?.maxDailyCostUsd !== undefined && pairDailyCost(a.states, priorName, a.now) >= prior.maxDailyCostUsd) {
157
+ return { reason: "step-cap", returnsAt: midnight };
158
+ }
159
+ if (prior &&
160
+ subscriptionBackendOf(prior, a.accounts) !== null &&
161
+ prior.extraUsage &&
162
+ a.accounts("claude") &&
163
+ claudeOverageRefused(a.accounts("claude"))) {
164
+ return { reason: "overage" };
165
+ }
166
+ return { reason: "cap", returnsAt: midnight };
167
+ }
168
+ /** What the active step has spent against its own cap: both keys absent unless the step is a
169
+ * priced capped one (INV-BN2). Answers "what has the active step spent against its cap?". */
170
+ function movedTierSpend(a, pair) {
171
+ if (pair.maxDailyCostUsd === undefined || !a.isPriced(pair))
172
+ return {};
173
+ return { spentUsd: pairDailyCost(a.states, modelPairName(pair), a.now), capUsd: pair.maxDailyCostUsd };
174
+ }
318
175
  /** The distinct non-null fallback pairs a resolution hands out (tier order, first occurrence
319
176
  * first) — the handback match list: a reopen hands back every in-flight run on any of
320
177
  * them (part 5c/8's per-tier handback). */
@@ -1,10 +1,10 @@
1
1
  import { MODEL_TIERS } from "../config/config-schema.js";
2
- import { publishInfoFields } from "../team/orchestrator-info.js";
2
+ import { publishInfoFields } from "../team/team-orchestrator-info.js";
3
3
  import { DIRECTOR_ROLE } from "../roles/roles.js";
4
4
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
5
5
  import { fallbackDemotion } from "./fallback-breaker.js";
6
- import { gateRoleConfig, runOnPair, subscriptionBackendOf, } from "../gates/budget-gates.js";
7
- import { configForRole, roleSeamTier, withModelOverrides } from "../config/config-views.js";
6
+ import { gateRoleConfig, runOnPair, } from "../gates/budget-gates.js";
7
+ import { roleSeamTier } from "../config/config-views.js";
8
8
  function publishBudgetWiring(root, info, budget, view) {
9
9
  // Publish what observers cannot derive themselves: every pair's demotion — a per-tier
10
10
  // fallback can demote a pair the legacy single `fallback` never names, and a dashboard that
@@ -54,51 +54,31 @@ function publishBudgetWiring(root, info, budget, view) {
54
54
  }
55
55
  function assignRunnerBudgetConfig(runners, liveConfig, view) {
56
56
  // The director keeps the live config — an explicit human prompt outranks the
57
- // autonomous-spend cap (gateRoleConfig). Assigned every poll (not only on transitions)
57
+ // (gateRoleConfig). Assigned every poll (not only on transitions)
58
58
  // so a runner created mid-gate, or one left behind by a broken-file poll that skipped
59
59
  // the reload, can never run on the wrong model.
60
60
  for (const r of runners) {
61
- r.config = gateRoleConfig(r.role, liveConfig, view.roleConfig, view.directorConfig);
61
+ r.config = gateRoleConfig(r.role, liveConfig, view.roleConfig);
62
+ // An image reroute must never spend past the daily cap: while the gate has the team on
63
+ // its free fallback pairs, a reroute looks only among those pairs (plans/image-input.md
64
+ // "Reroute target" "Regime"). Set beside the config assignment so the scheduler's regime
65
+ // and the roles' never disagree.
66
+ r.budgetFallbackActive = view.onFallback;
62
67
  }
63
68
  }
64
- function handBackFallbackRuns(root, runners, liveConfig, accounts, view) {
69
+ function handBackFallbackRuns(root, runners, view) {
65
70
  // The budget reopened: a run that started on a fallback pair keeps it until it ends, so hand
66
71
  // those runs back — abort them resumably (session and worktree edits kept) and let their next
67
72
  // run continue the same session on the primary, whose config the assignment above already
68
73
  // installed (PLANS.md 2026-09-30). Per-tier fallbacks put different roles on different pairs
69
- // (part 5c/8), so the match runs over EVERY pair the price-based resolution hands out, not only
70
- // one. A run started on the primary keeps running, the director is exempt, and the merges (the
71
- // slot's own runs) are untouched. One event per pair names who was handed back, so the resulting
72
- // aborted runs read as the budget reopening, not as unexplained failures. The subscription
73
- // window reset (below the cap): the same hand-back for the pairs the window move used. A window
74
- // that resets while the cap still holds hands back nothing.
75
- const handBack = view.resumed || (view.windowResumedBackends.length > 0 && !view.budgetActive);
76
- // The window handback names the backend whose window resumed (the entry's goal) — not the
77
- // handed-back pair's own backend: for claude-only the fallback pair carries no backend (it would
78
- // read undefined), and with two subscription backends it would name the opposite one. A pair
79
- // belongs to those resumed backends whose tiers' price-resolved pair it is.
80
- const resumedBackendSet = new Set(view.windowResumedBackends);
81
- const resumedBackendForPair = (leftPair) => {
82
- for (const tier of MODEL_TIERS) {
83
- const p = view.priceResolved[tier]?.pair;
84
- if (p && p.provider === leftPair.provider && p.model === leftPair.model) {
85
- const b = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
86
- if (b !== null && resumedBackendSet.has(b))
87
- return b;
88
- }
89
- }
90
- return undefined;
91
- };
92
- if (handBack && view.pairs.length > 0) {
93
- // A cap reopen while the window is still used up leaves the window's tiers on their
94
- // fallback pairs: handing those runs back would abort them to restart on the same pair.
95
- const stillMoved = Object.keys(view.windowTiers).flatMap((t) => {
96
- const p = view.priceResolved[t].pair;
97
- return p ? [p] : [];
98
- });
74
+ // (part 5c/8), so the match runs over EVERY pair the price-based resolution hands out, not
75
+ // only one. A run started on the primary keeps running, the director is exempt, and the merges
76
+ // (the slot's own runs) are untouched. One event per pair names who was handed back, so the
77
+ // resulting aborted runs read as the budget reopening, not as unexplained failures. The
78
+ // subscription-window reset below the cap handed back too, but that move is gone (part 5d/6):
79
+ // only the cap reopen hands back now.
80
+ if (view.resumed && view.pairs.length > 0) {
99
81
  for (const leftPair of view.pairs) {
100
- if (stillMoved.some((p) => p.provider === leftPair.provider && p.model === leftPair.model))
101
- continue;
102
82
  const matching = runners.filter((r) => r.role !== DIRECTOR_ROLE && runOnPair(r.runModelPair(), leftPair));
103
83
  if (matching.length > 0) {
104
84
  logEventBestEffort(root, {
@@ -107,9 +87,6 @@ function handBackFallbackRuns(root, runners, liveConfig, accounts, view) {
107
87
  roles: matching.map((r) => r.role),
108
88
  provider: leftPair.provider,
109
89
  model: leftPair.model,
110
- ...(view.resumed
111
- ? {}
112
- : { reason: "window", backend: resumedBackendForPair(leftPair) }),
113
90
  });
114
91
  for (const r of matching)
115
92
  r.handBackRun();
@@ -120,43 +97,30 @@ function handBackFallbackRuns(root, runners, liveConfig, accounts, view) {
120
97
  /** Perform the budget gate's wiring for one poll, exactly as `pollTeamGates` did inline:
121
98
  * publish the demotions and the switch/serve info fields, log the one `fallback_switched`
122
99
  * transition event per tier change, assign every runner's config and cap flag, and hand
123
- * in-flight fallback runs back on a cap reopen or window reset. */
100
+ * in-flight fallback runs back on a cap reopen. */
124
101
  export function applyBudgetGateWiring(input) {
125
- const { root, runners, liveConfig, info, accounts, budget, view } = input;
102
+ const { root, runners, liveConfig, info, budget, view } = input;
126
103
  publishBudgetWiring(root, info, budget, view);
127
104
  assignRunnerBudgetConfig(runners, liveConfig, view);
128
- handBackFallbackRuns(root, runners, liveConfig, accounts, view);
105
+ handBackFallbackRuns(root, runners, view);
129
106
  }
130
- /** The per-tier budget pause subject sets (part 5c/8), computed exactly as `pollTeamGates` did
107
+ /** The per-tier budget pause subject set (part 5c/8), computed exactly as `pollTeamGates` did
131
108
  * inline: `budgetPausedRoles` holds every role whose model tier resolved to no usable free pair
132
- * while the cap is reached, and `windowMovedRoles` the role whose own subscription backend owns
133
- * a used-up window's moved tier. The director is exempt (as under every autonomous gate), a role
134
- * below the cap is held only when it was window-moved, and the demoted pair's probe pierces the
135
- * set for its own tier's roles only — probeRoles in the orchestrator's scheduling pass. */
109
+ * while the cap is reached. The window-moved set is gone (part 5d/6): below the cap nothing
110
+ * pauses on a usage window anymore. The director is exempt (as under every autonomous gate),
111
+ * and the demoted pair's probe pierces the set for its own tier's roles only — probeRoles in
112
+ * the orchestrator's scheduling pass. */
136
113
  export function budgetPausedSubjects(input) {
137
- const { runners, liveConfig, accounts, budgetActive, servingResolved, windowTiers } = input;
114
+ const { runners, liveConfig, budgetActive, servingResolved } = input;
138
115
  const budgetPausedRoles = new Set();
139
- const windowMovedRoles = new Set();
140
- const movedTiers = Object.keys(windowTiers);
141
- // The backend whose used-up window moved a given tier: the tier's own seam's backend — the same
142
- // read budget-gates made to put it in `windowTiers`. A role on a moved tier whose own backend is
143
- // DIFFERENT (a healthy backend riding the same tier) keeps its pair: only the role whose own
144
- // backend owns the out window is moved.
145
- const tierMovedBackend = (tier) => subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
146
116
  for (const r of runners) {
147
117
  if (r.role === DIRECTOR_ROLE)
148
118
  continue;
149
119
  const tier = roleSeamTier(liveConfig, r.role);
150
- // A role pinned to the moved tier's own subscription backend keeps it through the window move
151
- // (it is not on a pair); a role on the same tier pinning a healthy backend does not.
152
- if (movedTiers.includes(tier) &&
153
- subscriptionBackendOf(configForRole(liveConfig, r.role), accounts) === tierMovedBackend(tier))
154
- windowMovedRoles.add(r.role);
155
- // Below the cap only a window-moved role can sit on a demoted fallback.
156
- if (!budgetActive && !windowMovedRoles.has(r.role))
120
+ if (!budgetActive)
157
121
  continue;
158
122
  if (servingResolved[tier].pair === null)
159
123
  budgetPausedRoles.add(r.role);
160
124
  }
161
- return { budgetPausedRoles, windowMovedRoles };
125
+ return { budgetPausedRoles };
162
126
  }
@@ -152,11 +152,10 @@ export function modelPairName(p) {
152
152
  * `strong` tiers resolved to a usable free pair (resolveTierFallbacks over the full usable
153
153
  * predicate — price AND breaker serving) — plus whether the review gate is on. The gate is
154
154
  * `paused` when `default` resolves to pause (nothing for the team's bulk to run on), or when
155
- * review is on and `strong` does (nothing could merge for the roles a pre-merge review gates, so
156
- * their authoring runs are wasted work — a `review.afterMerge` role's is not);
157
- * `fallback` otherwise, with the tiers that did resolve running their pairs and the rest held
158
- * per role. The single definition of the gate: the orchestrator enforces it and both dashboards
159
- * display it, so what an operator sees is what the scheduler is doing. */
155
+ * review is on and `strong` does; `fallback` otherwise, with the tiers that did resolve running
156
+ * their pairs and the rest held per role. This is the value the dashboards display; the
157
+ * orchestrator's role holds come from the per-tier set `budgetPausedRoles` (a tier whose own
158
+ * pair did not resolve), not from a reviewer-derived team-wide pause. */
160
159
  export function budgetGate(reached, tiers, reviewOn) {
161
160
  if (!reached)
162
161
  return "open";
@@ -114,8 +114,8 @@ export function recordFallbackRun(b, ranOn, result, probe, now, policy = FALLBAC
114
114
  return { ...b, failures, probeAt: now + policy.cooldownMs, cooldownMs: policy.cooldownMs, probing: false };
115
115
  }
116
116
  /** What orchestrator.json publishes while the breaker holds the fallback demoted
117
- * (team/orchestrator-info.ts's OrchestratorInfo.fallbackDemoted). Observers need it because the
118
- * price check alone would advertise the demoted pair as a working fallback —
117
+ * (team/team-orchestrator-info.ts's OrchestratorInfo.fallbackDemoted). Observers need
118
+ * it because the price check alone would advertise the demoted pair as a working fallback —
119
119
  * status/status-data.ts nulls the snapshot's `fallback` from it and `tumwater doctor` names it.
120
120
  * Undefined while the fallback is trusted or not engaged. */
121
121
  export function fallbackDemotion(b) {