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,22 +1,24 @@
1
- // vocabulary: plain
2
- import { AGENT_IDS, AGENT_KEYS, CLAUDE_AGENT_KEYS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, CUSTOM_LOOP_KEYS, CUSTOM_NAME_RE, CUSTOM_TASK_MAX_CHARS, FALLBACK_STEP_KEYS, HARNESS_PI_FLAGS, MODEL_TRIPLE_KEYS, REVIEW_KEYS, ROLE_ENTRY_KEYS, TOP_LEVEL_KEYS, ROLE_INSTRUCTIONS_MAX_CHARS, TIER_MAP_KEYS, } from "./config-schema.js";
3
- import { allRoleIds } from "../roles/roles.js";
4
- import { INSTANCE_ROLES, baseRoleOf } from "../roles/loop-ids.js";
1
+ import { MODEL_TIERS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, CUSTOM_ROLE_KEYS, CUSTOM_NAME_RE, CUSTOM_TASK_MAX_CHARS, REVIEW_KEYS, ROLE_ENTRY_KEYS, TOP_LEVEL_KEYS, ROLE_INSTRUCTIONS_MAX_CHARS, } from "./config-schema.js";
2
+ import { DIRECTOR_ROLE, allRoleIds } from "../roles/roles.js";
3
+ import { INSTANCE_ROLES, baseRoleOf } from "../roles/runner-ids.js";
4
+ import { isHarnessCodexConfigKey } from "../agent/codex/codex-args.js";
5
5
  import { isJsonObject } from "../files/json-object.js";
6
6
  import { expandHomeTilde } from "../files/files.js";
7
7
  import os from "node:os";
8
8
  import path from "node:path";
9
9
  import { isNonBlankString, tooLongMessage } from "../text/text.js";
10
- import { typoSuffix } from "../text/suggest.js";
11
10
  import { PREFILL_REASON } from "../text/phrases.js";
12
11
  import { parseQuietHours } from "../scheduling/quiet-hours.js";
13
- import { isAgentId, parseModelSelector } from "./model-selector.js";
12
+ import { isBackendId } from "./model-selector.js";
14
13
  import { TUMWATER_SOURCE } from "../events/events.js";
