tumwater 0.2.0 → 0.3.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 (446) 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 +42 -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 +4 -3
  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 +402 -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 +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -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 +1 -2
  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 +118 -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 +150 -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 +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. 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,6 +162,7 @@ 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
  }
@@ -130,25 +194,22 @@ export function configForRole(config, role) {
130
194
  minRunIntervalSeconds: rc.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
131
195
  };
132
196
  }
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) {
197
+ /** The config a role's runs SCHEDULE from: configForRole, except that while PLANS.md still
198
+ * has planning room (planBacklogOpen — below the plan role's eligible-plan target, or holding
199
+ * a plan that needs review/needs replan) the plan role schedules with a ZERO min gap, so a
200
+ * productive plan run reruns on the next poll and an unproductive one drops to the idle
201
+ * backoff. The plan role's own machine signal is `planBacklogFull` (src/scheduling/claims.ts):
202
+ * a fresh project's empty backlog is not full, so this subsumes the old placeholder-status
203
+ * trigger. One home for the rule: isEligible enforces the same gap when it decides whether
204
+ * plan may start, and runner-run.ts hands this config to applyRunOutcome so nextRunAt is
205
+ * scheduled from the same figure instead of the role default. Every other role, and the plan
206
+ * role once the target is met (which already holds the run via the plans-full deferral),
207
+ * schedule configForRole unchanged. */
208
+ export function scheduleConfigForRole(config, role, planBacklogOpen = false) {
145
209
  const resolved = configForRole(config, role);
146
- if (descriptionIncomplete && baseRoleOf(role) === "plan") {
210
+ if (planBacklogOpen && baseRoleOf(role) === "plan") {
147
211
  return { ...resolved, minRunIntervalSeconds: 0 };
148
212
  }
149
- if (bootstrapActive && baseRoleOf(role) === "plan") {
150
- return { ...resolved, minRunIntervalSeconds: config.minRunIntervalSeconds };
151
- }
152
213
  return resolved;
153
214
  }
154
215
  /** The config as seen by the review gate's pi runs: the top-level `review` section's
@@ -173,10 +234,10 @@ export function resolverConfig(config) {
173
234
  return withModelOverrides(config, "strong", {});
174
235
  }
175
236
  /** 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
237
+ * the merge slot the whole queue waits on, and its only other limits were the run's budget
177
238
  * and the quiet watchdog: reviews measured 2026-09-22/23 ran 2.3 min median, 9.2 min p90 and
178
239
  * 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. */
240
+ * kills a wedged one in minutes instead of the run's hours. */
180
241
  export const REVIEW_TIMEOUT_S = 900;
181
242
  /** The reviewer's wall-clock floor while the budget fallback carries the team. The free model
182
243
  * is typically local and far slower per turn than the budgeted one it replaces, but a review
@@ -189,7 +250,7 @@ export const REVIEW_TIMEOUT_S = 900;
189
250
  export const FALLBACK_REVIEW_TIMEOUT_S = 3600;
190
251
  /** The config as seen by the gate's reviewer run: the reviewer's model wiring (reviewConfig)
191
252
  * 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
253
+ * the run's. A smaller runTimeoutSeconds still wins. A still-progressing gate run gets one
193
254
  * extension of the same length (src/pi/pi-watchdogs.ts), so the effective ceiling on a large
194
255
  * diff's review is twice this budget. */
195
256
  export function reviewRunConfig(config) {
@@ -221,21 +282,21 @@ export function resolvedModelFields(provider, model, thinking) {
221
282
  * provider/model/thinking triple formatted as one selector string, or an empty object when
222
283
  * no model id is resolved (pi's own default) — the key stays absent rather than empty, the
223
284
  * 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
285
+ * that assembly and guard, shared by the two run-start event writers (runner-run.ts's run_start,
286
+ * review.ts's review_start) and the per-role status snapshot (status-data.ts) so the three
287
+ * observability surfaces cannot disagree about which model a run starts on. A non-pi backend is
288
+ * named as the selector's provider namespace (backendQualifiedSelector), so a claude run reads
228
289
  * `claude/claude-sonnet-5-5` in every surface instead of a bare model id. */
229
290
  export function modelSelectorField(cfg) {
230
291
  const { model } = cfg;
231
- return model ? { model: agentQualifiedSelector({ ...cfg, model }) } : {};
292
+ return model ? { model: backendQualifiedSelector({ ...cfg, model }) } : {};
232
293
  }
233
- /** The resolved seam as one selector string, agent-qualified: a non-pi backend id is the
294
+ /** The resolved seam as one selector string, backend-qualified: a non-pi backend id is the
234
295
  * 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
296
+ * into that backend; a pi seam keeps `provider/model[:thinking]` byte-identical to before, since
236
297
  * pi's providers are real model providers, not a backend namespace. `model` is required — the
237
298
  * caller has already guarded that a model resolved (modelSelectorField). */
238
- export function agentQualifiedSelector(cfg) {
299
+ export function backendQualifiedSelector(cfg) {
239
300
  const base = formatModelSelector({ provider: cfg.provider, model: cfg.model, thinking: cfg.thinking });
240
301
  return cfg.agent && cfg.agent !== "pi" ? `${cfg.agent}/${base}` : base;
241
302
  }
@@ -246,9 +307,9 @@ export function agentQualifiedSelector(cfg) {
246
307
  export function seamHoldKey(cfg) {
247
308
  return cfg.agent !== undefined && cfg.agent !== "pi" ? cfg.agent : cfg.provider;
248
309
  }
249
- /** The `modelPairName` a resolved seam's run is charged under (LoopState.dayCostByPair): the
310
+ /** The `modelPairName` a resolved seam's run is charged under (RunnerState.dayCostByPair): the
250
311
  * 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
312
+ * pairs carry it (config-fallbacks.ts's seamKeyedPairFields), so a run and the chain
252
313
  * step it ran on name one key. */
253
314
  export function seamPairName(cfg) {
254
315
  return modelPairName({ provider: seamHoldKey(cfg), model: cfg.model });
@@ -273,18 +334,31 @@ export function seamConcurrencyKey(cfg, piDefault) {
273
334
  * event: null when no tier resolves to a model (pi's own default), the single selector when
274
335
  * every set tier agrees (the string-form `model`), and the `small=…, default=…, strong=…`
275
336
  * 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
337
+ * label. Each tier's label is agent-qualified (backendQualifiedSelector), so flipping the
277
338
  * top-level `agent` over a bare-id model relabels every tier and the reload now emits
278
339
  * `model_changed` — a team's backend is part of what it runs (PLANS.md "Mixed claude + pi
279
340
  * fleets, part 3/4"), and an edit that moves a bare id to another backend must not read as
280
341
  * "nothing changed". */
281
342
  export function teamModelLabel(config) {
282
343
  const entries = MODEL_TIERS.map((tier) => {
283
- const sel = tierModel(config, tier);
284
- if (!sel)
344
+ const { sel, backend, thinking } = resolveTopTier(config, tier);
345
+ if (!sel && backend === undefined)
346
+ return null;
347
+ const routed = routeModel(sel?.provider, sel?.model, config.backend ?? "pi", tier, backend);
348
+ if (routed.model === undefined)
285
349
  return null;
286
- const { agent } = routeModel(sel.provider, sel.model, config.backend ?? "pi", tier);
287
- return { tier, label: agentQualifiedSelector({ ...sel, agent }) };
350
+ // routeModel consumed an agent-naming provider (or an explicit backend) as the agent; keep
351
+ // the provider only for a pi seam so the label doesn't prefix the agent twice.
352
+ const provider = routed.agent === "pi" ? sel?.provider : undefined;
353
+ return {
354
+ tier,
355
+ label: backendQualifiedSelector({
356
+ provider,
357
+ model: routed.model,
358
+ thinking: sel?.thinking ?? thinking,
359
+ agent: routed.agent,
360
+ }),
361
+ };
288
362
  }).filter((e) => e !== null);
289
363
  if (entries.length === 0)
290
364
  return null;
@@ -293,18 +367,18 @@ export function teamModelLabel(config) {
293
367
  return entries.map((e) => `${e.tier}=${e.label}`).join(", ");
294
368
  }
295
369
  /** 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`,
370
+ * fleets, part 3/4" gap 7): a pair routed to a non-pi backend — its `backend`, per
371
+ * seamKeyedPairFields, so `claude` — renders as `claude/claude-sonnet-5-5`,
298
372
  * the same agent-qualified form every other surface carries; a pi pair keeps the bare model
299
373
  * byte-identical to before (the badge, the team alert and `tumwater role` all name the
300
374
  * model a pi fallback runs, never its provider). Falls back through the provider to the
301
375
  * caller's `<agent> default` label when both halves are absent. One home so the three display
302
376
  * surfaces cannot render one fallback two ways. */
303
- export function fallbackDisplayPair(pair, agentDefault) {
377
+ export function fallbackDisplayPair(pair, backendDefault) {
304
378
  const p = pair ?? {};
305
379
  if (p.model !== undefined)
306
- return p.provider !== undefined && isAgentId(p.provider) ? `${p.provider}/${p.model}` : p.model;
380
+ return p.backend !== undefined && p.backend !== "pi" ? `${p.backend}/${p.model}` : p.model;
307
381
  if (p.provider !== undefined)
308
382
  return p.provider;
309
- return agentDefault;
383
+ return backendDefault;
310
384
  }
@@ -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
+ }