tumwater 0.4.2 → 0.4.3

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 (305) 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} +25 -12
  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 +1 -8
  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 +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -1,19 +1,17 @@
1
1
  /** The budget/tier FALLBACK side of the config views, split out of config-views.ts: how the
2
2
  * team falls back at the daily spend cap — which free pair each tier runs on
3
- * (resolveTierFallbacks and its BORROW_ORDER), the per-role fallback config a model-failure
4
- * or cap episode hands a role (fallbackRoleConfig, applyFallbackModel), and the surfaces
5
- * that name those pairs (seamsInUse, modelFallbackView, tierFallbackLabels). The per-seam
3
+ * (resolveTierFallbacks and its BORROW_ORDER), the per-role fallback config a cap episode
4
+ * hands a role (applyFallbackModel), and the surfaces
5
+ * that name those pairs (seamsInUse, tierFallbackLabels). The per-seam
6
6
  * resolution these build on — how one role/reviewer/resolver provider/model/thinking resolves
7
7
  * (configForRole, reviewConfig, withModelOverrides, …) — stays in config-views.ts, so the
8
8
  * one-way dependency runs fallback → seam and never back. Pure, like its sibling: no I/O and
9
9
  * no caching. */
10
10
  import { MODEL_TIERS, } from "./config-schema.js";
11
- import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
11
+ import { formatModelSelector, parseModelSelector, routeModel } from "./config-model-selector.js";
12
12
  import { modelPairName } from "../budget/budget.js";
13
13
  import { DIRECTOR_ROLE } from "../roles/roles.js";
14
- import { baseRoleOf } from "../roles/runner-ids.js";
15
14
  import { enabledRoleIds } from "./config.js";
16
- import { modelFallbackActive } from "../runner/model-fallback.js";
17
15
  import { FALLBACK_REVIEW_TIMEOUT_S, REVIEW_TIMEOUT_S, configForRole, isTierName, pairOverrides, resolvedModelFields, resolverConfig, reviewConfig, reviewRunConfig, roleSeamTier, topTierBackend, topTierSelector, withModelOverrides, } from "./config-views.js";
