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,25 +1,25 @@
1
- // vocabulary: plain
2
1
  /** The config AS SEEN BY A RUN — the per-seam resolution half of the config views, split out
3
2
  * of config.ts: which provider/model/thinking a role, the reviewer, or the conflict resolver
4
3
  * resolves to, and which timeout a reviewer run gets. The budget/tier fallback side — which
5
4
  * free pair the team runs on at the daily cap, and the per-role fallback config (fallbackPair,
6
5
  * resolveTierFallbacks, applyFallbackModel, …) — lives in config-fallbacks.ts, which builds on
7
6
  * this module one-way. No I/O and no caching — the persistence side (defaults, loading,
8
- * saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts. */
7
+ * saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts
8
+ * (editor verbs) and config-request.ts (the director's staged request). */
9
9
  import { MODEL_TIERS } from "./config-schema.js";
10
- import { formatModelSelector, isAgentId, parseModelSelector, routeModel } from "./model-selector.js";
10
+ import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
11
11
  import { modelPairName } from "../budget/budget.js";
12
12
  import { isJsonObject } from "../files/json-object.js";
13
13
  import { DIRECTOR_ROLE, roleById } from "../roles/roles.js";
14
- import { baseRoleOf } from "../roles/loop-ids.js";
14
+ import { baseRoleOf } from "../roles/runner-ids.js";
15
15
  /** The effective `backends.<id>` entry as a run sees it (plans/agent-backends.md "Back-compat"):
16
16
  * the file's own entry, with the legacy top-level `agentBin`/`piArgs` read as the pi backend's
17
17
  * `bin`/`args` when the new form leaves a field absent. Pure — it never writes those legacy
18
18
  * values back into `config.backends`, so a config write that re-serializes the merged config
19
19
  * cannot leak a second spelling into the file (which validateConfig would then reject).
20
- * pi-backend.ts and claude-backend.ts read `.bin` through resolveAgentBin (pi-bin.ts);
20
+ * pi-backend.ts and claude-backend.ts read `.bin` through resolveBackendBin (pi-bin.ts);
21
21
  * claude-backend.ts reads `.args` for passthrough argv. */
22
- export function resolvedAgentConfig(config, id) {
22
+ export function resolvedBackendConfig(config, id) {
23
23
  const entry = config.backends?.[id];
24
24
  // A view must own its array: returning the config's own `args` reference would let a caller
25
25
  // that appends passthrough argv silently mutate the loaded config, breaking the
@@ -31,6 +31,20 @@ export function resolvedAgentConfig(config, id) {
31
31
  }
32
32
  return { bin: entry?.bin ?? config.agentBin, args: (entry?.args ?? config.piArgs ?? []).slice() };
33
33
  }
34
+ /** A backend's subscription-window reserve (plans/codex-backend.md part 2b/9): the nested
35
+ * `backends[backend].windowReserve` when the backend has one, else the top-level
36
+ * `claudeWindowReserve` for claude (its older spelling), else undefined. The two spellings are
37
+ * mutually exclusive in a loaded file (validateConfig rejects one holding both), so the
38
+ * fallback only ever answers for a config written before the rename. Pure: it never writes a
39
+ * spelling back into the file. */
40
+ export function windowReserveFor(config, backend) {
41
+ const nested = config.backends?.[backend]?.windowReserve;
42
+ if (nested !== undefined)
43
+ return nested;
44
+ if (backend === "claude")
45
+ return config.claudeWindowReserve;
46
+ return undefined;
47
+ }
34
48
  /** True when `s` names one of the three tiers — the form `roles.<id>.model` /
35
49
  * `review.model` may take to point at that tier of the top-level `model` map. */
36
50
  export function isTierName(s) {
@@ -43,13 +57,40 @@ export function isTierName(s) {
43
57
  * leaves pi's own default in charge. Map-form + legacy provider is a validation error, so
44
58
  * no legacy provider is in scope for a map's entries. */
45
59
  export function topTierSelector(config, tier) {
60
+ return resolveTopTier(config, tier).sel;
61
+ }
62
+ function resolveTopTier(config, tier) {
46
63
  if (typeof config.model === "string")
47
- return parseModelSelector(config.model, config.provider);
48
- if (isJsonObject(config.model)) {
49
- const sel = config.model[tier] ?? config.model.default;
50
- return sel !== undefined ? parseModelSelector(sel) : undefined;
64
+ return { sel: parseModelSelector(config.model, config.provider) };
65
+ if (!isJsonObject(config.model))
66
+ return {};
67
+ const raw = config.model[tier] ?? config.model.default;
68
+ if (raw === undefined)
69
+ return {};
70
+ if (typeof raw === "string")
71
+ return { sel: parseModelSelector(raw) };
72
+ const withThinking = (t) => {
73
+ if (raw.thinking === undefined)
74
+ return t;
75
+ return t.sel ? { ...t, sel: { ...t.sel, thinking: raw.thinking } } : { ...t, thinking: raw.thinking };
76
+ };
77
+ if (raw.model !== undefined) {
78
+ const sel = parseModelSelector(raw.model);
79
+ return withThinking({ sel, ...(raw.backend !== undefined ? { backend: raw.backend } : {}) });
51
80
  }
52
- return undefined;
81
+ // No model of its own: take `default`'s, but an explicit backend only when `default` routes
82
+ // there already (the inherited model then routes by its own spelling, exactly as `default` does).
83
+ const dflt = tier === "default" ? {} : resolveTopTier(config, "default");
84
+ if (raw.backend !== undefined &&
85
+ (tier === "default" || withModelOverrides(config, "default", {}).agent !== raw.backend))
86
+ return withThinking({ backend: raw.backend });
87
+ return withThinking(dflt);
88
+ }
89
+ /** The explicit `backend` a tier's entry names — undefined for a string entry or an object entry
90
+ * without one, or when the entry names a backend `default`'s model does not route to and so
91
+ * inherits nothing (the backend then rides only its own tier default). */
92
+ export function topTierBackend(config, tier) {
93
+ return resolveTopTier(config, tier).backend;
53
94
  }
54
95
  /** The selector one seam's tier resolves to at the top level — the tier's own map entry,
55
96
  * else `default`'s, else none (pi's own default). The pricing view (teamModelsFree) and
@@ -77,21 +118,43 @@ export function tierModel(config, tier) {
77
118
  export function withModelOverrides(config, tier, o) {
78
119
  // The top-level model resolves at the seam's tier: a string form is the default tier's
79
120
  // selector; a map form takes the tier's entry, else `default`'s.
80
- const topSel = topTierSelector(config, tier);
121
+ const top = resolveTopTier(config, tier);
81
122
  const ownSel = o.model !== undefined ? parseModelSelector(o.model, o.provider ?? config.provider) : undefined;
123
+ const configBackend = config.backend ?? "pi";
124
+ // A role's own `backend` binds only to its own model; alone it inherits the tier's model only
125
+ // when the tier already routes there, else the backend's tier default fills in. A role's
126
+ // `model` without `backend` routes by the legacy rules, never by the tier entry's backend.
127
+ const inherit = o.model === undefined &&
128
+ (o.backend === undefined ||
129
+ routeModel(top.sel?.provider, top.sel?.model, configBackend, tier, top.backend).agent === o.backend);
130
+ // The tier's thinking level rides every path but a backend-only role whose tier routes elsewhere.
131
+ const topThinking = o.model === undefined && !inherit ? undefined : (top.sel?.thinking ?? top.thinking);
132
+ const topSel = inherit ? top.sel : undefined;
133
+ const backend = o.model !== undefined ? o.backend : inherit ? top.backend : o.backend;
82
134
  const provider = ownSel ? ownSel.provider : (o.provider ?? topSel?.provider ?? config.provider);
83
135
  const model = ownSel ? ownSel.model : topSel?.model;
84
136
  // The routed backend decides whether the provider half survives: a pi provider name is kept,
85
137
  // a backend namespace such as `claude` is dropped (that backend takes no provider).
86
- const routed = routeModel(provider, model, config.backend ?? "pi", tier);
138
+ const routed = routeModel(provider, model, configBackend, tier, backend);
87
139
  return {
88
140
  ...config,
89
141
  provider: routed.agent === "pi" ? provider : undefined,
90
142
  model: routed.model,
91
- thinking: o.thinking ?? ownSel?.thinking ?? topSel?.thinking ?? config.thinking,
143
+ thinking: o.thinking ?? ownSel?.thinking ?? topThinking ?? config.thinking,
92
144
  agent: routed.agent,
93
145
  };
94
146
  }
147
+ /** A fallback pair as withModelOverrides' overrides: always an explicit backend (a pi pair is pi
148
+ * even on a claude team), a non-pi pair dropping the seam-key provider (its model already
149
+ * carries the whole id), a pi pair keeping its provider. */
150
+ export function pairOverrides(pair) {
151
+ return {
152
+ provider: pair.backend === undefined ? pair.provider : undefined,
153
+ model: pair.model,
154
+ backend: pair.backend ?? "pi",
155
+ thinking: pair.thinking,
156
+ };
157
+ }
95
158
  /** The provider/model/thinking overrides a config section (a role entry or `review`) carries
96
159
  * into withModelOverrides: a tier-name `model` already chose the seam's tier, so it is not a
97
160
  * selector and drops out here. Shared by configForRole and reviewConfig. */
@@ -99,12 +162,13 @@ function seamOverrides(s) {
99
162
  return {
100
163
  provider: s.provider,
101
164
  model: isTierName(s.model) ? undefined : s.model,
165
+ backend: s.backend,
102
166
  thinking: s.thinking,
103
167
  };
104
168
  }
105
169
  /** The tier a role's model resolves at: a tier-name `roles.<id>.model` names it directly, and
106
170
  * every other role rides the model catalog's assignment. Exported for the per-tier budget gate's
107
- * pause set (src/gates/gate-polls.ts, part 5c/8) and the dashboards' per-role budget pause
171
+ * pause set (src/budget/budget-wiring.ts, part 5c/8) and the dashboards' per-role budget pause
108
172
  * (src/status/status-data.ts), which must resolve the SAME tier the seams resolve — the hold an
109
173
  * operator sees is the hold the scheduler enforces. */
110
174
  export function roleSeamTier(config, role) {
@@ -115,6 +179,14 @@ export function roleSeamTier(config, role) {
115
179
  return "strong";
116
180
  return roleById(role)?.tier ?? "default";
117
181
  }
182
+ /** Whether the review gate reviews this role's commits AFTER they merge (review.afterMerge
183
+ * lists the role's base id). One home for the membership test the review gate (review.ts's
184
+ * reviewAttempt), the runner's `already_queued` recovery arm (runner-run.ts) and the role
185
+ * view (`tumwater role <id>` / the dashboard role drawer) all share, so an operator reading
186
+ * one surface sees the same answer every consumer enforces. Pure, no I/O. */
187
+ export function reviewedAfterMerge(config, role) {
188
+ return (config.review.afterMerge ?? []).includes(baseRoleOf(role));
189
+ }
118
190
  /** The config as seen by one role: role-level provider/model/thinking overrides applied
119
191
  * over the top-level values, plus the per-role minRunIntervalSeconds (a slow clock for
120
192
  * roles that should act rarely) falling back to the global value when unset. The role's
@@ -130,25 +202,22 @@ export function configForRole(config, role) {
130
202
  minRunIntervalSeconds: rc.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
131
203
  };
132
204
  }
133
- /** The config a role's ticks SCHEDULE from: configForRole, except that while the new-project
134
- * bootstrap is active (plans/work-ratio.md, part 2/2) the plan role schedules on the GLOBAL
135
- * minRunIntervalSeconds rather than its per-role 3600 s slow clock, so a young project gets a
136
- * steady stream of plans for feature; and while the brief still carries the placeholder status
137
- * (descriptionIncomplete) the plan role schedules with a ZERO min gap, so a fresh project plans
138
- * back-to-back until a run changes nothing — the zero gap is tighter than bootstrap. One home
139
- * for the rule: isEligible enforces the same gap when it decides whether plan may start, and
140
- * loop-tick.ts hands this config to applyTickOutcome so nextRunAt is scheduled from the same
141
- * figure instead of the role default. `descriptionIncomplete` means the brief's status section
142
- * is still the placeholder; the initial prompt half is always present (init rejects an empty
143
- * one), so the placeholder status is the machine-detectable "description not yet complete". */
144
- export function scheduleConfigForRole(config, role, bootstrapActive, descriptionIncomplete = false) {
205
+ /** The config a role's runs SCHEDULE from: configForRole, except that while PLANS.md still
206
+ * has planning room (planBacklogOpen — below the plan role's eligible-plan target, or holding
207
+ * a plan that needs review/needs replan) the plan role schedules with a ZERO min gap, so a
208
+ * productive plan run reruns on the next poll and an unproductive one drops to the idle
209
+ * backoff. The plan role's own machine signal is `planBacklogFull` (src/scheduling/claims.ts):
210
+ * a fresh project's empty backlog is not full, so this subsumes the old placeholder-status
211
+ * trigger. One home for the rule: isEligible enforces the same gap when it decides whether
212
+ * plan may start, and runner-run.ts hands this config to applyRunOutcome so nextRunAt is
213
+ * scheduled from the same figure instead of the role default. Every other role, and the plan
214
+ * role once the target is met (which already holds the run via the plans-full deferral),
215
+ * schedule configForRole unchanged. */
216
+ export function scheduleConfigForRole(config, role, planBacklogOpen = false) {
145
217
  const resolved = configForRole(config, role);
146
- if (descriptionIncomplete && baseRoleOf(role) === "plan") {
218
+ if (planBacklogOpen && baseRoleOf(role) === "plan") {
147
219
  return { ...resolved, minRunIntervalSeconds: 0 };
148
220
  }
149
- if (bootstrapActive && baseRoleOf(role) === "plan") {
150
- return { ...resolved, minRunIntervalSeconds: config.minRunIntervalSeconds };
151
- }
152
221
  return resolved;
153
222
  }
154
223
  /** The config as seen by the review gate's pi runs: the top-level `review` section's
@@ -173,10 +242,10 @@ export function resolverConfig(config) {
173
242
  return withModelOverrides(config, "strong", {});
174
243
  }
175
244
  /** Default wall-clock budget for one reviewer run (`review.timeoutSeconds`). The reviewer holds
176
- * the merge slot the whole queue waits on, and its only other limits were the tick's budget
245
+ * the merge slot the whole queue waits on, and its only other limits were the run's budget
177
246
  * and the quiet watchdog: reviews measured 2026-09-22/23 ran 2.3 min median, 9.2 min p90 and
178
247
  * 24.6 min max, while earlier days had 1.5–3.5 h reviews. 15 min clears the ordinary review and
179
- * kills a wedged one in minutes instead of the tick's hours. */
248
+ * kills a wedged one in minutes instead of the run's hours. */
180
249
  export const REVIEW_TIMEOUT_S = 900;
181
250
  /** The reviewer's wall-clock floor while the budget fallback carries the team. The free model
182
251
  * is typically local and far slower per turn than the budgeted one it replaces, but a review
@@ -189,7 +258,7 @@ export const REVIEW_TIMEOUT_S = 900;
189
258
  export const FALLBACK_REVIEW_TIMEOUT_S = 3600;
190
259
  /** The config as seen by the gate's reviewer run: the reviewer's model wiring (reviewConfig)
191
260
  * with its own time budget — `review.timeoutSeconds`, default REVIEW_TIMEOUT_S — overriding
192
- * the tick's. A smaller runTimeoutSeconds still wins. A still-progressing gate run gets one
261
+ * the run's. A smaller runTimeoutSeconds still wins. A still-progressing gate run gets one
193
262
  * extension of the same length (src/pi/pi-watchdogs.ts), so the effective ceiling on a large
194
263
  * diff's review is twice this budget. */
195
264
  export function reviewRunConfig(config) {
@@ -221,21 +290,21 @@ export function resolvedModelFields(provider, model, thinking) {
221
290
  * provider/model/thinking triple formatted as one selector string, or an empty object when
222
291
  * no model id is resolved (pi's own default) — the key stays absent rather than empty, the
223
292
  * same empty-means-unset rule resolvedModelFields applies to the raw halves. One home of
224
- * that assembly and guard, shared by the two run-start event writers (loop-tick.ts's run_start,
225
- * review.ts's review_start) and the per-loop status snapshot (status-data.ts) so the three
226
- * observability surfaces cannot disagree about which model a run starts on. A non-pi agent is
227
- * named as the selector's provider namespace (agentQualifiedSelector), so a claude run reads
293
+ * that assembly and guard, shared by the two run-start event writers (runner-run.ts's run_start,
294
+ * review-run.ts's review_start) and the per-role status snapshot (status-data.ts) so the three
295
+ * observability surfaces cannot disagree about which model a run starts on. A non-pi backend is
296
+ * named as the selector's provider namespace (backendQualifiedSelector), so a claude run reads
228
297
  * `claude/claude-sonnet-5-5` in every surface instead of a bare model id. */
229
298
  export function modelSelectorField(cfg) {
230
299
  const { model } = cfg;
231
- return model ? { model: agentQualifiedSelector({ ...cfg, model }) } : {};
300
+ return model ? { model: backendQualifiedSelector({ ...cfg, model }) } : {};
232
301
  }
233
- /** The resolved seam as one selector string, agent-qualified: a non-pi backend id is the
302
+ /** The resolved seam as one selector string, backend-qualified: a non-pi backend id is the
234
303
  * selector's provider namespace (`claude/claude-sonnet-5-5`), the form routeModel parses back
235
- * into that agent; a pi seam keeps `provider/model[:thinking]` byte-identical to before, since
304
+ * into that backend; a pi seam keeps `provider/model[:thinking]` byte-identical to before, since
236
305
  * pi's providers are real model providers, not a backend namespace. `model` is required — the
237
306
  * caller has already guarded that a model resolved (modelSelectorField). */
238
- export function agentQualifiedSelector(cfg) {
307
+ export function backendQualifiedSelector(cfg) {
239
308
  const base = formatModelSelector({ provider: cfg.provider, model: cfg.model, thinking: cfg.thinking });
240
309
  return cfg.agent && cfg.agent !== "pi" ? `${cfg.agent}/${base}` : base;
241
310
  }
@@ -246,9 +315,9 @@ export function agentQualifiedSelector(cfg) {
246
315
  export function seamHoldKey(cfg) {
247
316
  return cfg.agent !== undefined && cfg.agent !== "pi" ? cfg.agent : cfg.provider;
248
317
  }
249
- /** The `modelPairName` a resolved seam's run is charged under (LoopState.dayCostByPair): the
318
+ /** The `modelPairName` a resolved seam's run is charged under (RunnerState.dayCostByPair): the
250
319
  * routed backend's id stands in for the provider on a non-pi seam, exactly as the fallback
251
- * pairs carry it (config-fallbacks.ts's seamKeyedFallbackProvider), so a run and the chain
320
+ * pairs carry it (config-fallbacks.ts's seamKeyedPairFields), so a run and the chain
252
321
  * step it ran on name one key. */
253
322
  export function seamPairName(cfg) {
254
323
  return modelPairName({ provider: seamHoldKey(cfg), model: cfg.model });
@@ -273,18 +342,31 @@ export function seamConcurrencyKey(cfg, piDefault) {
273
342
  * event: null when no tier resolves to a model (pi's own default), the single selector when
274
343
  * every set tier agrees (the string-form `model`), and the `small=…, default=…, strong=…`
275
344
  * list when the tiers differ. Reading EVERY tier makes an edit to any tier's entry change the
276
- * label. Each tier's label is agent-qualified (agentQualifiedSelector), so flipping the
345
+ * label. Each tier's label is agent-qualified (backendQualifiedSelector), so flipping the
277
346
  * top-level `agent` over a bare-id model relabels every tier and the reload now emits
278
347
  * `model_changed` — a team's backend is part of what it runs (PLANS.md "Mixed claude + pi
279
348
  * fleets, part 3/4"), and an edit that moves a bare id to another backend must not read as
280
349
  * "nothing changed". */
281
350
  export function teamModelLabel(config) {
282
351
  const entries = MODEL_TIERS.map((tier) => {
283
- const sel = tierModel(config, tier);
284
- if (!sel)
352
+ const { sel, backend, thinking } = resolveTopTier(config, tier);
353
+ if (!sel && backend === undefined)
354
+ return null;
355
+ const routed = routeModel(sel?.provider, sel?.model, config.backend ?? "pi", tier, backend);
356
+ if (routed.model === undefined)
285
357
  return null;
286
- const { agent } = routeModel(sel.provider, sel.model, config.backend ?? "pi", tier);
287
- return { tier, label: agentQualifiedSelector({ ...sel, agent }) };
358
+ // routeModel consumed an agent-naming provider (or an explicit backend) as the agent; keep
359
+ // the provider only for a pi seam so the label doesn't prefix the agent twice.
360
+ const provider = routed.agent === "pi" ? sel?.provider : undefined;
361
+ return {
362
+ tier,
363
+ label: backendQualifiedSelector({
364
+ provider,
365
+ model: routed.model,
366
+ thinking: sel?.thinking ?? thinking,
367
+ agent: routed.agent,
368
+ }),
369
+ };
288
370
  }).filter((e) => e !== null);
289
371
  if (entries.length === 0)
290
372
  return null;
@@ -293,18 +375,18 @@ export function teamModelLabel(config) {
293
375
  return entries.map((e) => `${e.tier}=${e.label}`).join(", ");
294
376
  }
295
377
  /** One fallback pair rendered for display, agent-qualified (PLANS.md "Mixed claude + pi
296
- * fleets, part 3/4" gap 7): a pair routed to a non-pi backend — its `provider` is that
297
- * backend's id, per seamKeyedFallbackProvider, so `claude` — renders as `claude/claude-sonnet-5-5`,
378
+ * fleets, part 3/4" gap 7): a pair routed to a non-pi backend — its `backend`, per
379
+ * seamKeyedPairFields, so `claude` — renders as `claude/claude-sonnet-5-5`,
298
380
  * the same agent-qualified form every other surface carries; a pi pair keeps the bare model
299
381
  * byte-identical to before (the badge, the team alert and `tumwater role` all name the
300
382
  * model a pi fallback runs, never its provider). Falls back through the provider to the
301
383
  * caller's `<agent> default` label when both halves are absent. One home so the three display
302
384
  * surfaces cannot render one fallback two ways. */
303
- export function fallbackDisplayPair(pair, agentDefault) {
385
+ export function fallbackDisplayPair(pair, backendDefault) {
304
386
  const p = pair ?? {};
305
387
  if (p.model !== undefined)
306
- return p.provider !== undefined && isAgentId(p.provider) ? `${p.provider}/${p.model}` : p.model;
388
+ return p.backend !== undefined && p.backend !== "pi" ? `${p.backend}/${p.model}` : p.model;
307
389
  if (p.provider !== undefined)
308
390
  return p.provider;
309
- return agentDefault;
391
+ return backendDefault;
310
392
  }
@@ -0,0 +1,135 @@
1
+ /** The shared write MACHINERY for tumwater.json — the one home for the load →
2
+ * validate → atomic-write idiom, the cross-process lock, and the raw/preserve-role
3
+ * helpers that every writer shares. The operator-editor verbs (config-write.ts's
4
+ * setConfigKey/unsetConfigKey/setRoleModelFields/setDailyBudgetUsd) are whole-config writes
5
+ * with the
6
+ * same traps — a fresh loadConfig that bypasses config.ts's stat cache, validateConfig over
7
+ * the merged candidate, an atomic write because readers poll tumwater.json every ~2 s, a lock
8
+ * that serializes separate-process writers, and a roles section that must never be re-expanded
9
+ * from the catalog — so the idiom lives here, apart from the editor module.
10
+ * (config-request.ts's applyConfigRequest hits the same traps but inlines its own load →
11
+ * validate → write; it imports only withConfigLock and rawConfigObject from this module.)
12
+ * The read side — defaults, loading, saving,
13
+ * and the role-selection helpers — lives in config.ts, the per-run model derivations in
14
+ * config-views.ts, the config KEY language in config-keys.ts, and the operator-editor verbs
15
+ * in config-write.ts. */
16
+ import { configPath, configLockPath, CONFIG_BASENAME } from "../paths.js";
17
+ import { errorMessage } from "../text/text.js";
18
+ import { writeJsonAtomic } from "../files/json-files.js";
19
+ import { objectOrEmpty } from "../files/json-object.js";
20
+ import { withStateLock } from "../concurrency/lock.js";
21
+ import { validateConfig } from "./config-validation.js";
22
+ import { loadConfig, parseJsonConfig, collapseBackendAliases } from "./config.js";
23
+ /** Serialize a config write through the cross-process lock (paths.ts's configLockPath): the
24
+ * CLI, a standalone dashboard's server, and the running team are separate processes, so an
25
+ * unguarded read-modify-write lets one writer's whole-file write silently drop another's change
26
+ * (the same lost update paused-roles.json already guards against). A lock that cannot be
27
+ * acquired within the budget throws rather than writing unlocked — a silently lost config edit
28
+ * is worse than a write that reports an error. */
29
+ const CONFIG_LOCK_TIMEOUT_MS = 10_000;
30
+ export function withConfigLock(root, fn) {
31
+ // withStateLock creates the state dir the lock sits in (a CLI write may run before any state
32
+ // file).
33
+ return withStateLock(configLockPath(root), fn, CONFIG_LOCK_TIMEOUT_MS);
34
+ }
35
+ /** The error arm of the shared write-result union — one home for the `{ ok: false, error }`
36
+ * constructor every writer's failure path returns (a broken on-disk file, a validation
37
+ * problem, a failed write, a lost lock), so the shape cannot drift between callers. */
38
+ export function fail(error) {
39
+ return { ok: false, error };
40
+ }
41
+ /** The one load → mutate → validate → atomic-write idiom the operator-editor verbs share
42
+ * (setDailyBudgetUsd, setConfigKey, unsetConfigKey, setRoleModelFields): a fresh loadConfig that
43
+ * bypasses config.ts's stat cache (a writer must see the latest file), the caller's mutation
44
+ * applied in memory, validateConfig over the whole merged candidate, then writeJsonAtomic
45
+ * (tmp file + rename) because readers poll tumwater.json every ~2 s. The whole load-to-write
46
+ * sequence runs under withConfigLock, so two processes racing different keys cannot each write
47
+ * from the same pre-race snapshot and lose one update. On any failure — a broken on-disk file,
48
+ * a type-invalid candidate, a failed write, or a lock that cannot be acquired — the file is
49
+ * left untouched (and no tmp remnant is left behind); the error surfaces as a string so
50
+ * callers (both dashboards, the CLI) can print it without try/catch plumbing. */
51
+ export function writeConfigMutation(root, mutate) {
52
+ try {
53
+ return withConfigLock(root, () => {
54
+ let cfg;
55
+ try {
56
+ cfg = loadConfig(root); // fresh — bypasses the stat cache on purpose
57
+ }
58
+ catch (err) {
59
+ return fail(errorMessage(err)); // broken file: never overwrite it with defaults
60
+ }
61
+ const mutated = mutate(cfg);
62
+ // An unset that finds nothing to remove returns the loaded config unchanged. Write nothing:
63
+ // re-serializing the merged config would reformat tumwater.json (defaults expanded) and
64
+ // break the no-op's "changes nothing" contract. The bytes stay identical; validation of an
65
+ // already-validated config is unnecessary.
66
+ if (mutated === cfg)
67
+ return { ok: true };
68
+ // Collapse legacy/new pi spellings before writing so a merged config (which always
69
+ // carries the default piArgs) never lands both forms in the file.
70
+ const candidate = collapseBackendAliases(mutated);
71
+ try {
72
+ validateConfig(candidate); // throws listing every problem — nothing is written on failure
73
+ }
74
+ catch (err) {
75
+ return fail(errorMessage(err));
76
+ }
77
+ try {
78
+ // The trailing newline is tumwater.json's convention (POSIX text file).
79
+ writeJsonAtomic(configPath(root), candidate, true);
80
+ }
81
+ catch (err) {
82
+ return fail(errorMessage(err));
83
+ }
84
+ return { ok: true };
85
+ });
86
+ }
87
+ catch (err) {
88
+ return fail(errorMessage(err)); // lock acquire timeout/failure — nothing written
89
+ }
90
+ }
91
+ /** A shallow copy of `obj` without `key` — the copy each `unsetConfigKey` branch edits
92
+ * before it decides whether the removal emptied its container, and the shape the branch then
93
+ * returns when it did (an emptied map, the last field of a `roles.<id>` entry, an emptied
94
+ * `model` map), so tumwater.json never keeps an empty object. */
95
+ export function without(obj, key) {
96
+ const copy = { ...obj };
97
+ delete copy[key];
98
+ return copy;
99
+ }
100
+ /** The raw parsed tumwater.json as a plain object — the pre-defaults view `unsetConfigKey`'s
101
+ * presence check needs, because the merged config always carries defaults a top-level key or a
102
+ * `roles.<id>.<field>` (e.g. the seeded `minRunIntervalSeconds`) may never have been set to.
103
+ * Read inside writeConfigMutation's lock; a missing or unparsable file is `{}` (loadConfig has
104
+ * already rejected a broken file before a mutation runs, and an absent file is the bare
105
+ * defaults — there is nothing to retract). */
106
+ export function rawConfigObject(root) {
107
+ const { raw } = parseJsonConfig(configPath(root), CONFIG_BASENAME);
108
+ return objectOrEmpty(raw);
109
+ }
110
+ /** Reinstall the raw tumwater.json's `roles` section onto a whole-config write candidate (or
111
+ * drop it when the file had none), so a bare `config set`/`unset` of an unrelated key never
112
+ * re-expands the file's roles to the built-in catalog. `roles` merges per-entry at load, so
113
+ * this is about the bytes the operator can read and edit, not the resolved view: omission
114
+ * still cannot disable a role, and a partial section stays exactly as written. */
115
+ export function preserveFileRoles(root, candidate) {
116
+ const record = candidate;
117
+ const rawRoles = rawConfigObject(root).roles;
118
+ if (rawRoles === undefined)
119
+ delete record.roles;
120
+ else
121
+ record.roles = rawRoles;
122
+ }
123
+ /** preserveFileRoles, returning the candidate so a whole-config write branch can
124
+ * `return preservingFileRoles(root, next)` after building it. */
125
+ export function preservingFileRoles(root, candidate) {
126
+ preserveFileRoles(root, candidate);
127
+ return candidate;
128
+ }
129
+ /** The config with top-level `key` set to `section`, or removed when `section` is empty — the
130
+ * unset shape for the per-role maps, `model` tiers and `backends`, which never leave an empty
131
+ * object in the file. A whole-config write, so the file's own roles section is kept. */
132
+ export function withSectionOrDropped(root, cfg, key, section) {
133
+ const next = Object.keys(section).length === 0 ? without(cfg, key) : { ...cfg, [key]: section };
134
+ return preservingFileRoles(root, next);
135
+ }