15
- import { reservedPassthroughFlag } from "../agent/backend-traits.js";
16
- import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkKnownRoleId, checkModelTripleField, checkNumberField, checkObjectSection, checkReservedArgs, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP, MAX_DURATION_SECONDS, POSITIVE, POSITIVE_INTEGER, show, typeName, } from "./config-field-checks.js";
14
+ import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkKnownRoleId, checkModelTripleField, checkNumberField, checkObjectSection, codexConfigKey, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP, MAX_DURATION_SECONDS, POSITIVE, POSITIVE_INTEGER, show, typeName, } from "./config-field-checks.js";
15
+ import { checkEntryBackend, checkRetiredSelector, checkSelectorHalves, } from "./config-selector-checks.js";
16
+ import { validateAgentsBlock } from "./config-validation-agents.js";
17
+ import { validateFallbackBlock, validateModelBlock, } from "./config-validation-model.js";
17
18
  /** Schema validation for tumwater.json: validateConfig, the gate every load and save passes
18
- * through (config.ts's load/save, config-write.ts's budget editing and director config
19
- * requests), judged against the key allow-lists and value shapes that live beside the types
19
+ * through (config.ts's load/save, config-write.ts's budget editing and
20
+ * config-request.ts's director config requests), judged against the key allow-lists and
21
+ * value shapes that live beside the types
20
22
  * they mirror in config-schema.ts. Split out of config.ts — which keeps defaultConfig and the
21
23
  * read side (load/save/cache, per-role views) — because this is a self-contained concern:
22
24
  * it depends only on the schema, the role catalog (allRoleIds), and message wording, not on
@@ -38,7 +40,7 @@ const DURATION_OR_DISABLED = {
38
40
  };
39
41
  /** A per-role instance count (`roles.feature.instances`): an integer from 1 to 8. Bounded so
40
42
  * a typo like `instances: 800` cannot spawn hundreds of runners, and integer so
41
- * `feature-1.5` — which no loop id can name — is refused. */
43
+ * `feature-1.5` — which no role id can name — is refused. */
42
44
  const INSTANCE_COUNT = {
43
45
  ok: (n) => Number.isInteger(n) && n >= 1 && n <= 8,
44
46
  what: "an integer from 1 to 8",
@@ -66,8 +68,8 @@ function checkPerRoleMap(r, key, expectation, customNames, problems, checkValue)
66
68
  /** Validate tumwater.json values, so a typo fails fast with an actionable message instead
67
69
  * of misbehaving at runtime — e.g. a non-numeric runTimeoutSeconds becomes NaN and kills
68
70
  * every pi run instantly, a non-numeric logMaxBytes rotates the event log on every write, an
69
- * unknown role id (a misspelled entry under `roles`) spawns a phantom loop that errors every
70
- * tick, and an unknown key is silently ignored so the intended setting never takes effect.
71
+ * unknown role id (a misspelled entry under `roles`) spawns a phantom role that errors every
72
+ * run, and an unknown key is silently ignored so the intended setting never takes effect.
71
73
  * Collects every problem so one edit can fix them all; throws a single Error listing them.
72
74
  * `label` names the file in the thrown messages — validateConfig also gates the tracked
73
75
  * example template, whose problems must not be misreported as tumwater.json's.
@@ -90,83 +92,91 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
90
92
  const checkNumber = (obj, prefix, key, rule) => checkNumberField(problems, obj, prefix, key, rule);
91
93
  const checkBoolean = (obj, prefix, key) => checkBooleanField(problems, obj, prefix, key);
92
94
  const checkStringArray = (obj, prefix, key) => checkStringArrayField(problems, obj, prefix, key);
95
+ // claude's and codex's per-backend config dir (plans/notes-command.md "Keeping funes out
96
+ // of team runs" and plans/codex-backend.md §4): a leading `~` expands; the value must be an
97
+ // absolute path after expansion, or claude would resolve a relative CLAUDE_CONFIG_DIR (and
98
+ // codex a relative CODEX_HOME) from the run's cwd and write transcripts somewhere
99
+ // unexpected. Only claude and codex accept the key, so it is inert nowhere else. The nested
100
+ // reserve spelling (part 2b/9): the same 0 < f ≤ 1 rule the top-level `claudeWindowReserve`
101
+ // map gets, so a bad entry can never silently no-op a reserve.
102
+ const checkBackendConfig = (entry, prefix) => {
103
+ checkString(entry, `${prefix}.`, "configDir", false);
104
+ if (typeof entry.configDir === "string" && entry.configDir !== "")
105
+ if (!path.isAbsolute(expandHomeTilde(entry.configDir, os.homedir())))
106
+ problems.push(`${prefix}.configDir must be an absolute path after ~ expansion (got ${show(entry.configDir)})`);
107
+ if (entry.windowReserve === undefined)
108
+ return;
109
+ const wr = entry.windowReserve;
110
+ if (!isJsonObject(wr)) {
111
+ problems.push(`${prefix}.windowReserve must be an object mapping window types to fractions (got ${show(wr)})`);
112
+ return;
113
+ }
114
+ for (const [type, value] of Object.entries(wr)) {
115
+ if (type.trim() === "")
116
+ problems.push(`${prefix}.windowReserve.<empty> — a window type must not be empty`);
117
+ else if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1)
118
+ problems.push(`${prefix}.windowReserve.${type} must be a fraction above 0 and at most 1 (got ${show(value)})`);
119
+ }
120
+ };
121
+ // codex's `-c <key>=…` / `--config <key>=…` passthrough (plans/codex-backend.md §2/§3): a
122
+ // key tumwater sets on every run (HARNESS_CODEX_CONFIG_KEYS) — or one the write guard owns,
123
+ // `hooks` / `hooks.<…>` (part 3/9) — is appended AFTER the harness's own `-c` and Codex's
124
+ // parser is last-wins, so it would silently override the model, effort, sandbox mode,
125
+ // telemetry or the write guard the harness resolved. The standalone-flag screen above
126
+ // (reservedPassthroughFlag + checkReservedArgs) splits on `=` and cannot see a separate
127
+ // `-c` token, so codex gets its own arity-aware scan of the key. Any other key is the
128
+ // operator's own and passes.
129
+ const checkCodexConfigKeys = (entry, prefix) => {
130
+ const args = entry.args;
131
+ for (let i = 0; i < args.length; i++) {
132
+ const arg = args[i];
133
+ if (typeof arg !== "string")
134
+ continue;
135
+ const key = codexConfigKey(arg, typeof args[i + 1] === "string" ? args[i + 1] : undefined);
136
+ if (key !== undefined && isHarnessCodexConfigKey(key))
137
+ problems.push(`${prefix}.args[${i}] ${show(arg)} passes the config key ${key}, which tumwater manages — drop it or use another key`);
138
+ }
139
+ };
93
140
  const r = raw; // Narrowed to an object by isJsonObject above.
94
141
  checkKnownKeys(r, TOP_LEVEL_KEYS, label, problems);
95
142
  // The top level's `model` may be a selector string or a map by tier (plans/model-tiers.md);
96
143
  // provider/thinking validate as before. A string selector that parses to an empty provider
97
144
  // or model half is reported separately (checkSelectorHalves).
98
- checkModelTriple(r, "", "tier-map");
99
- checkSelectorHalves(r, "", "model", r.provider, problems);
100
- // A map's entries are selector strings too, resolved by config-views' topTierSelector with
101
- // parseModelSelector; the keyed check above sees only the object, so each entry runs the
102
- // same empty-half check (the map form cannot coexist with a legacy provider, so it parses
103
- // as a pure selector).
104
- if (isJsonObject(r.model))
105
- for (const [tier, sel] of Object.entries(r.model))
106
- checkSelectorValue(`model.${tier}`, sel, undefined, problems);
107
- // An empty baseBranch would silently fall back to the checked-out branch — the one value
108
- // the operator's explicit setting must never degrade to unannounced.
109
- checkString(r, "", "baseBranch", false);
110
- // An empty agentBin would silently fall back to "pi" — the same silent-ignore class: the
111
- // operator named a binary, so a blank value must fail validation, not run the default.
112
- checkString(r, "", "agentBin", false);
113
- // The agent backend for bare/no-model selectors (plans/agent-backends.md "Config"): a
114
- // non-blank id from the schema's reserved set. An unknown id would fall back to pi at run
115
- // time (src/agent/run-agent.ts), silently running the wrong backend against a named one.
116
- checkString(r, "", "backend", false);
117
- if (typeof r.backend === "string" && !isAgentId(r.backend))
118
- problems.push(`backend must name a backend (${AGENT_IDS.join(", ")}; got ${show(r.backend)})${typoSuffix(r.backend, AGENT_IDS)}`);
119
- checkStringArray(r, "", "piArgs");
120
- // A piArgs entry that repeats a harness-managed flag is appended after the harness's own and
121
- // pi's parser is last-wins, so it silently overrides it (see HARNESS_PI_FLAGS). Name the
122
- // position and the flag so one edit removes it.
123
- if (Array.isArray(r.piArgs))
124
- checkReservedArgs(problems, r.piArgs, "piArgs", (flag) => HARNESS_PI_FLAGS.get(flag));
125
- // Agent backends (plans/agent-backends.md "Config"): each entry is an object with an
126
- // optional non-empty `bin` string and an optional array-of-strings `args`. A passthrough arg
127
- // that repeats a flag the backend's own harness invocation sets is appended last and wins,
128
- // so it silently overrides the harness — refused here for the two backends whose reserved
129
- // sets are known (claude-args.ts HARNESS_CLAUDE_FLAGS, config-schema.ts HARNESS_PI_FLAGS).
130
- checkObjectSection(r, "backends", problems, (o) => {
131
- for (const [id, entry] of Object.entries(o)) {
132
- const prefix = `backends.${id}`;
133
- if (!isJsonObject(entry)) {
134
- problems.push(`${prefix} must be an object with optional bin and args (got ${show(entry)})`);
135
- continue;
136
- }
137
- checkKnownKeys(entry, id === "claude" ? CLAUDE_AGENT_KEYS : AGENT_KEYS, prefix, problems);
138
- checkString(entry, `${prefix}.`, "bin", false);
139
- checkStringArray(entry, `${prefix}.`, "args");
140
- // claude's per-backend config dir (plans/notes-command.md "Keeping funes out of team runs"):
141
- // a leading `~` expands; the value must be an absolute path after expansion, or claude would
142
- // resolve a relative CLAUDE_CONFIG_DIR from the tick's cwd and write transcripts somewhere
143
- // unexpected. Only claude accepts the key (CLAUDE_AGENT_KEYS), so it is inert nowhere.
144
- if (id === "claude") {
145
- checkString(entry, `${prefix}.`, "configDir", false);
146
- if (typeof entry.configDir === "string" && entry.configDir !== "")
147
- if (!path.isAbsolute(expandHomeTilde(entry.configDir, os.homedir())))
148
- problems.push(`${prefix}.configDir must be an absolute path after ~ expansion (got ${show(entry.configDir)})`);
149
- }
150
- if (!Array.isArray(entry.args))
151
- continue; // Already reported by checkStringArray.
152
- checkReservedArgs(problems, entry.args, `${prefix}.args`, (flag) => reservedPassthroughFlag(id, flag));
153
- }
145
+ // The backend a selector with no `backend` of its own resolves to (retired-spelling checks).
146
+ const topBackend = typeof r.backend === "string" ? r.backend : "pi";
147
+ // The backend a tier's primary resolves to, which that tier's fallback steps inherit.
148
+ const tierBackend = (tier) => {
149
+ // A tier left out of the map takes the `default` entry, as resolveTopTier does.
150
+ const e = isJsonObject(r.model) ? (r.model[tier] ?? r.model.default) : undefined;
151
+ return isJsonObject(e) && typeof e.backend === "string" ? e.backend : topBackend;
152
+ };
153
+ // A string or list `fallback` serves every tier, each inheriting its own tier's backend, so it
154
+ // is checked against each distinct one; the same message from two backends is reported once.
155
+ const eachTierBackend = (check) => {
156
+ const out = [];
157
+ for (const b of new Set(MODEL_TIERS.map(tierBackend)))
158
+ check(b, out);
159
+ for (const m of new Set(out))
160
+ problems.push(m);
161
+ };
162
+ // The retired spelling doubled a backend id as the provider namespace, so a `provider` that IS
163
+ // a backend id other than `pi` on a section that otherwise runs on pi is the old back-compat
164
+ // form: configForRole still resolves the section to pi and hands the bare provider name to
165
+ // pi, which has no such provider — reject it with the edit the step check names (write the
166
+ // backend beside the model, drop the provider). checkRetiredSelector covers the no-provider
167
+ // spellings; this catches the pair a provider in scope used to make valid.
168
+ const checkLegacyProvider = (provider, at, backend) => {
169
+ if (typeof provider === "string" && isBackendId(provider) && provider !== "pi" && backend === "pi")
170
+ problems.push(`${at}provider "${provider}" is the retired backend namespace but this section runs on pi — write backend: "${provider}" beside the model and drop the provider`);
171
+ };
172
+ validateModelBlock(r, problems, { checkModelTriple, checkString, topBackend, checkLegacyProvider });
173
+ validateAgentsBlock(r, problems, rawFile, {
174
+ checkString,
175
+ checkStringArray,
176
+ checkKnownKeys,
177
+ checkBackendConfig,
178
+ checkCodexConfigKeys,
154
179
  });
155
- // Legacy back-compat (plans/agent-backends.md "Back-compat"): a file must not spell one pi
156
- // field both ways. This is a property of the FILE's own keys — the merged config always
157
- // carries a default `piArgs`, so only the raw pass (rawFile) can tell an explicit legacy
158
- // value from the default. The effective value resolves through config-views.ts
159
- // resolvedAgentConfig; a load-time mirror into `agents` would instead leak a duplicate
160
- // spelling into every later config write.
161
- if (rawFile && isJsonObject(r.backends)) {
162
- const pi = r.backends.pi;
163
- if (isJsonObject(pi)) {
164
- if (r.agentBin !== undefined && pi.bin !== undefined)
165
- problems.push("agentBin and backends.pi.bin set the same value — keep one (backends.pi.bin is the current spelling; agentBin is its old name)");
166
- if (r.piArgs !== undefined && pi.args !== undefined)
167
- problems.push("piArgs and backends.pi.args set the same value — keep one (backends.pi.args is the current spelling; piArgs is its old name)");
168
- }
169
- }
170
180
  checkNumber(r, "", "maxConcurrent", POSITIVE_INTEGER);
171
181
  checkNumber(r, "", "mergeBatchMax", POSITIVE_INTEGER);
172
182
  checkNumber(r, "", "maxConcurrentChecks", POSITIVE_INTEGER);
@@ -204,7 +214,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
204
214
  // New-project bootstrap (plans/work-ratio.md, "New-project bootstrap"): an absent section
205
215
  // means no bootstrap. `untilPlansDone` is a count of plans that must reach `## Done`, so it is
206
216
  // an integer of at least 1 — 0 or a fraction can never be reached and would hold the
207
- // maintenance loops forever.
217
+ // maintenance roles forever.
208
218
  checkObjectSection(r, "bootstrap", problems, (o) => {
209
219
  checkKnownKeys(o, BOOTSTRAP_KEYS, "bootstrap", problems);
210
220
  checkNumber(o, "bootstrap.", "untilPlansDone", POSITIVE_INTEGER);
@@ -233,6 +243,9 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
233
243
  // gateCommand is a string like command, but blank means off: falling back from it runs
234
244
  // the FULL check at the gate — the stronger check, never a silently weaker one.
235
245
  checkString(o, "check.", "gateCommand");
246
+ // mergeCommand likewise: an absent key falls back to the gate command (Fast merge
247
+ // "Defaults"), and a blank string means off, which runs the full check in the merge lock.
248
+ checkString(o, "check.", "mergeCommand");
236
249
  checkString(o, "check.", "cwd");
237
250
  checkNumber(o, "check.", "timeoutSeconds", DURATION_POSITIVE);
238
251
  });
@@ -246,6 +259,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
246
259
  checkKnownKeys(o, REVIEW_KEYS, "review", problems);
247
260
  checkBoolean(o, "review.", "enabled");
248
261
  checkStringArray(o, "review.", "exemptPaths");
262
+ checkStringArray(o, "review.", "afterMerge");
249
263
  // An exemption pattern is matched against a repo-relative path (exemptions.ts), so a
250
264
  // pattern shaped like something git never emits can never match: the diff it was meant
251
265
  // to exempt gets a model review anyway, and the operator never learns the pattern was
@@ -269,117 +283,21 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
269
283
  }
270
284
  checkModelTriple(o, "review.", "tier-name");
271
285
  checkSelectorHalves(o, "review.", "model", o.provider ?? r.provider, problems);
286
+ checkEntryBackend(o, "review", problems);
287
+ if ((o.provider ?? r.provider) === undefined)
288
+ checkRetiredSelector("review.model", o.model, typeof o.backend === "string" ? o.backend : topBackend, problems);
289
+ checkLegacyProvider(o.provider ?? r.provider, "review.", typeof o.backend === "string" ? o.backend : topBackend);
272
290
  checkNumber(o, "review.", "timeoutSeconds", DURATION_POSITIVE);
273
291
  });
274
- // The free fallback model (plans/fallback-model.md): shape only — whether the named pair is
275
- // actually cost-free is a question about pi's models.json, not about this file, so it is
276
- // answered at run time (src/pi/pi-models.ts) rather than failing a load here. An empty object is
277
- // rejected: it names nothing, so it would silently never engage.
278
- // The selector-string shorthand `fallback` (plans/model-tiers.md) validates as one non-empty
279
- // string; the map form takes per-tier entries whose values are selector strings or "pause"
280
- // (each tier's entry is consulted when that tier runs at the cap; part 5a/8). Setting both
281
- // forms — or the map form and `fallbackModel` — is an error naming both keys, since which
282
- // one wins is not a question the file should ever pose.
283
- if ("fallback" in r && r.fallback !== undefined) {
284
- const f = r.fallback;
285
- if (isJsonObject(f)) {
286
- checkKnownKeys(f, TIER_MAP_KEYS, "fallback", problems);
287
- for (const [tier, v] of Object.entries(f)) {
288
- if (Array.isArray(v))
289
- checkFallbackList(`fallback.${tier}`, v, problems);
290
- else if (isJsonObject(v))
291
- checkFallbackStep(`fallback.${tier}`, v, problems);
292
- else if (!isNonBlankString(v))
293
- problems.push(`fallback.${tier} must be a non-empty model id or "pause" (got ${show(v)})`);
294
- else if (v !== "pause")
295
- // An entry is a pure selector (config-views' fallbackSelectorFields parses it with
296
- // no legacy provider in scope); "pause" names no model and stays valid.
297
- checkSelectorValue(`fallback.${tier}`, v, undefined, problems);
298
- }
299
- }
300
- else if (Array.isArray(f)) {
301
- checkFallbackList("fallback", f, problems);
302
- }
303
- else if (!isNonBlankString(f)) {
304
- problems.push(`fallback must be a model id or a map by tier (got ${show(f)})`);
305
- }
306
- else if (f === "pause") {
307
- problems.push(`fallback "pause" is only valid as a tier map value — a string fallback must name a model id`);
308
- }
309
- else {
310
- // A selector-string fallback still parses to halves: report an empty one.
311
- checkSelectorHalves({ fallback: f }, "", "fallback", undefined, problems);
312
- }
313
- }
314
- if ("fallback" in r && "fallbackModel" in r)
315
- problems.push(`fallback and fallbackModel both name the budget fallback — keep only one (got fallback ${show(r.fallback)} and fallbackModel ${show(r.fallbackModel)})`);
316
- checkObjectSection(r, "fallbackModel", problems, (o) => {
317
- checkKnownKeys(o, MODEL_TRIPLE_KEYS, "fallbackModel", problems);
318
- checkModelTriple(o, "fallbackModel.");
319
- if (!("provider" in o) && !("model" in o))
320
- problems.push(`fallbackModel must name a provider or a model (got ${show(o)})`);
292
+ validateFallbackBlock(r, problems, {
293
+ checkModelTriple,
294
+ checkLegacyProvider,
295
+ tierBackend,
296
+ eachTierBackend,
321
297
  });
322
- /** A selector string that parses to an empty provider or model half (`"/id"`, `"p/"`) passes
323
- * the non-empty-string rule but never reaches pi: piArgs skips an empty value when it builds
324
- * its flags (config-field-checks' model-triple note), and fallbackPair drops an empty half, so
325
- * the team silently runs pi's default model — the same silent-ignore class the empty-string
326
- * rules exist to prevent. Checked only when no legacy provider is in scope: under one, the
327
- * whole string is the id (config-views' parse), so a `/` in it is ordinary text and the parse
328
- * cannot produce an empty half. */
329
- function checkSelectorHalves(obj, prefix, key, legacyProvider, problems) {
330
- checkSelectorValue(`${prefix}${key}`, obj[key], legacyProvider, problems);
331
- }
332
- /** Validate a `fallback` list (a tier's ordered chain): non-empty, each element a step — a
333
- * non-blank model id that is not `"pause"` (a whole-tier value only) and parses to both
334
- * halves, or a `{ model, maxDailyCostUsd? }` object (checkFallbackStep). */
335
- function checkFallbackList(name, list, problems) {
336
- if (list.length === 0)
337
- problems.push(`${name} list must not be empty — name at least one model id`);
338
- list.forEach((el, i) => {
339
- const at = `${name}[${i}]`;
340
- if (isJsonObject(el))
341
- checkFallbackStep(at, el, problems);
342
- else if (!isNonBlankString(el))
343
- problems.push(`${name} list element ${i} must be a non-empty model id or a step object (got ${show(el)})`);
344
- else if (el === "pause")
345
- problems.push(`${name} list element ${i} is "pause" — "pause" is only valid as a whole-tier value`);
346
- else
347
- checkSelectorValue(at, el, undefined, problems);
348
- });
349
- }
350
- /** Validate one object step `{ model, maxDailyCostUsd? }`: only those keys, `model` a non-blank
351
- * selector (not `"pause"`) and `maxDailyCostUsd`, when present, a positive finite number. */
352
- function checkFallbackStep(at, step, problems) {
353
- checkKnownKeys(step, FALLBACK_STEP_KEYS, at, problems);
354
- const m = step.model;
355
- if (!isNonBlankString(m))
356
- problems.push(`${at}.model must be a non-empty model id (got ${show(m)})`);
357
- else if (m === "pause")
358
- problems.push(`${at}.model is "pause" — "pause" is only valid as a whole-tier value`);
359
- else
360
- checkSelectorValue(`${at}.model`, m, undefined, problems);
361
- const cap = step.maxDailyCostUsd;
362
- if (cap !== undefined && !(typeof cap === "number" && Number.isFinite(cap) && cap > 0))
363
- problems.push(`${at}.maxDailyCostUsd must be a positive number (got ${show(cap)})`);
364
- }
365
- /** Report one selector string that parses to an empty provider or model half. `name` is the
366
- * config path the message names — `model`, a map entry's `model.small`, a section's
367
- * `roles.feature.model`. Split out of checkSelectorHalves so the map forms, which enumerate
368
- * their entries instead of looking one key up on an object, run the identical check: a
369
- * selector's empty half is the same silent-ignore class wherever the string lives. */
370
- function checkSelectorValue(name, v, legacyProvider, problems) {
371
- if (!isNonBlankString(v))
372
- return;
373
- const legacy = isNonBlankString(legacyProvider) ? legacyProvider : undefined;
374
- const sel = parseModelSelector(v, legacy);
375
- if (sel.provider?.trim() === "")
376
- problems.push(`${name} ${show(v)} parses to an empty provider half — pi would silently run its default model`);
377
- if (sel.model.trim() === "")
378
- problems.push(`${name} ${show(v)} parses to an empty model half — pi would silently run its default model`);
379
- }
380
- // Custom loops (plans/user-defined-loops.md): names become worktree dirs and git refs, so
298
+ // Custom roles (plans/user-defined-loops.md): names become worktree dirs and git refs, so
381
299
  // they are validated strictly — a colliding name would silently shadow a built-in's prompt
382
- // (roleById wins in tickPrompt), validation makes that impossible instead of subtle. The
300
+ // (roleById wins in runPrompt), validation makes that impossible instead of subtle. The
383
301
  // collected names feed the roles cross-check below: an id under `roles` is valid when it is
384
302
  // in the catalog OR listed as a customRoles name.
385
303
  const customNames = new Set();
@@ -395,7 +313,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
395
313
  problems.push(`${where} must be an object with keys name and task (got ${show(entry)})`);
396
314
  return;
397
315
  }
