tumwater 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,40 +1,43 @@
1
- // vocabulary: plain
2
1
  /** The budget/tier FALLBACK side of the config views, split out of config-views.ts: how the
3
2
  * team falls back at the daily spend cap — which free pair each tier runs on
4
3
  * (resolveTierFallbacks and its BORROW_ORDER), the per-role fallback config a model-failure
5
- * or cap episode hands a loop (fallbackRoleConfig, applyFallbackModel), and the surfaces
4
+ * or cap episode hands a role (fallbackRoleConfig, applyFallbackModel), and the surfaces
6
5
  * that name those pairs (seamsInUse, modelFallbackView, tierFallbackLabels). The per-seam
7
6
  * resolution these build on — how one role/reviewer/resolver provider/model/thinking resolves
8
7
  * (configForRole, reviewConfig, withModelOverrides, …) — stays in config-views.ts, so the
9
8
  * one-way dependency runs fallback → seam and never back. Pure, like its sibling: no I/O and
10
9
  * no caching. */
11
10
  import { MODEL_TIERS, } from "./config-schema.js";
12
- import { formatModelSelector, isAgentId, parseModelSelector } from "./model-selector.js";
11
+ import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
13
12
  import { modelPairName } from "../budget/budget.js";
14
13
  import { DIRECTOR_ROLE } from "../roles/roles.js";
15
- import { baseRoleOf } from "../roles/loop-ids.js";
14
+ import { baseRoleOf } from "../roles/runner-ids.js";
16
15
  import { enabledRoleIds } from "./config.js";
17
- import { modelFallbackActive } from "../loop/model-fallback.js";
18
- import { FALLBACK_REVIEW_TIMEOUT_S, REVIEW_TIMEOUT_S, configForRole, isTierName, resolvedModelFields, resolverConfig, reviewConfig, reviewRunConfig, roleSeamTier, topTierSelector, withModelOverrides, } from "./config-views.js";
16
+ import { modelFallbackActive } from "../runner/model-fallback.js";
17
+ import { FALLBACK_REVIEW_TIMEOUT_S, REVIEW_TIMEOUT_S, configForRole, isTierName, pairOverrides, resolvedModelFields, resolverConfig, reviewConfig, reviewRunConfig, roleSeamTier, topTierBackend, topTierSelector, withModelOverrides, } from "./config-views.js";
18
+ /** The resolved fields of one fallback step, keyed by the seam it routes to (PLANS.md "Mixed
19
+ * claude + pi fleets, part 2/4"): the backend is the step's own, else its tier primary's
20
+ * resolved backend (the tier entry's, else the top level's, else pi). A pair routed to a
21
+ * non-pi backend carries that backend as `backend` and its name as `provider` — the key the
22
+ * budget gate's handback (src/budget/budget-wiring.ts's runOnPair against the run's
23
+ * seamHoldKey capture, runner-run.ts's setRunPair) matches on — and the id passes to it as
24
+ * written (routeModel); a
25
+ * pi pair keeps its own provider (a bare pi selector keeps undefined). */
26
+ function seamKeyedPairFields(config, tier, provider, model, thinking, stepBackend) {
27
+ const backend = stepBackend ?? topTierBackend(config, tier) ?? config.backend ?? "pi";
28
+ const routed = model === undefined ? undefined : routeModel(provider, model, "pi", tier, backend);
29
+ return {
30
+ ...resolvedModelFields(backend !== "pi" ? backend : provider, routed?.model, thinking),
31
+ ...(backend !== "pi" ? { backend } : {}),
32
+ };
33
+ }
19
34
  /** The resolved model fields of a pure `fallback` selector string: parsed as a pure selector
20
35
  * (no legacy provider in scope), with the ambient top-level thinking when the selector
21
36
  * carries none. fallbackPair's map and string branches and tierOwnChain share this
22
37
  * assembly, so all three read one rule for how a `fallback` selector resolves. */