18
16
  /** The resolved fields of one fallback step, keyed by the seam it routes to (PLANS.md "Mixed
19
17
  * claude + pi fleets, part 2/4"): the backend is the step's own, else its tier primary's
@@ -80,6 +78,9 @@ const BORROW_ORDER = {
80
78
  default: ["default", "strong", "small"],
81
79
  strong: ["strong", "default"],
82
80
  };
81
+ /** Exported for imageRerouteTarget (src/agent/image-input.ts), which walks a rerouting role's
82
+ * tier through the same borrow order the budget fallback uses (skipping the tier itself). */
83
+ export { BORROW_ORDER };
83
84
  /** A tier's OWN fallback chain, first step first — what the `fallback` key declares at that
84
85
  * tier's map entry (a selector or a list of them), or (default only) the string/list form of
85
86
  * `fallback` or the legacy `fallbackModel` object (a one-element chain). Empty when the tier
@@ -163,27 +164,10 @@ export function resolveTierFallbacks(config, usable) {
163
164
  };
164
165
  return { small: resolve("small"), default: resolve("default"), strong: resolve("strong") };
165
166
  }
166
- /** The config a role's runs use while a model-fallback episode is active (PLANS.md "Model
167
- * failure fallback, part 1/2"): the role's tier pair from resolveTierFallbacks (the tier's own
168
- * pair, else BORROW_ORDER's borrow), shaped like configForRole — provider/model/thinking over
169
- * the role's own and top-level values, plus the role's minRunIntervalSeconds. Null when the
170
- * tier resolves to pause (no fallback configured), so the feature stays off. Pure. */
171
- export function fallbackRoleConfig(config, role) {
172
- const tier = roleSeamTier(config, role);
173
- const pair = resolveTierFallbacks(config, () => true)[tier].pair;
174
- if (pair === null)
175
- return null;
176
- const rc = config.roles[baseRoleOf(role)];
177
- return {
178
- ...withModelOverrides(config, tier, pairOverrides(pair)),
179
- minRunIntervalSeconds: rc?.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
180
- };
181
- }
182
167
  /** Every seam the team can spawn, each with its label: every enabled role's resolved config
183
168
  * (configForRole), the review gate when it is on (reviewRunConfig), the conflict resolver
184
169
  * (resolverConfig), the director (a catalog role, named explicitly so it is never missed), and
185
- * each tier's budget fallback (resolveTierFallbacks) — the model-failure fallback
186
- * (fallbackRoleConfig) runs the same tier pair, so one enumeration covers it. Deduplicated by
170
+ * each tier's budget fallback (resolveTierFallbacks). Deduplicated by
187
171
  * label + routed pair so a seam reached through several surfaces is listed once. Read by the
188
172
  * startup gate's agent list, doctor's pair sweep (and so its binary/credential/version checks
189
173
  * and routing table), and the live-edit reload's newly-routed-backend check — no surface can
@@ -211,25 +195,6 @@ export function seamsInUse(config) {
211
195
  }
212
196
  return [...seams.values()];
213
197
  }
214
- /** Build a ModelFallbackView for an episode, or null while the episode is not running the
215
- * fallback (no episode, or a due probe on the primary). role-view.ts's
216
- * RoleViewPayload.modelFallback and status/status-data.ts's per-role row both build this
217
- * shape, so one function keeps the two surfaces from disagreeing about the pair, its start,
218
- * and its reason. `now` is the caller's clock (the status snapshot passes its own poll
219
- * clock), so a row and its poll agree on what "active" means. */
220
- export function modelFallbackView(episode, config, role, now = Date.now()) {
221
- if (episode === undefined || !modelFallbackActive(episode, now))
222
- return null;
223
- const episodeConfig = fallbackRoleConfig(config, role);
224
- if (episodeConfig === null)
225
- return null;
226
- return {
227
- ...resolvedModelFields(episodeConfig.provider, episodeConfig.model, episodeConfig.thinking),
228
- ...(episodeConfig.agent !== undefined && episodeConfig.agent !== "pi" ? { backend: episodeConfig.agent } : {}),
229
- since: episode.since,
230
- reason: episode.reason,
231
- };
232
- }
233
198
  /** The per-tier fallback labels the observability surfaces render (part 7b/8): tier →
234
199
  * `provider/model[ (from <tier>)]` for every tier the resolution gives a pair, in tier order.
235
200
  * The `(from …)` suffix marks a borrowed pair — a strong-tier borrow is the fact an operator
@@ -300,6 +265,74 @@ function freeRoleModels(config, free) {
300
265
  return [id, isTierName(rc?.model) ? stripBackend(rc) : stripModel(rc)];
301
266
  }));
302
267
  }
268
+ /** A pair's selector as a tier-map entry. Keeps pi's provider; a non-pi pair's model is
269
+ * already the whole id. On a team whose backend is not pi — or a pair routed off pi — the
270
+ * entry names the pair's backend, so the team's own backend cannot take a pi pair's id as
271
+ * its own: a bare selector when the pair rides the team backend and it is pi, else an
272
+ * object entry naming the pair's backend. */
273
+ function pairSelectorOf(config, pair) {
274
+ if (!pair)
275
+ return undefined;
276
+ const selector = formatModelSelector({
277
+ provider: pair.backend === undefined ? pair.provider : undefined,
278
+ model: pair.model ?? "",
279
+ thinking: pair.backend === undefined ? pair.thinking : undefined,
280
+ });
281
+ if (pair.backend === undefined && (config.backend ?? "pi") === "pi")
282
+ return formatModelSelector({ provider: pair.provider, model: pair.model ?? "", thinking: pair.thinking });
283
+ return {
284
+ backend: pair.backend ?? "pi",
285
+ model: selector,
286
+ ...(pair.thinking !== undefined ? { thinking: pair.thinking } : {}),
287
+ };
288
+ }
289
+ /** What a free seam keeps running: the tier's own primary selector — the backend namespace a
290
+ * resolved seam has cleared, plus the ambient thinking level `topTierSelector` alone omits.
291
+ * A moved seam gets its pair selector, which already carries that ambient level. */
292
+ function freeTierEntry(config, tier, seam) {
293
+ const own = topTierSelector(config, tier);
294
+ // A tier entry that names a backend keeps it as an object entry: a bare selector string
295
+ // would route by namespace and drop the backend the entry chose.
296
+ const backend = topTierBackend(config, tier);
297
+ if (own !== undefined) {
298
+ const thinking = own.thinking ?? config.thinking;
299
+ if (backend !== undefined)
300
+ return {
301
+ backend,
302
+ model: formatModelSelector({ ...own, thinking: undefined }),
303
+ ...(thinking !== undefined ? { thinking } : {}),
304
+ };
305
+ else
306
+ return formatModelSelector({ ...own, thinking });
307
+ }
308
+ // A tier with no top-level selector — `agent: "claude"` riding its agent default —
309
+ // has no own selector to carry the level, so name the routed default plus it.
310
+ if (seam.model !== undefined) {
311
+ const routed = { provider: seam.provider, model: seam.model };
312
+ return backend !== undefined
313
+ ? {
314
+ backend,
315
+ model: formatModelSelector(routed),
316
+ ...(seam.thinking !== undefined ? { thinking: seam.thinking } : {}),
317
+ }
318
+ : formatModelSelector({ ...routed, thinking: seam.thinking });
319
+ }
320
+ return undefined;
321
+ }
322
+ /** A config with no resolved tier map installs the single fallback pair at the top level,
323
+ * keeping every other value (and the reviewer's time budget floor) as in the resolved-map
324
+ * path's legacy peer. */
325
+ function legacyPairConfig(config, pair) {
326
+ return {
327
+ ...config,
328
+ ...(pair.backend !== undefined || config.backend !== undefined ? { backend: pair.backend ?? "pi" } : {}),
329
+ provider: pair.backend === undefined ? pair.provider : undefined,
330
+ model: pair.model,
331
+ thinking: pair.thinking,
332
+ review: fallbackReview(config.review),
333
+ roles: stripRoleModels(config.roles, false),
334
+ };
335
+ }
303
336
  /** The config as seen by a role running on the free fallback model
304
337
  * (plans/fallback-model.md): the fallback's provider/model/thinking installed as the top-level
305
338
  * values AND every per-role and reviewer model override dropped, so that EVERY seam that could
@@ -323,26 +356,6 @@ function freeRoleModels(config, free) {
323
356
  * model, while every priced seam still lands on its tier's fallback pair. */