398
- checkKnownKeys(entry, CUSTOM_LOOP_KEYS, where, problems);
316
+ checkKnownKeys(entry, CUSTOM_ROLE_KEYS, where, problems);
399
317
  const name = entry.name;
400
318
  if (typeof name !== "string") {
401
319
  problems.push(`${where}.name must be a string matching /^[a-z0-9][a-z0-9_-]{0,31}$/ (got ${show(name)})`);
@@ -420,7 +338,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
420
338
  }
421
339
  const task = entry.task;
422
340
  // A whitespace-only task is as inert as an empty one — it rides into every one of
423
- // this loop's tick prefills as blank text, so the loop has nothing to do and ticks
341
+ // this role's run prefills as blank text, so the role has nothing to do and runs
424
342
  // no_change forever. Reject both shapes with the same message; the model-triple
425
343
  // checkString rule uses the same trim-based emptiness test.
426
344
  if (!isNonBlankString(task)) {
@@ -439,8 +357,8 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
439
357
  }
440
358
  else {
441
359
  for (const [id, rc] of Object.entries(roles)) {
442
- // An id outside the catalog and customRoles cannot work: tickPrompt has no prompt for
443
- // it and the loop would error every tick forever (checkKnownRoleId).
360
+ // An id outside the catalog and customRoles cannot work: runPrompt has no prompt for
361
+ // it and the role would error every run forever (checkKnownRoleId).
444
362
  if (!checkKnownRoleId("roles", id, customNames, problems))
445
363
  continue;
446
364
  if (!isJsonObject(rc)) {
@@ -450,16 +368,20 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
450
368
  const o = rc;
451
369
  checkKnownKeys(o, ROLE_ENTRY_KEYS, `roles.${id}`, problems);
452
370
  // Empty is a deliberate "no extra instructions". A non-empty value rides into every
453
- // one of this role's tick prefills, so it is capped like a custom loop's task.
371
+ // one of this role's run prefills, so it is capped like a custom role's task.
454
372
  checkString(o, `roles.${id}.`, "instructions");
455
373
  const instructions = o.instructions;
456
374
  if (typeof instructions === "string" && instructions.length > ROLE_INSTRUCTIONS_MAX_CHARS)
457
375
  problems.push(tooLongMessage(`roles.${id}.instructions`, instructions.length, ROLE_INSTRUCTIONS_MAX_CHARS, PREFILL_REASON));
458
376
  checkModelTriple(o, `roles.${id}.`, "tier-name");
459
377
  checkSelectorHalves(o, `roles.${id}.`, "model", o.provider ?? r.provider, problems);
378
+ checkEntryBackend(o, `roles.${id}`, problems);
379
+ if ((o.provider ?? r.provider) === undefined)
380
+ checkRetiredSelector(`roles.${id}.model`, o.model, typeof o.backend === "string" ? o.backend : topBackend, problems);
381
+ checkLegacyProvider(o.provider ?? r.provider, `roles.${id}.`, typeof o.backend === "string" ? o.backend : topBackend);
460
382
  checkBoolean(o, `roles.${id}.`, "enabled");
461
383
  checkNumber(o, `roles.${id}.`, "minRunIntervalSeconds", DURATION_NON_NEGATIVE);
462
- // Only feature and bugfix may split (src/roles/loop-ids.ts INSTANCE_ROLES); an
384
+ // Only feature and bugfix may split (src/roles/runner-ids.ts INSTANCE_ROLES); an
463
385
  // instances count under any other role would never spawn a runner, so reject it
464
386
  // rather than let the operator believe it did something.
465
387
  if ("instances" in o && !INSTANCE_ROLES.has(id)) {
@@ -471,10 +393,22 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
471
393
  }
472
394
  }
473
395
  }
396
+ // review.afterMerge names roles, so a typo'd id would silently keep that role reviewed before
397
+ // merging; the director is refused because its changes always merge on a review first.
398
+ if (isJsonObject(r.review) && Array.isArray(r.review.afterMerge)) {
399
+ r.review.afterMerge.forEach((id, i) => {
400
+ if (typeof id !== "string" || id.trim() === "")
401
+ return; // Already reported by checkStringArray.
402
+ if (id === DIRECTOR_ROLE)
403
+ problems.push(`review.afterMerge[${i}] must not be "${DIRECTOR_ROLE}" — its changes are always reviewed before merging`);
404
+ else
405
+ checkKnownRoleId("review.afterMerge", id, customNames, problems);
406
+ });
407
+ }
474
408
  // Per-role daily cost caps (src/gates/role-cap-gates.ts): each key must name a known role —
475
409
  // built-in or customRoles — because a typo'd id would silently no-op the cap, the exact
476
410
  // silent-ignore class the `roles.<id>` check exists to prevent (the customNames set above
477
- // is fully collected by this point, so a custom loop can be capped). Each value is a
411
+ // is fully collected by this point, so a custom role can be capped). Each value is a
478
412
  // spend threshold with the NON_NEGATIVE_OR_DISABLED semantics: 0 disables that role's cap.
479
413
  checkPerRoleMap(r, "maxDailyCostUsdPerRole", "USD caps", customNames, problems, (id) => {
480
414
  // Same MAX_SAFE_INTEGER bound as maxDailyCostUsd's DOLLAR_CAP rule (BUGS.md
@@ -536,6 +470,14 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
536
470
  }
537
471
  }
538
472
  }
473
+ // The reserve's two spellings are mutually exclusive (plans/codex-backend.md part 2b/9): the
474
+ // nested key is the current one; `claudeWindowReserve` is kept as claude's older spelling. A
475
+ // file holding both is ambiguous about which drives the window, so it is rejected naming both.
476
+ if (r.claudeWindowReserve !== undefined &&
477
+ isJsonObject(r.backends) &&
478
+ isJsonObject(r.backends.claude) &&
479
+ r.backends.claude.windowReserve !== undefined)
480
+ problems.push("claudeWindowReserve and backends.claude.windowReserve set the same value — keep one (backends.claude.windowReserve is the current spelling; claudeWindowReserve is its old name)");
539
481
  // Cross-field (judged on the merged config, where both sides are always present; on a raw
540
482
  // file only when the file itself names both): scheduleBackoff clamps every idle wait — the
541
483
  // first included — to min(initialSeconds, maxSeconds), so a smaller max silently discards