23
- function fallbackSelectorFields(selector, config) {
38
+ function fallbackSelectorFields(selector, config, tier, backend) {
24
39
  const parsed = parseModelSelector(selector);
25
- return resolvedModelFields(seamKeyedFallbackProvider(config, parsed.provider), parsed.model, parsed.thinking ?? config.thinking);
26
- }
27
- /** A fallback pair's provider keyed by the seam it routes to (PLANS.md "Mixed claude + pi
28
- * fleets, part 2/4"): a pair that routes to a non-pi backend carries that backend's name as
29
- * its provider — `claude/claude-sonnet-5-5` and a bare `claude-sonnet-5-5` on a claude
30
- * team both read `claude` — so the budget gate's handback (gate-polls.ts's tickOnPair
31
- * against the tick's seamHoldKey capture, loop-tick.ts's setTickPair) matches a tick on
32
- * either selector form. The same routing rule as routeModel: a provider naming an agent is
33
- * that agent, no provider runs on the config's `agent`, anything else is pi, which keeps
34
- * its own provider (a bare pi selector keeps undefined, exactly as before). */
35
- function seamKeyedFallbackProvider(config, provider) {
36
- const agent = provider === undefined ? config.backend : isAgentId(provider) ? provider : "pi";
37
- return agent !== "pi" ? agent : provider;
40
+ return seamKeyedPairFields(config, tier, parsed.provider, parsed.model, parsed.thinking ?? config.thinking, backend);
38
41
  }
39
42
  /** The team-level fallback pair: the `default` tier's resolved pair under
40
43
  * resolveTierFallbacks' borrow order, or null when the tier resolves to pause. The
@@ -61,10 +64,12 @@ function legacyFallbackPair(config) {
61
64
  // The top-level model is a selector too (parsed the same way withModelOverrides does), so a
62
65
  // fallback field that omits model borrows the parsed id, never the raw selector string.
63
66
  const topSel = topTierSelector(config, "default");
64
- const provider = seamKeyedFallbackProvider(config, fbo.provider ?? topSel?.provider ?? config.provider);
67
+ const provider = fbo.provider ?? topSel?.provider ?? config.provider;
65
68
  const model = fbo.model ?? topSel?.model;
66
69
  const thinking = fbo.thinking ?? topSel?.thinking ?? config.thinking;
67
- return resolvedModelFields(provider, model, thinking);
70
+ // The legacy object's own `provider` is a pi provider (claude takes none), so naming one
71
+ // keeps the pair on pi whatever the team's backend; without one it inherits like any step.
72
+ return seamKeyedPairFields(config, "default", provider, model, thinking, fbo.provider !== undefined ? "pi" : undefined);
68
73
  }
69
74
  /** The order a tier borrows other tiers' own fallbacks in, after trying its own: small
70
75
  * climbs (small → default → strong), default keeps near itself (default → strong → small),
@@ -77,7 +82,7 @@ const BORROW_ORDER = {
77
82
  };
78
83
  /** Exported for imageRerouteTarget (src/agent/image-input.ts), which walks a rerouting role's
79
84
  * tier through the same borrow order the budget fallback uses (skipping the tier itself), and
80
- * for the doctor's `image input` check naming the tier a text-only loop reroutes to. */
85
+ * for the doctor's `image input` check naming the tier a text-only role reroutes to. */
81
86
  export { BORROW_ORDER };
82
87
  /** A tier's OWN fallback chain, first step first — what the `fallback` key declares at that
83
88
  * tier's map entry (a selector or a list of them), or (default only) the string/list form of
@@ -104,7 +109,7 @@ function tierOwnChain(config, tier) {
104
109
  if (legacy === null)
105
110
  return [];
106
111
  const primaryClaude = withModelOverrides(config, tier, {}).agent === "claude";
107
- return [legacy.provider === "claude" && primaryClaude ? { ...legacy, extraUsage: true } : legacy];
112
+ return [legacy.backend === "claude" && primaryClaude ? { ...legacy, extraUsage: true } : legacy];
108
113
  }
109
114
  if (raw === undefined || raw === "pause")
110
115
  return [];
@@ -115,9 +120,9 @@ function tierOwnChain(config, tier) {
115
120
  .filter((s) => s !== "pause")
116
121
  .map((s) => {
117
122
  const step = typeof s === "string" ? { model: s } : s;
118
- const fields = fallbackSelectorFields(step.model, config);
119
- const extraUsage = fields.provider === "claude" && claudeSeen;
120
- if (fields.provider === "claude")
123
+ const fields = fallbackSelectorFields(step.model, config, tier, step.backend);
124
+ const extraUsage = fields.backend === "claude" && claudeSeen;
125
+ if (fields.backend === "claude")
121
126
  claudeSeen = true;
122
127
  return {
123
128
  ...fields,
@@ -162,7 +167,7 @@ export function resolveTierFallbacks(config, usable) {
162
167
  };
163
168
  return { small: resolve("small"), default: resolve("default"), strong: resolve("strong") };
164
169
  }
165
- /** The config a role's ticks run on while a model-fallback episode is active (PLANS.md "Model
170
+ /** The config a role's runs use while a model-fallback episode is active (PLANS.md "Model
166
171
  * failure fallback, part 1/2"): the role's tier pair from resolveTierFallbacks (the tier's own
167
172
  * pair, else BORROW_ORDER's borrow), shaped like configForRole — provider/model/thinking over
168
173
  * the role's own and top-level values, plus the role's minRunIntervalSeconds. Null when the
@@ -174,11 +179,7 @@ export function fallbackRoleConfig(config, role) {
174
179
  return null;
175
180
  const rc = config.roles[baseRoleOf(role)];
176
181
  return {
177
- ...withModelOverrides(config, tier, {
178
- provider: pair.provider,
179
- model: pair.model,
180
- thinking: pair.thinking,
181
- }),
182
+ ...withModelOverrides(config, tier, pairOverrides(pair)),
182
183
  minRunIntervalSeconds: rc?.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
183
184
  };
184
185
  }
@@ -210,13 +211,13 @@ export function seamsInUse(config) {
210
211
  add("resolver", resolverConfig(config));
211
212
  add("director", configForRole(config, DIRECTOR_ROLE));
212
213
  for (const { tier, step, pair } of tierChainSteps(config)) {
213
- add(step === 0 ? `fallback:${tier}` : `fallback:${tier}#${step}`, withModelOverrides(config, tier, { provider: pair.provider, model: pair.model, thinking: pair.thinking }));
214
+ add(step === 0 ? `fallback:${tier}` : `fallback:${tier}#${step}`, withModelOverrides(config, tier, pairOverrides(pair)));
214
215
  }
215
216
  return [...seams.values()];
216
217
  }
217
218
  /** Build a ModelFallbackView for an episode, or null while the episode is not running the
218
219
  * fallback (no episode, or a due probe on the primary). role-view.ts's
219
- * RoleViewPayload.modelFallback and status/status-data.ts's per-loop row both build this
220
+ * RoleViewPayload.modelFallback and status/status-data.ts's per-role row both build this
220
221
  * shape, so one function keeps the two surfaces from disagreeing about the pair, its start,
221
222
  * and its reason. `now` is the caller's clock (the status snapshot passes its own poll
222
223
  * clock), so a row and its poll agree on what "active" means. */
@@ -259,11 +260,17 @@ export function tiersResolveDistinctPairs(resolved) {
259
260
  .map((p) => modelPairName(p));
260
261
  return new Set(pairs).size >= 2;
261
262
  }
262
- /** Drop a model-override seam's provider/model/thinking fields — the one rule the fallback
263
+ /** Drop a model-override seam's provider/model/backend/thinking fields — the one rule the fallback
263
264
  * view's role and reviewer strips share, so a field on that triple cannot be dropped from one
264
265
  * and left on the other (where it would reach a priced model after the cap). */
265
266
  function stripModel(o) {
266
- const { provider: _p, model: _m, thinking: _t, ...rest } = o;
267
+ const { provider: _p, model: _m, backend: _b, thinking: _t, ...rest } = o;
268
+ return rest;
269
+ }
270
+ /** Drop only a seam's explicit backend, for a tier-name entry kept through the cap: the tier
271
+ * reference stays valid, but a paid `backend` pinned beside it must not outlive the cap. */
272
+ function stripBackend(o) {
273
+ const { backend: _b, ...rest } = o;
267
274
  return rest;
268
275
  }
269
276
  /** The reviewer config as the fallback view sees it: model seam stripped, and its time budget
@@ -282,7 +289,7 @@ function fallbackReview(review) {
282
289
  function stripRoleModels(roles, keepTierNames) {
283
290
  return Object.fromEntries(Object.entries(roles).map(([id, rc]) => [
284
291
  id,
285
- keepTierNames && isTierName(rc?.model) ? rc : stripModel(rc),
292
+ keepTierNames && isTierName(rc?.model) ? stripBackend(rc) : stripModel(rc),
286
293
  ]));
287
294
  }
288
295
  /** Every `roles.<id>` entry with its model seam stripped, except a seam `free` calls free — a
@@ -294,10 +301,10 @@ function freeRoleModels(config, free) {
294
301
  return Object.fromEntries(Object.entries(config.roles).map(([id, rc]) => {
295
302
  if (rc && free?.(withModelOverrides(config, roleSeamTier(config, id), rc)))
296
303
  return [id, rc];
297
- return [id, isTierName(rc?.model) ? rc : stripModel(rc)];
304
+ return [id, isTierName(rc?.model) ? stripBackend(rc) : stripModel(rc)];
298
305
  }));
299
306
  }
300
- /** The config as seen by a role loop running on the free fallback model
307
+ /** The config as seen by a role running on the free fallback model
301
308
  * (plans/fallback-model.md): the fallback's provider/model/thinking installed as the top-level
302
309
  * values AND every per-role and reviewer model override dropped, so that EVERY seam that could
303
310
  * otherwise reach a priced model — an author run, its reviewer, a conflict resolver — resolves
@@ -320,13 +327,26 @@ function freeRoleModels(config, free) {
320
327
  * model, while every priced seam still lands on its tier's fallback pair. */
321
328
  export function applyFallbackModel(config, resolved, free) {
322
329
  if (resolved) {
323
- const selectorOf = (f) => f.pair
324
- ? formatModelSelector({
325
- provider: f.pair.provider,
326
- model: f.pair.model ?? "",
327
- thinking: f.pair.thinking,
328
- })
329
- : undefined;
330
+ // A pair's selector keeps pi's provider; a non-pi pair's model is already the whole id. On a
331
+ // team whose backend is not pi — or a pair routed off pi — the entry names the pair's
332
+ // backend, so the team's own backend cannot take a pi pair's id as its own.
333
+ const selectorOf = (f) => {
334
+ const pair = f.pair;
335
+ if (!pair)
336
+ return undefined;
337
+ const selector = formatModelSelector({
338
+ provider: pair.backend === undefined ? pair.provider : undefined,
339
+ model: pair.model ?? "",
340
+ thinking: pair.backend === undefined ? pair.thinking : undefined,
341
+ });
342
+ if (pair.backend === undefined && (config.backend ?? "pi") === "pi")
343
+ return formatModelSelector({ provider: pair.provider, model: pair.model ?? "", thinking: pair.thinking });
344
+ return {
345
+ backend: pair.backend ?? "pi",
346
+ model: selector,
347
+ ...(pair.thinking !== undefined ? { thinking: pair.thinking } : {}),
348
+ };
349
+ };
330
350
  const model = {};
331
351
  const stepCaps = {};
332
352
  for (const tier of MODEL_TIERS) {
@@ -336,16 +356,33 @@ export function applyFallbackModel(config, resolved, free) {
336
356
  const seam = withModelOverrides(config, tier, {});
337
357
  if (free?.(seam)) {
338
358
  const own = topTierSelector(config, tier);
339
- if (own !== undefined)
340
- model[tier] = formatModelSelector({ ...own, thinking: own.thinking ?? config.thinking });
359
+ // A tier entry that names a backend keeps it as an object entry: a bare selector string
360
+ // would route by namespace and drop the backend the entry chose.
361
+ const backend = topTierBackend(config, tier);
362
+ if (own !== undefined) {
363
+ const thinking = own.thinking ?? config.thinking;
364
+ if (backend !== undefined)
365
+ model[tier] = {
366
+ backend,
367
+ model: formatModelSelector({ ...own, thinking: undefined }),
368
+ ...(thinking !== undefined ? { thinking } : {}),
369
+ };
370
+ else
371
+ model[tier] = formatModelSelector({ ...own, thinking });
372
+ }
341
373
  // A tier with no top-level selector — `agent: "claude"` riding its agent default —
342
374
  // has no own selector to carry the level, so name the routed default plus it.
343
- else if (seam.model !== undefined)
344
- model[tier] = formatModelSelector({
345
- provider: seam.provider,
346
- model: seam.model,
347
- thinking: seam.thinking,
348
- });
375
+ else if (seam.model !== undefined) {
376
+ const routed = { provider: seam.provider, model: seam.model };
377
+ model[tier] =
378
+ backend !== undefined
379
+ ? {
380
+ backend,
381
+ model: formatModelSelector(routed),
382
+ ...(seam.thinking !== undefined ? { thinking: seam.thinking } : {}),
383
+ }
384
+ : formatModelSelector({ ...routed, thinking: seam.thinking });
385
+ }
349
386
  continue;
350
387
  }
351
388
  const sel = selectorOf(resolved[tier]);
@@ -370,7 +407,8 @@ export function applyFallbackModel(config, resolved, free) {
370
407
  return config;
371
408
  return {
372
409
  ...config,
373
- provider: pair.provider,
410
+ ...(pair.backend !== undefined || config.backend !== undefined ? { backend: pair.backend ?? "pi" } : {}),
411
+ provider: pair.backend === undefined ? pair.provider : undefined,
374
412
  model: pair.model,
375
413
  thinking: pair.thinking,
376
414
  review: fallbackReview(config.review),
@@ -1,8 +1,7 @@
1
- // vocabulary: plain
2
1
  import { MODEL_TRIPLE_KEYS, MODEL_TIERS, TIER_MAP_KEYS, THINKING_LEVELS, } from "./config-schema.js";
3
2
  import { allRoleIds } from "../roles/roles.js";
4
3
  import { isJsonObject } from "../files/json-object.js";
5
- import { typoSuffix } from "../text/suggest.js";
4
+ import { didYouMean, suggestClosest, typoSuffix } from "../text/suggest.js";
6
5
  import { truncate, isNonBlankString } from "../text/text.js";
7
6
  /** The generic field-check machinery behind validateConfig (config-validation.ts): error
8
7
  * message rendering (show, typeName), the unknown-key and known-role guards, the NumberRule
@@ -43,7 +42,7 @@ export function typeName(v) {
43
42
  export function checkKnownKeys(obj, known, where, problems) {
44
43
  for (const key of Object.keys(obj)) {
45
44
  if (!known.includes(key))
46
- problems.push(`unknown key "${key}" in ${where} (valid keys: ${known.join(", ")})${typoSuffix(key, known)}`);
45
+ problems.push(`unknown key "${key}" in ${where} (valid keys: ${known.join(", ")})${typoSuffix(key, known)}${key === "agent" ? ' — "agent" is retired, write "backend"' : ""}`);
47
46
  }
48
47
  }
49
48
  /** Read an optional field once and hand it to `check` — the rule every per-field checker and
@@ -99,9 +98,9 @@ export const AT_LEAST_ONE = { ok: (n) => n >= 1, what: "a number of at least 1"
99
98
  export const MAX_DURATION_SECONDS = 2147483;
100
99
  /** Shared known-role guard for the per-role config sections (`roles.<id>` entries and
101
100
  * `maxDailyCostUsdPerRole.<id>` keys): an id outside the role catalog and customRoles cannot
102
- * work — a `roles` typo spawns a phantom loop that errors every tick forever; a caps typo
101
+ * work — a `roles` typo spawns a phantom role that errors every run forever; a caps typo
103
102
  * silently no-ops the cap — so both reject it here with the valid ids, the same message shape
104
- * `tumwater logs --role` uses for a bad flag value. Returns true when the id is known. */
103
+ * `tumwater logs --transcript` uses for a bad flag value. Returns true when the id is known. */
105
104
  export function checkKnownRoleId(section, id, customNames, problems) {
106
105
  if (allRoleIds().includes(id) || customNames.has(id))
107
106
  return true;
@@ -124,7 +123,7 @@ export function checkStringField(problems, obj, prefix, key, allowEmpty = true)
124
123
  /** Every model-override section validates the same provider/model/thinking triple the same
125
124
  * way: empty strings are rejected (see checkStringField) and thinking is value-checked —
126
125
  * pi only recognizes THINKING_LEVELS and silently drops anything else, so a typo would run
127
- * the loops at pi's default depth and never say so.
126
+ * the roles at pi's default depth and never say so.
128
127
  *
129
128
  * `modelShape` widens `model` per plans/model-tiers.md: "selector" (the default, for the
130
129
  * legacy `fallbackModel` object) keeps it a plain selector string; "tier-map" (top level)
@@ -132,9 +131,10 @@ export function checkStringField(problems, obj, prefix, key, allowEmpty = true)
132
131
  * selector strings — a map cannot coexist with a legacy provider in the same section, since
133
132
  * the provider's old meaning (the whole string is a bare id under it) has no per-tier
134
133
  * reading; "tier-name" (roles.<id>.model, review.model) also accepts a bare tier name as a
135
- * reference into the top-level map. Tier names are valid ONLY where a tier reference means
136
- * something: elsewhere (top level, fallbackModel) one is rejected with the two places that
137
- * accept it, so a misplaced `"model": "strong"` is an error naming its fix instead of a
134
+ * reference into the top-level map. A bare value within two edits of a tier name (ignoring
135
+ * case) is rejected as a misspelled tier reference. Tier names are valid ONLY where a tier
136
+ * reference means something: elsewhere (top level, fallbackModel) one is rejected with the two
137
+ * places that accept it, so a misplaced `"model": "strong"` is an error naming its fix instead of a
138
138
  * bare model pattern pi can never resolve. */
139
139
  export function checkModelTripleField(problems, obj, prefix, modelShape = "selector") {
140
140
  for (const key of MODEL_TRIPLE_KEYS)
@@ -151,7 +151,8 @@ export function checkModelTripleField(problems, obj, prefix, modelShape = "selec
151
151
  if (obj.provider !== undefined)
152
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
153
  for (const [tier, v] of Object.entries(m))
154
- if (!isNonBlankString(v))
154
+ // An object entry (`{ backend?, model?, thinking? }`) is validated by validateConfig.
155
+ if (!isNonBlankString(v) && !isJsonObject(v))
155
156
  problems.push(`${prefix}model.${tier} must be a non-empty model id (got ${show(v)})`);
156
157
  return;
157
158
  }
@@ -172,6 +173,12 @@ export function checkModelTripleField(problems, obj, prefix, modelShape = "selec
172
173
  if (modelShape === "tier-name")
173
174
  return; // A valid tier reference.
174
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)}`);
175
182
  }
176
183
  }
177
184
  /** Validate one numeric field against a NumberRule when present. */
@@ -223,3 +230,26 @@ export function checkReservedArgs(problems, args, argsPath, whyFor) {
223
230
  problems.push(`${argsPath}[${i}] ${show(arg)} duplicates a flag tumwater sets — ${why}`);
224
231
  });
225
232
  }
233
+ /** The config <key> a `-c <key>=…` / `--config <key>=…` / `-c<key>=…` / `-c=<key>=…` /
234
+ * `--config=<key>=…` argv element names, or undefined when the element is not a keyed codex
235
+ * config passthrough. Codex takes the key and value in one token (`-c key=value`), and accepts a
236
+ * joined `-c=<key>=<value>` identically to `-c <key>=<value>`; `-c` and `--config` alone are
237
+ * followed by the `<key>=<value>` token as the next element, so the scanner passes both. Kept
238
+ * here beside its one caller (the codex `backends.args` screen); the tokens actually split by
239
+ * checkReservedArgs cannot see this form, which is why the scan is its own loop. */
240
+ export function codexConfigKey(arg, next) {
241
+ let rest;
242
+ if (arg === "-c" || arg === "--config")
243
+ rest = next;
244
+ else if (arg.startsWith("--config="))
245
+ rest = arg.slice("--config=".length);
246
+ else if (arg.startsWith("-c="))
247
+ rest = arg.slice(3);
248
+ else if (arg.startsWith("-c") && arg.length > 2 && !arg.startsWith("--"))
249
+ rest = arg.slice(2);
250
+ if (rest === undefined)
251
+ return undefined;
252
+ const eq = rest.indexOf("=");
253
+ const key = (eq === -1 ? rest : rest.slice(0, eq)).trim();
254
+ return key === "" ? undefined : key;
255
+ }
@@ -1,14 +1,15 @@
1
- // vocabulary: plain
2
1
  /** The config KEY language — how `tumwater config get/set/unset` and the dashboard editors
3
2
  * name a config value and what makes one legitimate, apart from the writes that change it.
4
3
  * One family, in its own module because it serves both verbs AND the read side: `config get`,
5
4
  * shell completion, doctor's holds and the dashboard controllers all address keys and screen
6
5
  * per-key values without performing a write, so burying this in the config-write module made
7
6
  * read-side callers import from the "config WRITES" layer. The mutators (the load →
8
- * validate → atomic-write idiom, the cross-process lock) live in src/config/config-write.ts;
7
+ * validate → atomic-write idiom, the cross-process lock) live in src/config/config-write-core.ts
8
+ * (the operator-editor verbs in src/config/config-write.ts;
9
+ * the director's staged request in src/config/config-request.ts);
9
10
  * the per-key validators and key-shape parsing here are the one home of the wording both the
10
11
  * writers and the read side show. */
11
- import { AGENT_KEYS, CLAUDE_AGENT_KEYS, ROLE_ENTRY_KEYS, TIER_MAP_KEYS, TOP_LEVEL_KEYS } from "./config-schema.js";
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";
12
13
  import { typoSuffix } from "../text/suggest.js";
13
14
  import { MAX_DURATION_SECONDS, POSITIVE_INTEGER, show } from "./config-field-checks.js";
14
15
  import { parseQuietHours } from "../scheduling/quiet-hours.js";
@@ -83,11 +84,19 @@ export const PER_KEY_VALIDATORS = {
83
84
  * check the per-role maps apply. The per-key value validator runs before the write, the same
84
85
  * screening a bare top-level key gets. */
85
86
  export const DOTTED_MAP_KEYS = ["maxDailyCostUsdPerRole", "quietHoursPerRole", "maxConcurrentPerProvider"];
87
+ /** The fixed-shape top-level sections `config get/set/unset` reaches one field of with a
88
+ * two-segment key (`review.model`, `bootstrap.untilPlansDone`), each with its field list. */
89
+ export const DOTTED_SECTION_FIELDS = {
90
+ review: REVIEW_KEYS,
91
+ check: CHECK_KEYS,
92
+ idleBackoff: BACKOFF_KEYS,
93
+ bootstrap: BOOTSTRAP_KEYS,
94
+ };
86
95
  /** The valid first segments of a dotted config key — the per-role maps plus the `roles.` and
87
96
  * `model.` sections. The generic dotted-shape error suggests against these so a typo'd head
88
97
  * (`modle.small`, `maxDailyCostUsdPerRoll.feature`) gets the same did-you-mean the sibling
89
98
  * unknown-key errors attach. */
90
- const DOTTED_KEY_HEADS = [...DOTTED_MAP_KEYS, "roles", "model", "backends"];
99
+ const DOTTED_KEY_HEADS = [...DOTTED_MAP_KEYS, "roles", "model", "backends", ...Object.keys(DOTTED_SECTION_FIELDS)];
91
100
  /** Split a config key into its write/get shape: `foo` is top-level,
92
101
  * `maxDailyCostUsdPerRole.feature` / `quietHoursPerRole.qa` name one map entry,
93
102
  * `roles.qa.model` names one role-entry field, and anything else dotted is an error
@@ -107,9 +116,10 @@ export function parseConfigKey(key) {
107
116
  // `roles.<id>.<field>` names one role field). The id is shape-checked only — routing owns
108
117
  // which agent ids exist (part 4b); a typo'd field is refused here with the valid ones named.
109
118
  if (parts.length === 3 && first === "backends" && second !== undefined && second !== "" && third !== undefined) {
110
- // configDir is claude-only (CLAUDE_AGENT_KEYS): a config set backends.pi.configDir would
111
- // write a key nothing reads, so it is refused everywhere but claude.
112
- const validKeys = second === "claude" ? CLAUDE_AGENT_KEYS : AGENT_KEYS;
119
+ // configDir/windowReserve are claude/codex-only (CLAUDE_AGENT_KEYS / CODEX_BACKEND_KEYS):
120
+ // a config set backends.pi.configDir would write a key nothing reads, so it is refused
121
+ // everywhere but claude and codex.
122
+ const validKeys = second === "claude" ? CLAUDE_AGENT_KEYS : second === "codex" ? CODEX_BACKEND_KEYS : BACKEND_KEYS;
113
123
  if (validKeys.includes(third))
114
124
  return { kind: "agent", id: second, field: third };
115
125
  return {
@@ -129,6 +139,18 @@ export function parseConfigKey(key) {
129
139
  error: `unknown model tier "${second ?? ""}" — a dotted \`model.<tier>\` names one of ${TIER_MAP_KEYS.join(", ")} (e.g. \`config set model.strong huggingface/zai-org/GLM-5.3-Flash:together:low\`)${typoSuffix(second ?? "", TIER_MAP_KEYS)}`,
130
140
  };
131
141
  }
142
+ // `<section>.<field>` names one field of a fixed-shape section (review, check, idleBackoff,
143
+ // bootstrap); the field must be one the schema lists, or the write would be a dead key.
144
+ if (parts.length === 2 && Object.hasOwn(DOTTED_SECTION_FIELDS, first)) {
145
+ const section = first;
146
+ const fields = DOTTED_SECTION_FIELDS[section];
147
+ if (fields.includes(second ?? ""))
148
+ return { kind: "section", section, field: second ?? "" };
149
+ return {
150
+ kind: "error",
151
+ error: `unknown ${first} field "${second ?? ""}" (valid fields: ${fields.join(", ")})${typoSuffix(second ?? "", fields)}`,
152
+ };
153
+ }
132
154
  // A typo'd first segment is the common miss (`maxDailyCostUsdPerRoll.feature`,
133
155
  // `modle.small`); when the head is one of the valid ones the shape itself is wrong, so
134
156
  // suggesting that head back would mislead — the generic wording is the whole story there.
@@ -137,7 +159,7 @@ export function parseConfigKey(key) {
137
159
  : typoSuffix(first, DOTTED_KEY_HEADS);
138
160
  return {
139
161
  kind: "error",
140
- error: `unknown config key "${key}" — dotted keys name one entry of a per-role map or one field of a role entry (e.g. \`maxDailyCostUsdPerRole.feature 1.5\`, \`roles.qa.model x\`); a bare key names a whole top-level value${headHint}`,
162
+ error: `unknown config key "${key}" — dotted keys name one entry of a per-role map or one field of a role entry (e.g. \`maxDailyCostUsdPerRole.feature 1.5\`, \`roles.qa.model x\`, \`review.model x\`); a bare key names a whole top-level value${headHint}`,
141
163
  };
142
164
  }
143
165
  /** The error `tumwater config get <key>` and `config set <key> <value>` both surface for a
@@ -1,12 +1,14 @@
1
1
  import { changedConfigKeys, loadConfigCached } from "./config.js";
2
- import { loopIds } from "../roles/loop-ids.js";
2
+ import { runnerIds } from "../roles/runner-ids.js";
3
3
  import { configForRole, teamModelLabel, modelSelectorField } from "./config-views.js";
4
- import { seamsInUse } from "./config-fallbacks.js";
5
4
  import { bestEffortEventLogger, TUMWATER_SOURCE } from "../events/events.js";
6
- import { LoopRunner } from "../loop/loop.js";
5
+ import { RoleRunner } from "../runner/runner.js";
7
6
  import { configPath } from "../paths.js";
7
+ import { errorMessage } from "../text/text.js";
8
+ import { backendsInUse, seedOpencodeCatalog } from "../gates/startup-gate.js";
8
9
  import { seedClaudeAccount } from "../agent/claude/claude-account.js";
9
- import { agentMissingMessage, findAgentBinary, resolveAgentBin } from "../pi/pi-bin.js";
10
+ import { seedCodexAccount, readCodexAccount } from "../agent/codex/codex-account.js";
11
+ import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
10
12
  /** A live config reloader over the given wiring; poll it once per scheduler cycle (see
11
13
  * LiveConfigReload for the contract). */
12
14
  export function newLiveConfigReload(deps) {
@@ -16,7 +18,7 @@ export function newLiveConfigReload(deps) {
16
18
  // transition warnings).
17
19
  let lastConfigError = null;
18
20
  let configMissing = false;
19
- let prevEnabled = new Set(loopIds(deps.config));
21
+ let prevEnabled = new Set(runnerIds(deps.config));
20
22
  let live = deps.config;
21
23
  // The previous successful reload's config, for the one-shot config_changed event. Seeded from
22
24
  // the startup config, so the first poll of an unchanged file logs nothing.
@@ -32,6 +34,13 @@ export function newLiveConfigReload(deps) {
32
34
  // reload block below), so repeated polls of a config that keeps a claude seam cannot respawn
33
35
  // `claude auth status` on every cycle.
34
36
  const liveClaudeSeedAttempted = new Set();
37
+ // Likewise for the codex-account seed: one `codex login status` per root per process.
38
+ const liveCodexSeedAttempted = new Set();
39
+ // Set while a live edit routing a codex seam is being refused for an API-key login, so the
40
+ // refusal warns once on the transition in (and once again when it re-enters after clearing).
41
+ let codexLiveRefused = false;
42
+ // Likewise for the opencode models catalog: one models.dev refresh per root per process.
43
+ const liveOpencodeSeedAttempted = new Set();
35
44
  // Best-effort event logging: poll() runs inside the orchestrator's poll loop
36
45
  // (src/orchestrator/orchestrator.ts), which has no catch, so an unwritable feed (ENOSPC,
37
46
  // EACCES, the path replaced by a directory) would otherwise end the team — and, worse, skip
@@ -59,8 +68,32 @@ export function newLiveConfigReload(deps) {
59
68
  configMissing = false;
60
69
  }
61
70
  if (reloaded.config) {
62
- live = reloaded.config;
63
71
  const nextConfig = reloaded.config;
72
+ // A live edit that routes a seam to a codex logged in with an API key is refused before
73
+ // the reload is applied: startup refuses the same codex seam (decision 5), and here a
74
+ // warn-only would let the running team spend per-token Codex the budget cap cannot see
75
+ // (review objection 1). Refusing keeps the last-known-good config, like a missing or
76
+ // broken file. The seed runs once per root (`codex login status` is a real probe); the
77
+ // persisted account file is the voice of the refusal on later polls, re-probing a stale
78
+ // api answer (codex-account.ts) so the operator's `codex login --device-auth` remedy
79
+ // clears it at the next seed. A subscription codex passes through to the backend loop.
80
+ const prevBackends = new Set(backendsInUse(prevLive));
81
+ const newlyRoutedCodex = !prevBackends.has("codex") && backendsInUse(nextConfig).includes("codex");
82
+ if (newlyRoutedCodex && !liveCodexSeedAttempted.has(deps.root)) {
83
+ liveCodexSeedAttempted.add(deps.root);
84
+ seedCodexAccount(deps.root, nextConfig);
85
+ }
86
+ if (newlyRoutedCodex && readCodexAccount(deps.root)?.billing === "api") {
87
+ if (codexLiveRefused)
88
+ return prevLive;
89
+ codexLiveRefused = true;
90
+ 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`");
91
+ return prevLive;
92
+ }
93
+ // A poll that did not refuse clears the warn-once marker so a later re-entry into the
94
+ // refusal (the operator reverts, then re-adds the API-billed codex seam) warns again.
95
+ codexLiveRefused = false;
96
+ live = reloaded.config;
64
97
  // A live edit that changes behavior elsewhere logs one event naming the settings that
65
98
  // changed (maxConcurrent and sessionRetentionDays have their own events elsewhere).
66
99
  const changedKeys = changedConfigKeys(prevLive, reloaded.config);
@@ -89,31 +122,37 @@ export function newLiveConfigReload(deps) {
89
122
  });
90
123
  }
91
124
  // A live edit that routes a seam to a backend the previous config did not use: the
92
- // team keeps running, but that backend must be verified and (for claude) its account
93
- // seeded, exactly as startup would — so a claude reviewer added to a pi team does not
94
- // start uncheckable. A missing binary is a warning event, never a crash; a present one
95
- // logs its verdict as a backend_binary event. The seed is attempted once per root.
96
- const prevAgents = new Set(seamsInUse(prevLive).map((s) => s.agent ?? "pi"));
97
- for (const agent of new Set(seamsInUse(reloaded.config).map((s) => s.agent ?? "pi"))) {
98
- if (prevAgents.has(agent))
125
+ // team keeps running, but that backend must be verified and its account (claude) or
126
+ // models catalog (opencode) seeded, exactly as startup would — so a claude reviewer
127
+ // added to a pi team does not start uncheckable. A missing binary is a warning event,
128
+ // never a crash; a present one logs its verdict as a backend_binary event. The seed is
129
+ // attempted once per root. (The codex seed moved to the refusal check at the top of
130
+ // this block, which also refuses an API-key-logged-in codex seam.)
131
+ for (const backend of backendsInUse(reloaded.config)) {
132
+ if (prevBackends.has(backend))
99
133
  continue;
100
- const resolved = resolveAgentBin(reloaded.config, agent);
101
- if (findAgentBinary(resolved)) {
102
- emit({ source: TUMWATER_SOURCE, type: "backend_binary", agent, found: true });
134
+ const resolved = resolveBackendBin(reloaded.config, backend);
135
+ if (findBackendBinary(resolved)) {
136
+ emit({ source: TUMWATER_SOURCE, type: "backend_binary", agent: backend, found: true });
103
137
  }
104
138
  else {
105
- warn(`newly routed ${agent} backend: ${agentMissingMessage(resolved, agent)}`);
139
+ warn(`newly routed ${backend} backend: ${backendMissingMessage(resolved, backend)}`);
106
140
  }
107
- if (agent === "claude" && !liveClaudeSeedAttempted.has(deps.root)) {
141
+ if (backend === "claude" && !liveClaudeSeedAttempted.has(deps.root)) {
108
142
  liveClaudeSeedAttempted.add(deps.root);
109
143
  seedClaudeAccount(deps.root, reloaded.config);
110
144
  }
145
+ if (backend === "opencode" && !liveOpencodeSeedAttempted.has(deps.root)) {
146
+ liveOpencodeSeedAttempted.add(deps.root);
147
+ // Not awaited: poll() is synchronous and a models.dev fetch must not stall the loop.
148
+ void seedOpencodeCatalog(deps.root, reloaded.config, { ...deps.opencodeSeed, warn }).catch((err) => warn(`open code model catalog seed failed: ${errorMessage(err)}`));
149
+ }
111
150
  }
112
151
  prevLive = reloaded.config;
113
152
  for (const r of deps.runners)
114
153
  r.config = reloaded.config;
115
154
  // Live-resize the concurrency cap: a mid-run edit changes how many pi runs execute
116
- // concurrently within this poll — no restart. Growing admits already-queued ticks;
155
+ // concurrently within this poll — no restart. Growing admits already-queued runs;
117
156
  // shrinking never preempts in-flight work, it only caps future grants.
118
157
  const newMaxConcurrent = Math.max(1, reloaded.config.maxConcurrent);
119
158
  if (newMaxConcurrent !== lastMaxConcurrent) {
@@ -141,17 +180,17 @@ export function newLiveConfigReload(deps) {
141
180
  }
142
181
  }
143
182
  lastKeyCaps = nextKeyCaps;
144
- const nowEnabled = loopIds(reloaded.config);
183
+ const nowEnabled = runnerIds(reloaded.config);
145
184
  // Enabling a role — or raising `roles.<id>.instances` — mid-run starts it: create the
146
- // runner for each newly present loop id (its persisted state survives). A lowered
147
- // instance count keeps its runner; `loopEnabled` skips its future ticks. A scoped round
185
+ // runner for each newly present role id (its persisted state survives). A lowered
186
+ // instance count keeps its runner; `runnerEnabled` skips its future runs. A scoped round
148
187
  // skips every other id: the enabling is logged (the warn loop below), but its runner
149
188
  // waits for the next unscoped round.
150
189
  for (const role of nowEnabled) {
151
- if (deps.roleFilter !== undefined && role !== deps.roleFilter)
190
+ if (deps.roleFilter !== undefined && !deps.roleFilter.includes(role))
152
191
  continue;
153
192
  if (!deps.runners.some((r) => r.role === role))
154
- deps.runners.push(new LoopRunner(deps.root, role, reloaded.config, deps.mainBranch, deps.runnerSignal));
193
+ deps.runners.push(new RoleRunner(deps.root, role, reloaded.config, deps.mainBranch, deps.runnerSignal));
155
194
  }
156
195
  for (const role of prevEnabled)
157
196
  if (!nowEnabled.includes(role))