324
357
  export function applyFallbackModel(config, resolved, free) {
325
358
  if (resolved) {
326
- // A pair's selector keeps pi's provider; a non-pi pair's model is already the whole id. On a
327
- // team whose backend is not pi — or a pair routed off pi — the entry names the pair's
328
- // backend, so the team's own backend cannot take a pi pair's id as its own.
329
- const selectorOf = (f) => {
330
- const pair = f.pair;
331
- if (!pair)
332
- return undefined;
333
- const selector = formatModelSelector({
334
- provider: pair.backend === undefined ? pair.provider : undefined,
335
- model: pair.model ?? "",
336
- thinking: pair.backend === undefined ? pair.thinking : undefined,
337
- });
338
- if (pair.backend === undefined && (config.backend ?? "pi") === "pi")
339
- return formatModelSelector({ provider: pair.provider, model: pair.model ?? "", thinking: pair.thinking });
340
- return {
341
- backend: pair.backend ?? "pi",
342
- model: selector,
343
- ...(pair.thinking !== undefined ? { thinking: pair.thinking } : {}),
344
- };
345
- };
346
359
  const model = {};
347
360
  const stepCaps = {};
348
361
  for (const tier of MODEL_TIERS) {
@@ -351,37 +364,12 @@ export function applyFallbackModel(config, resolved, free) {
351
364
  // seam gets its pair selector, which already carries that ambient level.
352
365
  const seam = withModelOverrides(config, tier, {});
353
366
  if (free?.(seam)) {
354
- const own = topTierSelector(config, tier);
355
- // A tier entry that names a backend keeps it as an object entry: a bare selector string
356
- // would route by namespace and drop the backend the entry chose.
357
- const backend = topTierBackend(config, tier);
358
- if (own !== undefined) {
359
- const thinking = own.thinking ?? config.thinking;
360
- if (backend !== undefined)
361
- model[tier] = {
362
- backend,
363
- model: formatModelSelector({ ...own, thinking: undefined }),
364
- ...(thinking !== undefined ? { thinking } : {}),
365
- };
366
- else
367
- model[tier] = formatModelSelector({ ...own, thinking });
368
- }
369
- // A tier with no top-level selector — `agent: "claude"` riding its agent default —
370
- // has no own selector to carry the level, so name the routed default plus it.
371
- else if (seam.model !== undefined) {
372
- const routed = { provider: seam.provider, model: seam.model };
373
- model[tier] =
374
- backend !== undefined
375
- ? {
376
- backend,
377
- model: formatModelSelector(routed),
378
- ...(seam.thinking !== undefined ? { thinking: seam.thinking } : {}),
379
- }
380
- : formatModelSelector({ ...routed, thinking: seam.thinking });
381
- }
367
+ const own = freeTierEntry(config, tier, seam);
368
+ if (own !== undefined)
369
+ model[tier] = own;
382
370
  continue;
383
371
  }
384
- const sel = selectorOf(resolved[tier]);
372
+ const sel = pairSelectorOf(config, resolved[tier].pair);
385
373
  if (sel !== undefined)
386
374
  model[tier] = sel;
387
375
  const cap = resolved[tier].pair?.maxDailyCostUsd;
@@ -401,13 +389,5 @@ export function applyFallbackModel(config, resolved, free) {
401
389
  const pair = fallbackPair(config);
402
390
  if (!pair)
403
391
  return config;
404
- return {
405
- ...config,
406
- ...(pair.backend !== undefined || config.backend !== undefined ? { backend: pair.backend ?? "pi" } : {}),
407
- provider: pair.backend === undefined ? pair.provider : undefined,
408
- model: pair.model,
409
- thinking: pair.thinking,
410
- review: fallbackReview(config.review),
411
- roles: stripRoleModels(config.roles, false),
412
- };
392
+ return legacyPairConfig(config, pair);
413
393
  }
@@ -96,6 +96,22 @@ export const AT_LEAST_ONE = { ok: (n) => n >= 1, what: "a number of at least 1"
96
96
  * duration rules (config-validation.ts) and checkMinRunInterval (config-keys.ts), so the
97
97
  * dashboard's interval editor cannot drift from the schema's bound. */
98
98
  export const MAX_DURATION_SECONDS = 2147483;
99
+ /** The seconds-valued duration rules, bounded by MAX_DURATION_SECONDS so a `×1000` delay stays
100
+ * inside node's signed-32-bit range. One home for the family, so the whole-gate section
101
+ * (config-validation.ts) and the per-role section (config-validation-roles.ts) name the same
102
+ * rules instead of each declaring DURATION_NON_NEGATIVE for itself. */
103
+ export const DURATION_POSITIVE = {
104
+ ok: (n) => n > 0 && n <= MAX_DURATION_SECONDS,
105
+ what: `a number greater than 0, at most ${MAX_DURATION_SECONDS}`,
106
+ };
107
+ export const DURATION_NON_NEGATIVE = {
108
+ ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
109
+ what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS}`,
110
+ };
111
+ export const DURATION_OR_DISABLED = {
112
+ ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
113
+ what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS} (0 disables)`,
114
+ };
99
115
  /** Shared known-role guard for the per-role config sections (`roles.<id>` entries and
100
116
  * `maxDailyCostUsdPerRole.<id>` keys): an id outside the role catalog and customRoles cannot
101
117
  * work — a `roles` typo spawns a phantom role that errors every run forever; a caps typo
@@ -120,6 +136,53 @@ export function checkStringField(problems, obj, prefix, key, allowEmpty = true)
120
136
  problems.push(`${prefix}${key} must not be empty (got ${show(v)})`);
121
137
  });
122
138
  }
139
+ function checkModelSiblingFields(problems, obj, prefix) {
140
+ for (const key of MODEL_TRIPLE_KEYS)
141
+ if (key !== "model")
142
+ checkStringField(problems, obj, prefix, key, false);
143
+ const t = obj.thinking;
144
+ if (isNonBlankString(t) && !THINKING_LEVELS.has(t))
145
+ problems.push(`${prefix}thinking must be one of ${[...THINKING_LEVELS].join(", ")} (got ${show(t)})${typoSuffix(t, [...THINKING_LEVELS])}`);
146
+ }
147
+ function checkTierMapModel(problems, obj, prefix, m) {
148
+ checkKnownKeys(m, TIER_MAP_KEYS, `${prefix}model`, problems);
149
+ if (obj.provider !== undefined)
150
+ problems.push(`${prefix}model as a map by tier cannot coexist with ${prefix ? prefix : "the legacy top-level "}provider — move the provider into each tier's model id (got provider ${show(obj.provider)})`);
151
+ for (const [tier, v] of Object.entries(m))
152
+ // An object entry (`{ backend?, model?, thinking? }`) is validated by validateConfig.
153
+ if (!isNonBlankString(v) && !isJsonObject(v))
154
+ problems.push(`${prefix}model.${tier} must be a non-empty model id (got ${show(v)})`);
155
+ }
156
+ function whatModelMustBe(modelShape) {
157
+ return modelShape === "tier-map"
158
+ ? "a model id or a map by tier (small, default, strong)"
159
+ : modelShape === "tier-name"
160
+ ? "a model id or a tier name (small, default, strong)"
161
+ : "a string";
162
+ }
163
+ function checkStringModel(problems, obj, prefix, m, modelShape) {
164
+ if (typeof m !== "string") {
165
+ problems.push(`${prefix}model must be ${whatModelMustBe(modelShape)} (got ${show(m)})`);
166
+ return;
167
+ }
168
+ if (m.trim() === "") {
169
+ problems.push(`${prefix}model must not be empty (got ${show(m)})`);
170
+ return;
171
+ }
172
+ if (MODEL_TIERS.includes(m)) {
173
+ if (modelShape === "tier-name")
174
+ return; // A valid tier reference.
175
+ problems.push(`${prefix}model must name a model id — tier names are valid only as roles.<id>.model or review.model values (got ${show(m)})`);
176
+ return;
177
+ }
178
+ if (modelShape === "tier-name" && !m.includes("/") && obj.provider === undefined)
179
+ suggestTierFix(problems, prefix, m);
180
+ }
181
+ function suggestTierFix(problems, prefix, m) {
182
+ const tier = suggestClosest(m, MODEL_TIERS);
183
+ if (tier !== null)
184
+ problems.push(`${prefix}model ${show(m)} is not a tier name or a model id pi can resolve${didYouMean(tier)}`);
185
+ }
123
186
  /** Every model-override section validates the same provider/model/thinking triple the same
124
187
  * way: empty strings are rejected (see checkStringField) and thinking is value-checked —
125
188
  * pi only recognizes THINKING_LEVELS and silently drops anything else, so a typo would run
@@ -137,49 +200,15 @@ export function checkStringField(problems, obj, prefix, key, allowEmpty = true)
137
200
  * places that accept it, so a misplaced `"model": "strong"` is an error naming its fix instead of a
138
201
  * bare model pattern pi can never resolve. */
139
202
  export function checkModelTripleField(problems, obj, prefix, modelShape = "selector") {
140
- for (const key of MODEL_TRIPLE_KEYS)
141
- if (key !== "model")
142
- checkStringField(problems, obj, prefix, key, false);
143
- const t = obj.thinking;
144
- if (isNonBlankString(t) && !THINKING_LEVELS.has(t))
145
- problems.push(`${prefix}thinking must be one of ${[...THINKING_LEVELS].join(", ")} (got ${show(t)})${typoSuffix(t, [...THINKING_LEVELS])}`);
203
+ checkModelSiblingFields(problems, obj, prefix);
146
204
  const m = obj.model;
147
205
  if (m === undefined)
148
206
  return;
149
207
  if (modelShape === "tier-map" && isJsonObject(m)) {
150
- checkKnownKeys(m, TIER_MAP_KEYS, `${prefix}model`, problems);
151
- if (obj.provider !== undefined)
152
- problems.push(`${prefix}model as a map by tier cannot coexist with ${prefix ? prefix : "the legacy top-level "}provider — move the provider into each tier's model id (got provider ${show(obj.provider)})`);
153
- for (const [tier, v] of Object.entries(m))
154
- // An object entry (`{ backend?, model?, thinking? }`) is validated by validateConfig.
155
- if (!isNonBlankString(v) && !isJsonObject(v))
156
- problems.push(`${prefix}model.${tier} must be a non-empty model id (got ${show(v)})`);
157
- return;
158
- }
159
- if (typeof m !== "string") {
160
- const what = modelShape === "tier-map"
161
- ? "a model id or a map by tier (small, default, strong)"
162
- : modelShape === "tier-name"
163
- ? "a model id or a tier name (small, default, strong)"
164
- : "a string";
165
- problems.push(`${prefix}model must be ${what} (got ${show(m)})`);
208
+ checkTierMapModel(problems, obj, prefix, m);
166
209
  return;
167
210
  }
168
- if (m.trim() === "") {
169
- problems.push(`${prefix}model must not be empty (got ${show(m)})`);
170
- return;
171
- }
172
- if (MODEL_TIERS.includes(m)) {
173
- if (modelShape === "tier-name")
174
- return; // A valid tier reference.
175
- problems.push(`${prefix}model must name a model id — tier names are valid only as roles.<id>.model or review.model values (got ${show(m)})`);
176
- return;
177
- }
178
- if (modelShape === "tier-name" && !m.includes("/") && obj.provider === undefined) {
179
- const tier = suggestClosest(m, MODEL_TIERS);
180
- if (tier !== null)
181
- problems.push(`${prefix}model ${show(m)} is not a tier name or a model id pi can resolve${didYouMean(tier)}`);
182
- }
211
+ checkStringModel(problems, obj, prefix, m, modelShape);
183
212
  }
184
213
  /** Validate one numeric field against a NumberRule when present. */
185
214
  export function checkNumberField(problems, obj, prefix, key, rule) {
@@ -11,7 +11,7 @@
11
11
  * writers and the read side show. */
12
12
  import { BACKEND_KEYS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, CLAUDE_AGENT_KEYS, CODEX_BACKEND_KEYS, REVIEW_KEYS, ROLE_ENTRY_KEYS, TIER_MAP_KEYS, TOP_LEVEL_KEYS } from "./config-schema.js";
13
13
  import { typoSuffix } from "../text/suggest.js";
14
- import { MAX_DURATION_SECONDS, POSITIVE_INTEGER, show } from "./config-field-checks.js";
14
+ import { MAX_DURATION_SECONDS, show } from "./config-field-checks.js";
15
15
  import { parseQuietHours } from "../scheduling/quiet-hours.js";
16
16
  /** One definition of "a valid daily budget cap" (the TUI's Ctrl+B editor and the GUI's
17
17
  * /api/budget endpoint both run their input through it): a finite number of 0 or more —
@@ -31,18 +31,6 @@ export function checkDailyBudgetUsd(value, key = "maxDailyCostUsd") {
31
31
  return `${key} must be a number of 0 or more, at most ${Number.MAX_SAFE_INTEGER}, 0 disables (got ${show(value)})`;
32
32
  return null;
33
33
  }
34
- /** One definition of "a valid per-provider concurrency cap" (plans/provider-concurrency.md
35
- * §1): a positive integer, accepted for any (possibly unknown) provider name — a fallback's
36
- * provider may only come into use later, so the key is not checked against a catalog. The one
37
- * validation both `config set maxConcurrentPerProvider.<provider>` and validateConfig show,
38
- * so they cannot drift apart on what a valid cap is. `key` names the config key in the error,
39
- * defaulting to the map name; the dotted map passes `maxConcurrentPerProvider.<provider>`
40
- * so a rejected entry names the key actually set. */
41
- export function checkProviderConcurrencyCap(value, key = "maxConcurrentPerProvider") {
42
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1)
43
- return `${key} must be ${POSITIVE_INTEGER.what} (got ${show(value)})`;
44
- return null;
45
- }
46
34
  /** One definition of "a valid quietHours window" (src/scheduling/quiet-hours.ts owns the format):
47
35
  * "HH:MM-HH:MM" in local time, wrapping permitted, empty means off. parseQuietHours's
48
36
  * message is the one wording every surface (validateConfig, `config set`, the TUI/GUI
@@ -78,12 +66,9 @@ export const PER_KEY_VALIDATORS = {
78
66
  };
79
67
  /** The per-role maps `config get/set` reaches with dotted keys: each names ONE entry
80
68
  * (`maxDailyCostUsdPerRole.<role>`, `quietHoursPerRole.<role>`) that merges into the
81
- * existing map, so raising one role's cap never requires re-typing another's. The
82
- * `maxConcurrentPerProvider` map is dotted the same way (`maxConcurrentPerProvider.<provider>`);
83
- * its keys are provider names, not role ids, so its per-key validation skips the role-catalog
84
- * check the per-role maps apply. The per-key value validator runs before the write, the same
85
- * screening a bare top-level key gets. */
86
- export const DOTTED_MAP_KEYS = ["maxDailyCostUsdPerRole", "quietHoursPerRole", "maxConcurrentPerProvider"];
69
+ * existing map, so raising one role's cap never requires re-typing another's. The per-key
70
+ * value validator runs before the write, the same screening a bare top-level key gets. */
71
+ export const DOTTED_MAP_KEYS = ["maxDailyCostUsdPerRole", "quietHoursPerRole"];
87
72
  /** The fixed-shape top-level sections `config get/set/unset` reaches one field of with a
88
73
  * two-segment key (`review.model`, `bootstrap.untilPlansDone`), each with its field list. */
89
74
  export const DOTTED_SECTION_FIELDS = {
@@ -116,7 +101,7 @@ export function parseConfigKey(key) {
116
101
  // `roles.<id>.<field>` names one role field). The id is shape-checked only — routing owns
117
102
  // which agent ids exist (part 4b); a typo'd field is refused here with the valid ones named.
118
103
  if (parts.length === 3 && first === "backends" && second !== undefined && second !== "" && third !== undefined) {
119
- // configDir/windowReserve are claude/codex-only (CLAUDE_AGENT_KEYS / CODEX_BACKEND_KEYS):
104
+ // configDir is claude/codex-only (CLAUDE_AGENT_KEYS / CODEX_BACKEND_KEYS):
120
105
  // a config set backends.pi.configDir would write a key nothing reads, so it is refused
121
106
  // everywhere but claude and codex.
122
107
  const validKeys = second === "claude" ? CLAUDE_AGENT_KEYS : second === "codex" ? CODEX_BACKEND_KEYS : BACKEND_KEYS;
@@ -5,9 +5,9 @@ import { bestEffortEventLogger, TUMWATER_SOURCE } from "../events/events.js";
5
5
  import { RoleRunner } from "../runner/runner.js";
6
6
  import { configPath } from "../paths.js";
7
7
  import { errorMessage } from "../text/text.js";
8
- import { backendsInUse, seedOpencodeCatalog } from "../gates/startup-gate.js";
8
+ import { backendsInUse, seedOpencodeCatalog } from "../gates/startup-gates.js";
9
9
  import { seedClaudeAccount } from "../agent/claude/claude-account.js";
10
- import { seedCodexAccount, readCodexAccount } from "../agent/codex/codex-account.js";
10
+ import { codexApiKeyRefusalMessage, seedCodexAccount, readCodexAccount } from "../agent/codex/codex-account.js";
11
11
  import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
12
12
  /** A live config reloader over the given wiring; poll it once per scheduler cycle (see
13
13
  * LiveConfigReload for the contract). */
@@ -26,10 +26,6 @@ export function newLiveConfigReload(deps) {
26
26
  // The cap last applied to the semaphore (live-resized on each reload), so a change logs
27
27
  // exactly one event per distinct value — not once per poll.
28
28
  let lastMaxConcurrent = Math.max(1, deps.config.maxConcurrent);
29
- // The per-provider caps last applied to the semaphore (live-resized on each reload), so a
30
- // change logs exactly one event per distinct value — not once per poll. Seeded from the
31
- // startup config, so the first poll of an unchanged file emits nothing.
32
- let lastKeyCaps = deps.config.maxConcurrentPerProvider ?? {};
33
29
  // Roots this process has already attempted a live-edit claude-account seed for (see the
34
30
  // reload block below), so repeated polls of a config that keeps a claude seam cannot respawn
35
31
  // `claude auth status` on every cycle.
@@ -86,7 +82,7 @@ export function newLiveConfigReload(deps) {
86
82
  if (codexLiveRefused)
87
83
  return true;
88
84
  codexLiveRefused = true;
89
- warn("refusing live edit: Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`");
85
+ warn(`refusing live edit: ${codexApiKeyRefusalMessage("<home>")}`);
90
86
  return true;
91
87
  }
92
88
  // A poll that did not refuse clears the warn-once marker so a later re-entry into the
@@ -154,7 +150,7 @@ export function newLiveConfigReload(deps) {
154
150
  }
155
151
  }
156
152
  }
157
- // Live-resize the concurrency cap and the per-provider key caps.
153
+ // Live-resize the concurrency cap.
158
154
  function resizeConcurrency(nextConfig) {
159
155
  // Live-resize the concurrency cap: a mid-run edit changes how many pi runs execute
160
156
  // concurrently within this poll — no restart. Growing admits already-queued runs;
@@ -165,26 +161,6 @@ export function newLiveConfigReload(deps) {
165
161
  emit({ source: TUMWATER_SOURCE, type: "max_concurrent_changed", from: lastMaxConcurrent, to: newMaxConcurrent });
166
162
  lastMaxConcurrent = newMaxConcurrent;
167
163
  }
168
- // Live-resize the per-provider caps (plans/provider-concurrency.md §7): an edit
169
- // re-applies the whole map to the pool, and each changed provider fires its own
170
- // `provider_concurrency_changed` event. setKeyCaps never preempts — shrinking only
171
- // withholds future grants on that key until releases drain it under the new cap.
172
- const nextKeyCaps = nextConfig.maxConcurrentPerProvider ?? {};
173
- deps.semaphore.setKeyCaps(nextKeyCaps);
174
- for (const provider of new Set([...Object.keys(lastKeyCaps), ...Object.keys(nextKeyCaps)])) {
175
- const from = lastKeyCaps[provider];
176
- const to = nextKeyCaps[provider];
177
- if (from !== to) {
178
- emit({
179
- source: TUMWATER_SOURCE,
180
- type: "provider_concurrency_changed",
181
- provider,
182
- from: from ?? 0,
183
- to: to ?? 0,
184
- });
185
- }
186
- }
187
- lastKeyCaps = nextKeyCaps;
188
164
  }
189
165
  // Sync the runner list to newly enabled roles and warn on enable/disable.
190
166
  function syncRunners(nextConfig) {
@@ -65,12 +65,12 @@ const BACKEND_DEFAULT_MODELS = {
65
65
  // belongs in tumwater.json, not in code.
66
66
  opencode: {},
67
67
  // codex's tier defaults (plans/codex-backend.md decision 4), from the catalog's own
68
- // descriptions (design finding 14): strong is `gpt-6-astra`, default is `gpt-6.1-sol` (Codex's
69
- // own default), small is `gpt-6-luna`. The director runs the strong tier, as on claude.
68
+ // descriptions (design finding 14): strong is `gpt-6.1-sol`, default and small are
69
+ // `gpt-6-luna`. The director runs the strong tier, as on claude.
70
70
  codex: {
71
71
  small: "gpt-6-luna",
72
- default: "gpt-6.1-sol",
73
- strong: "gpt-6-astra",
72
+ default: "gpt-6-luna",
73
+ strong: "gpt-6.1-sol",
74
74
  },
75
75
  };
76
76
  /** The backend and concrete model a resolved seam's provider/model pair routes to