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,7 +1,6 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import { allRoleIds } from "../roles/roles.js";
4
- import { loopIds } from "../roles/loop-ids.js";
3
+ import { runnerIds } from "../roles/runner-ids.js";
5
4
  import { cachedByStat } from "../files/stat-cache.js";
6
5
  import { CONFIG_BASENAME, configPath } from "../paths.js";
7
6
  import { errorMessage } from "../text/text.js";
@@ -31,7 +30,7 @@ export function parseJsonConfig(file, basename) {
31
30
  return { raw, renamed };
32
31
  }
33
32
  /** Build the default TumwaterConfig: every role enabled except the opt-in selfcheck (steward on its
34
- * slow ~6 h tick, qa and selfcheck on ~2 h, readme on 30 min, plan and organize on 1 h), with
33
+ * slow ~6 h run, qa and selfcheck on ~2 h, readme on 30 min, plan and organize on 1 h), with
35
34
  * defaults for concurrency, timeouts, log size, retention, thrash detection, idle backoff,
36
35
  * self-redeploy, and review settings. The return type is the resolved view (config-views.ts): a
37
36
  * freshly defaulted config has no model map to resolve — only a loaded file can carry one — so
@@ -47,18 +46,22 @@ export function defaultConfig() {
47
46
  roles.steward = { enabled: true, minRunIntervalSeconds: 21600 };
48
47
  // QA exercises the product like a user on a ~2 h clock: user flows change slower than code.
49
48
  roles.qa = { enabled: true, minRunIntervalSeconds: 7200 };
50
- // Selfcheck reads the event log on the same slow clock: the digest is a windowed view, so a
51
- // tick a couple of hours apart reads a new window rather than re-filing the same cluster.
52
- // It is the one opt-in role: its evidence is this harness's own event log and its charter
53
- // files only harness misbehavior, so on any other project every finding is a tumwater bug
54
- // filed into the wrong backlog, which that project's bugfix loop rightly rejects. Tumwater's
55
- // own tumwater.example.json enables it, so this repo's checkouts still get it at `init`.
49
+ // Selfcheck and feedback are the two opt-in roles, both on the same ~2 h clock: the digest is
50
+ // a windowed view, so a run a couple of hours apart reads a new window rather than re-filing
51
+ // the same cluster. Selfcheck's evidence is this harness's own event log and its charter files
52
+ // only harness misbehavior, so on any other project every finding is a tumwater bug filed into
53
+ // the wrong backlog, which that project's bugfix role rightly rejects — that is why it is
54
+ // opt-in. Feedback files the same digest's findings as GitHub issues on znation/tumwater, so
55
+ // nothing leaves the machine until the operator opts in (plans/feedback-role.md). Tumwater's
56
+ // own tumwater.example.json keeps selfcheck (dogfood) and not feedback, so this repo's
57
+ // checkouts still get their findings at `init` and file nothing upstream.
56
58
  roles.selfcheck = { enabled: false, minRunIntervalSeconds: 7200 };
59
+ roles.feedback = { enabled: false, minRunIntervalSeconds: 7200 };
57
60
  // The bookkeeping roles run on slower clocks too. In dogfood readme (95 commits, 82 of them
58
61
  // status syncs) and plan (80, 44 of them refine/re-audit notes) were 30% of every commit and
59
- // 20% of every tick: readme woke on every merge to restamp one line, and plan re-audited plans
62
+ // 20% of every run: readme woke on every merge to restamp one line, and plan re-audited plans
60
63
  // nobody had touched. A 30 min readme clock batches a burst of merges into one sync; a 1 h
61
- // plan clock leaves the slots to the loops that ship. Both still wake early when main moves —
64
+ // plan clock leaves the slots to the roles that ship. Both still wake early when main moves —
62
65
  // the clock only bounds how often.
63
66
  roles.readme = { enabled: true, minRunIntervalSeconds: 1800 };
64
67
  roles.plan = { enabled: true, minRunIntervalSeconds: 3600 };
@@ -74,7 +77,7 @@ export function defaultConfig() {
74
77
  piArgs: [],
75
78
  // Agent backends (plans/agent-backends.md, part 4a/7): empty until a file names one.
76
79
  // Keeping the map always present means read sites need no `?? {}`; the legacy
77
- // agentBin/piArgs are read as pi's entry through resolvedAgentConfig (config-views.ts).
80
+ // agentBin/piArgs are read as pi's entry through resolvedBackendConfig (config-views.ts).
78
81
  backends: {},
79
82
  maxConcurrent: 6,
80
83
  // Three is the team's realistic concurrent-role count: a busy queue coalesces the
@@ -84,7 +87,7 @@ export function defaultConfig() {
84
87
  // without a burst stacking every check on the host beside the authors' own test runs.
85
88
  maxConcurrentChecks: 2,
86
89
  minRunIntervalSeconds: 20,
87
- // Four hours: the largest tick a hosted model legitimately took and still merged work
90
+ // Four hours: the largest run a hosted model legitimately took and still merged work
88
91
  // measured 185 minutes (BUGS.md 2026-09-29), so 30 minutes discarded real work and burned
89
92
  // every resume before the task could finish. A run that stops making progress is still
90
93
  // bounded earlier by the quiet watchdog, not by this cap.
@@ -103,12 +106,12 @@ export function defaultConfig() {
103
106
  maxDailyCostUsd: 50,
104
107
  // Work ratio 1a/4 and 1b/4 (plans/work-ratio.md): the maintenance allowance is
105
108
  // maintenancePerWorkMerge × work merges + 12 per rolling 24 h, and the scheduler holds
106
- // maintenance loops once the window reaches it. Two keeps roughly two upkeep merges per
109
+ // maintenance roles once the window reaches it. Two keeps roughly two upkeep merges per
107
110
  // shipped change while the fixed floor lets hygiene run on an empty backlog.
108
111
  maintenancePerWorkMerge: 2,
109
112
  // Disk floor (plans/disk-floor.md, part 1/4): when the volume the worktrees live on drops
110
113
  // below this many GB free, the team holds new work until it climbs 5 GB back above it.
111
- // 10 GB leaves room for an in-flight tick to finish and a state write to land. 0 disables.
114
+ // 10 GB leaves room for an in-flight run to finish and a state write to land. 0 disables.
112
115
  diskHoldGB: 10,
113
116
  // Pressure reclaim (plans/disk-floor.md, part 2/4): below this many GB free, a background
114
117
  // pass deletes gitignored build outputs from idle worktrees before the 10 GB hold engages.
@@ -119,9 +122,9 @@ export function defaultConfig() {
119
122
  // whatever the free space. A day catches paused, retired and rarely due roles without
120
123
  // sweeping a worktree that merely slept through the night. 0 disables idle reclaim.
121
124
  worktreeIdleReclaimHours: 24,
122
- // Friction is flagged only when a changed tick burns BOTH thresholds
123
- // (src/tick/tick-stage.ts): the absolute turn count alone measures model speed, so a fast
124
- // model's ordinary 40+ turn / few-minute tick stays unflagged, while a genuinely hard tick
125
+ // Friction is flagged only when a changed run burns BOTH thresholds
126
+ // (src/run/run-stage.ts): the absolute turn count alone measures model speed, so a fast
127
+ // model's ordinary 40+ turn / few-minute run stays unflagged, while a genuinely hard run
125
128
  // that burned 40+ turns over half an hour or more still is (BUGS.md 2026-09-19). Tuned for
126
129
  // the ~27B local model at 24-35 tok/s, where 40 turns is half an hour of work.
127
130
  thrashTurns: 40,
@@ -134,9 +137,16 @@ export function defaultConfig() {
134
137
  // writes a request file the harness applies to the live config before any commit, so no
135
138
  // diff can ever touch tumwater.json and the exemption — once the only thing keeping an
136
139
  // explicit user instruction from being discarded by a rejected review — has nothing left
137
- // to exempt. Removing it also closes the path where a director tick could merge a mixed
140
+ // to exempt. Removing it also closes the path where a director run could merge a mixed
138
141
  // doc-plus-config diff unreviewed.
139
- review: { enabled: true, exemptPaths: ["*.md", "docs/**"] },
142
+ // Reviewer as a regular role (part 1/4): the high-volume, low-blast-radius roles — bugfix,
143
+ // readme, organize, plus clean, dedupe, coverage — are reviewed AFTER they merge, on their
144
+ // merged commit (src/review/post-merge-review.ts), instead of holding the merge queue for a
145
+ // reviewer run on their (usually trivial) diff; a rejection reverts the commit back to the
146
+ // author as a revision. feature, director, perf, improve and custom roles stay pre-merge. A
147
+ // config that omits `review.afterMerge` inherits this, and `"afterMerge": []` opts out (back
148
+ // to reviewing before merging).
149
+ review: { enabled: true, exemptPaths: ["*.md", "docs/**"], afterMerge: ["bugfix", "clean", "coverage", "dedupe", "organize", "readme"] },
140
150
  customRoles: [],
141
151
  roles,
142
152
  };
@@ -161,7 +171,7 @@ export function overlayDefaults(base, cfg) {
161
171
  review: { ...base.review, ...(cfg.review ?? {}) },
162
172
  piArgs: cfg.piArgs ?? base.piArgs,
163
173
  // Clone each backend entry (and its args array) so the result owns its data like every
164
- // other section. The legacy agentBin/piArgs stay beside it; resolvedAgentConfig picks
174
+ // other section. The legacy agentBin/piArgs stay beside it; resolvedBackendConfig picks
165
175
  // between them.
166
176
  backends: Object.fromEntries(Object.entries(cfg.backends ?? base.backends ?? {}).map(([id, a]) => [
167
177
  id,
@@ -172,16 +182,16 @@ export function overlayDefaults(base, cfg) {
172
182
  customRoles: (cfg.customRoles ?? []).map((c) => ({ ...c })),
173
183
  roles: { ...base.roles },
174
184
  };
175
- // Seed each user-defined loop into roles BEFORE the overlay loop so a file's `roles` section
185
+ // Seed each user-defined role into roles BEFORE the overlay loop so a file's `roles` section
176
186
  // overrides per-key for a custom exactly like for built-ins (enabled, instructions,
177
- // tickInterval, provider/model); seeding after would silently ignore those overrides.
187
+ // runInterval, provider/model); seeding after would silently ignore those overrides.
178
188
  // A custom absent from `roles` stays enabled. Insertion order = built-ins then customs in
179
189
  // array order — the display and startup tie-break order.
180
190
  for (const c of merged.customRoles) {
181
191
  // Own-property test, not a bare truthiness read: the custom name `constructor` passes the
182
192
  // lowercase charset check and collides with an Object.prototype key, whose inherited value
183
- // is truthy — a bare `!merged.roles[c.name]` would skip the seed and the loop would never
184
- // become an own key, so loopIds silently dropped it.
193
+ // is truthy — a bare `!merged.roles[c.name]` would skip the seed and the role would never
194
+ // become an own key, so runnerIds silently dropped it.
185
195
  if (!Object.hasOwn(merged.roles, c.name))
186
196
  merged.roles[c.name] = { enabled: true };
187
197
  }
@@ -295,7 +305,7 @@ export function liveConfig(root) {
295
305
  * dropped from the serialized candidate. A merged config always carries the default `piArgs`,
296
306
  * so without this a whole-config write would put both spellings on disk and the next load's
297
307
  * raw pass would reject the file; a config that uses only the legacy spellings is unchanged. */
298
- export function collapseAgentAliases(config) {
308
+ export function collapseBackendAliases(config) {
299
309
  const pi = config.backends?.pi;
300
310
  if (pi === undefined)
301
311
  return config;
@@ -312,11 +322,11 @@ export function collapseAgentAliases(config) {
312
322
  return next;
313
323
  }
314
324
  /** Persist a config after validating it, so an invalid tumwater.json can never be written.
315
- * Written atomically (writeJsonAtomic, trailing newline — the POSIX-newline convention here
316
- * and at config-write.ts, the file's other writers): the config-live poller re-reads the file
325
+ * Written atomically (writeJsonAtomic, trailing newline — the POSIX-newline convention shared
326
+ * with config-write.ts and config-request.ts): the config-live poller re-reads the file
317
327
  * every ~2 s, so a plain overwrite could hand it a torn read mid-write. */
318
328
  export function saveConfig(root, config) {
319
- const collapsed = collapseAgentAliases(config);
329
+ const collapsed = collapseBackendAliases(config);
320
330
  validateConfig(collapsed);
321
331
  writeJsonAtomic(configPath(root), collapsed, true);
322
332
  }
@@ -359,36 +369,36 @@ export function changedConfigKeys(prev, next) {
359
369
  }
360
370
  return keys.sort();
361
371
  }
362
- /** The names of the user-defined loops, in array order (plans/user-defined-loops.md).
372
+ /** The names of the user-defined roles, in array order (plans/user-defined-loops.md).
363
373
  * One source of truth for "which ids are user-defined" so consumers cannot drift. */
364
- export function customLoopNames(config) {
374
+ export function customRoleNames(config) {
365
375
  return config.customRoles.map((c) => c.name);
366
376
  }
367
- /** True when `id` is a user-defined loop rather than a catalog role. */
377
+ /** True when `id` is a user-defined role rather than a catalog role. */
368
378
  export function isCustomRole(config, id) {
369
379
  return config.customRoles.some((c) => c.name === id);
370
380
  }
371
- /** Every valid role id — the catalog plus the user-defined loops (the one answer for "which
381
+ /** Every valid role id — the catalog plus the user-defined roles (the one answer for "which
372
382
  * ids exist", so consumers cannot drift from each other). */
373
383
  export function knownRoleIds(config) {
374
- const ids = [...allRoleIds(), ...customLoopNames(config)];
384
+ const ids = [...allRoleIds(), ...customRoleNames(config)];
375
385
  const seen = new Set(ids);
376
- // Parallel work instances (part 5b/7): an enabled role's instance loop ids (`feature-2`) are
386
+ // Parallel work instances (part 5b/7): an enabled role's instance role ids (`feature-2`) are
377
387
  // valid CLI and GUI targets even though they are not catalog roles. The bare ids are already
378
388
  // present, so this only appends the extras; deduping keeps the catalog+customs order intact.
379
- for (const id of loopIds(config))
389
+ for (const id of runnerIds(config))
380
390
  if (!seen.has(id)) {
381
391
  ids.push(id);
382
392
  seen.add(id);
383
393
  }
384
394
  return ids;
385
395
  }
386
- /** Every valid role id a READ-ONLY surface accepts: the catalog plus the user-defined loops,
396
+ /** Every valid role id a READ-ONLY surface accepts: the catalog plus the user-defined roles,
387
397
  * read through loadConfigCached (which never throws), so a transiently broken tumwater.json
388
398
  * falls back to the built-in catalog instead of taking the view down or refusing every id.
389
399
  * The one home of that fallback rule — every read-only role-id consumer resolves through it:
390
400
  * parseRoleScope (cmdLogs/cmdHistory), validRoleIds (the GUI endpoints), cmdDiff's role flag,
391
- * cmdTick's positional, cmdPrompt's --list and list-wide --cancel/--edit scope, and
401
+ * cmdRunDetail's positional, cmdPrompt's --list and list-wide --cancel/--edit scope, and
392
402
  * collectTeamChanges' roster — so they cannot drift. (Read-only deliberately: state-changing
393
403
  * commands resolve ids through loadConfig and fail loudly, because the operator is owed the
394
404
  * config error before a write.) */
@@ -1,10 +1,9 @@
1
- // vocabulary: plain
2
1
  /** Parsing model selector strings — `provider/id[:thinking]`, the form pi's own `--model`
3
2
  * accepts (plans/model-tiers.md "Selectors"). Tumwater parses them itself because pricing
4
3
  * (src/pi/pi-models.ts) needs the provider and id apart, while the argv builder
5
4
  * (src/pi/pi-args.ts) still passes `--provider` / `--model` / `--thinking` separately. Pure
6
5
  * string algebra — no I/O, no config knowledge beyond THINKING_LEVELS. */
7
- import { AGENT_IDS, THINKING_LEVELS } from "./config-schema.js";
6
+ import { BACKEND_IDS, THINKING_LEVELS } from "./config-schema.js";
8
7
  /** Parse one selector string. Rules:
9
8
  * 1. A trailing `:x` is a thinking level only when `x` is in THINKING_LEVELS, so
10
9
  * `…:together` (a provider pin) survives on the id while `…:together:low` strips to
@@ -36,27 +35,25 @@ export function parseModelSelector(s, legacyProvider) {
36
35
  })();
37
36
  return thinking ? { ...base, thinking } : base;
38
37
  }
39
- /** True when `s` names a registered agent backend — the reserved selector provider namespace
40
- * (`claude/<id>` always runs on claude). Validation restricts the config's `agent` key to the
41
- * same ids; a `provider/id` selector's provider is checked here. */
42
- export function isAgentId(s) {
43
- return s !== undefined && AGENT_IDS.includes(s);
38
+ /** True when `s` names a registered agent backend. Validation restricts the config's `backend`
39
+ * key to these ids; a model id never names one. */
40
+ export function isBackendId(s) {
41
+ return s !== undefined && BACKEND_IDS.includes(s);
44
42
  }
45
43
  /** Resolve a seam's backend identity: its own `agent` when it carries one (configForRole,
46
- * reviewConfig and resolverConfig all do), else `provider` when it names an agent backend (a
47
- * `claude/<id>` selector runs on claude), else pi — the one backend with providers, the
48
- * default a selector routes to when neither an agent nor a reserved provider prefix names one.
44
+ * reviewConfig, resolverConfig and a fallback pair's `backend` all do), else pi — the one
45
+ * backend with providers.
49
46
  * The single home of the rule: the budget gate's claude check, pi's free-ness and
50
47
  * context-window lookups, and the image-input seam all read a seam's backend through it, so
51
48
  * no two sites can disagree on which backend a selector runs. */
52
49
  export function seamAgent(seam) {
53
- return seam.agent ?? (isAgentId(seam.provider) ? seam.provider : "pi");
50
+ return seam.agent ?? "pi";
54
51
  }
55
52
  /** Each agent backend's per-tier default model, applied only when the config resolves no model
56
53
  * id for a seam (plans/agent-backends.md "Config" decision 5). pi has none — its own default
57
54
  * applies. It lives here, in the pure config layer, because `routeModel` runs at config-
58
55
  * resolution time and must not import the runtime backend registry. */
59
- const AGENT_DEFAULT_MODELS = {
56
+ const BACKEND_DEFAULT_MODELS = {
60
57
  pi: {},
61
58
  claude: {
62
59
  small: "claude-haiku-5-5",
@@ -67,30 +64,33 @@ const AGENT_DEFAULT_MODELS = {
67
64
  // applies, exactly as pi's does. The free model is a time-limited promotion, so its id
68
65
  // belongs in tumwater.json, not in code.
69
66
  opencode: {},
67
+ // codex's tier defaults (plans/codex-backend.md decision 4), from the catalog's own
68
+ // descriptions (design finding 14): strong is `gpt-6-astra`, default is `gpt-6.1-sol` (Codex's
69
+ // own default), small is `gpt-6-luna`. The director runs the strong tier, as on claude.
70
+ codex: {
71
+ small: "gpt-6-luna",
72
+ default: "gpt-6.1-sol",
73
+ strong: "gpt-6-astra",
74
+ },
70
75
  };
71
76
  /** The backend and concrete model a resolved seam's provider/model pair routes to
72
- * (plans/agent-backends.md "Config"). The provider segment decides the backend: a provider
73
- * naming a registered agent id runs that agent (and its provider field is cleared, since it is
74
- * a namespace, not a pi provider); any other provider runs on pi, the one backend with
75
- * providers; no provider at all runs on the config's `agent`. When no model id survives the
76
- * resolution, the backend's tier default fills in (pi supplies none).
77
- *
78
- * Rule 3a (plans/provider-concurrency.md "Routing a bare Claude model id to claude"): a bare id
79
- * (no provider) that starts with `claude-` runs on claude, whatever `configAgent` says — the one
80
- * prefix where the model id decides the backend. A named provider still wins
81
- * (`anthropic/claude-…` stays pi); bare aliases (`sonnet`, `opus`) keep today's routing. */
82
- export function routeModel(provider, model, configAgent, tier) {
83
- const agent = provider === undefined
84
- ? model?.startsWith("claude-")
85
- ? "claude"
86
- : configAgent
87
- : isAgentId(provider)
88
- ? provider
89
- : "pi";
90
- if (model !== undefined)
91
- return { agent, model };
92
- const def = AGENT_DEFAULT_MODELS[agent]?.[tier];
93
- return def !== undefined ? { agent, model: def } : { agent };
77
+ * (plans/agent-backends.md "Config"). The backend is the `explicitBackend` (a tier entry's,
78
+ * role's or review's `backend` key), else the config's `backend`, else pi — a model id never
79
+ * names one. pi keeps the parsed provider (the caller's); claude and opencode take the whole
80
+ * `provider/model` id with no provider, verbatim; codex takes the bare model as written (there
81
+ * is no `codex/<id>` spelling — a model id never names a backend, and a whole `codex/<id>`
82
+ * parses to provider `codex` which codex, like pi, drops, leaving the model half). When no
83
+ * model id survives the resolution, the
84
+ * backend's tier default fills in (pi and opencode supply none). */
85
+ export function routeModel(provider, model, configBackend, tier, explicitBackend) {
86
+ const backend = explicitBackend ?? configBackend;
87
+ const whole = model !== undefined && provider !== undefined && backend !== "pi" && backend !== "codex"
88
+ ? `${provider}/${model}`
89
+ : model;
90
+ if (whole !== undefined)
91
+ return { agent: backend, model: whole };
92
+ const def = BACKEND_DEFAULT_MODELS[backend]?.[tier];
93
+ return def !== undefined ? { agent: backend, model: def } : { agent: backend };
94
94
  }
95
95
  /** The inverse of parseModelSelector (in its no-legacyProvider form): the selector string a
96
96
  * triple round-trips through, for messages and future writers. */
@@ -0,0 +1,287 @@
1
+ import { andMore, readDocChecked, FIX_CLAIMS_CHECKED, FIX_CLAIM_HEADING_MAX, REFUSAL_NOTE_MAX, STALE_QUESTION_DAYS, STALE_BUG_DAYS, STALE_WAIT_DAYS } from "./doctor-backlog.js";
2
+ import path from "node:path";
3
+ import { truncate } from "../text/text.js";
4
+ import { ageInDays, filingDate, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
5
+ import { stripEntryStamp } from "../backlog/backlog-structure.js";
6
+ import { questionFilingDate } from "../backlog/question-answer.js";
7
+ import { refusalNote, waitingNote } from "../backlog/backlog-eligibility.js";
8
+ import { plannedPartKeys, plansWaitingOn, prerequisitesMerged } from "../backlog/backlog-parts.js";
9
+ import { fixedEntriesAt } from "../verdict/fix-claim.js";
10
+ import { lastDate } from "../backlog/backlog-md.js";
11
+ import { formatTimestamp } from "../text/datetime.js";
12
+ import { stringList } from "../files/json-object.js";
13
+ import { readEventsSince } from "../events/event-window.js";
14
+ import { okOutcome, warnOutcome } from "./outcome.js";
15
+ /** The doctor's open-bug and question checks, split out of doctor-backlog.ts: questions that
16
+ * have waited too long for an answer (checkStaleQuestions), bugs left unfixed (checkStaleBugs),
17
+ * bugs held for their next failure (checkWaitingBugs, with its quotedTestName / waitingNoteDate /
18
+ * failureMentions helpers), fixed records whose quoted test regressed (checkRegressedFixes), and
19
+ * entries parked by a Refused note (checkRefusedEntries). They import the shared helpers
20
+ * (readDocChecked, andMore) from doctor-backlog.ts, plus the age/trim bounds, and
21
+ * all warn instead of failing: a stale or
22
+ * parked entry is operator signal, not a broken environment. doctor.ts composes them beside the
23
+ * plan-structure and queue checks; the shared CheckOutcome shape is imported from outcome.ts. */
24
+ /** Stale questions — open QUESTIONS.md questions that have waited more than 7 days for an
25
+ * answer. A team can stay parked on a question for days while `tumwater doctor` reports
26
+ * nothing; the `--list` age (`questionAgeSuffix` on `tumwater questions`) is the only other
27
+ * place the wait shows, and only when the operator asks. Reads the tree at `root` — the
28
+ * primary checkout IS main's tree — like checkRefusedEntries, reusing
29
+ * `questionFilingDate`/`openQuestionEntries` (src/backlog) unchanged. The age is the whole
30
+ * days between the heading's `(asked by …)` stamp and now through backlog.js's ageInDays,
31
+ * the one age formula, floored at 0; an undated heading is skipped. A warn, never a fail —
32
+ * the checkStalledPlans/checkRefusedEntries precedent: a question awaiting a human is
33
+ * operator signal, not a broken environment. */
34
+ export function checkStaleQuestions(root) {
35
+ const doc = readDocChecked(path.join(root, "QUESTIONS.md"), "QUESTIONS.md", "no QUESTIONS.md — nothing to verify");
36
+ if (typeof doc !== "string")
37
+ return doc;
38
+ const stale = openQuestionEntries(root).flatMap((q) => {
39
+ const stamp = questionFilingDate(q.title);
40
+ if (stamp === null)
41
+ return [];
42
+ const days = ageInDays(stamp);
43
+ return days >= STALE_QUESTION_DAYS ? [{ title: q.title, days }] : [];
44
+ });
45
+ const [first, ...rest] = stale;
46
+ if (!first)
47
+ return okOutcome("no open question has waited more than 7 days for an answer");
48
+ const more = andMore(rest, (q) => `"${truncate(q.title, FIX_CLAIM_HEADING_MAX)}"`);
49
+ return warnOutcome(`QUESTIONS.md question "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" has waited ${first.days} d for an answer${more} — ` +
50
+ `a question that waits too long for a human answer is worth your attention`);
51
+ }
52
+ /** Stale bugs — unrefused open BUGS.md bugs whose `(found by … YYYY-MM-DD)` or `(reported …)`
53
+ * stamp is at least 3 days old. The bugfix role fixes a bug within hours, so one that sits means
54
+ * no role is fixing it. Positions are the 1-based `bug --list` numbers; a refused bug is skipped
55
+ * (checkRefusedEntries names it), a waiting bug is skipped (checkWaitingBugs names it) and so is
56
+ * an undated heading. A warn, never a fail. */
57
+ export function checkStaleBugs(root) {
58
+ const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
59
+ if (typeof doc !== "string")
60
+ return doc;
61
+ const stale = openBugEntries(root).flatMap((entry, index) => {
62
+ if (refusalNote(entry.body) !== null || waitingNote(entry.body) !== null)
63
+ return [];
64
+ const stamp = filingDate(entry.title);
65
+ if (stamp === null)
66
+ return [];
67
+ const days = ageInDays(stamp);
68
+ return days >= STALE_BUG_DAYS ? [{ position: index + 1, title: entry.title, days }] : [];
69
+ });
70
+ const [first, ...rest] = stale;
71
+ if (!first)
72
+ return okOutcome("no open bug has waited more than 3 days for a fix");
73
+ const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
74
+ return warnOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" has waited ${first.days} d for a fix${more} — ` +
75
+ `no role is fixing it: check that the bugfix role is enabled and not failing, or retract it with \`tumwater bug --cancel ${first.position}\``);
76
+ }
77
+ /** The first span between straight double quotes in a bug heading that is at least 12
78
+ * characters long — the test name a flake bug's heading quotes — or null. The floor stops a
79
+ * short quoted word from matching unrelated failures. */
80
+ export function quotedTestName(title) {
81
+ return /"([^"]{12,})"/.exec(title)?.[1] ?? null;
82
+ }
83
+ /** Local midnight at the end of a `YYYY-MM-DD` day, in ms: the earliest instant a failure can
84
+ * be told apart from the one a same-day note or fix record was written about. NaN for a
85
+ * malformed date. */
86
+ function dayAfterMs(date) {
87
+ return new Date(Number(date.slice(0, 4)), Number(date.slice(5, 7)) - 1, Number(date.slice(8, 10)) + 1).getTime();
88
+ }
89
+ /** The first `YYYY-MM-DD` in a Waiting for the next failure note's text, or null. */
90
+ export function waitingNoteDate(note) {
91
+ return /\d{4}-\d{2}-\d{2}/.exec(note)?.[0] ?? null;
92
+ }
93
+ /** Every event at or after `sinceMs` that records a failure naming `name`: a `warning` whose
94
+ * message includes it (gate and merge flake warnings, a merge check kept for one retry), or a
95
+ * `review_rejected` with a reason that includes it (its text is the first such reason). In
96
+ * event order. */
97
+ export function failureMentions(events, name, sinceMs) {
98
+ return events.flatMap((e) => {
99
+ if (typeof e.ts !== "number" || e.ts < sinceMs)
100
+ return [];
101
+ if (e.type === "warning" && typeof e.message === "string" && e.message.includes(name))
102
+ return [{ ts: e.ts, text: e.message }];
103
+ if (e.type === "review_rejected") {
104
+ const reason = stringList(e.reasons).find((r) => r.includes(name));
105
+ if (reason !== undefined)
106
+ return [{ ts: e.ts, text: reason }];
107
+ }
108
+ return [];
109
+ });
110
+ }
111
+ /** The read-events window (ms) that reaches the oldest of `sinces` — `Date.now()` minus the
112
+ * oldest timestamp, floored at 0 — so a recurrence check reads events past every candidate's
113
+ * `since`. The single home of the "oldest of a set of since-times" step, shared by
114
+ * checkWaitingBugs and checkRegressedFixes, so those sites cannot drift on the clamp or the
115
+ * time base. */
116
+ function sinceWindowMs(sinces) {
117
+ return Math.max(0, Date.now() - Math.min(...sinces));
118
+ }
119
+ /** Open BUGS.md bugs held by a **Waiting for the next failure …** note, in file order: the
120
+ * doctor's `checkWaitingBugs` selection and anchor, factored out so the always-on stale-wait
121
+ * feed (staleWaitingBugRows) reads the same held set and cannot drift from the doctor's rule.
122
+ * A bug that is also Refused is left to checkRefusedEntries. `since` is the day-after the
123
+ * note's date — the recurrence scan's boundary; `date` the note's own `YYYY-MM-DD` or null. */
124
+ function heldWaitingBugs(root) {
125
+ return openBugEntries(root).flatMap((entry, index) => {
126
+ const note = waitingNote(entry.body);
127
+ if (note === null || refusalNote(entry.body) !== null)
128
+ return [];
129
+ const name = quotedTestName(entry.title);
130
+ const date = waitingNoteDate(note);
131
+ const since = name !== null && date !== null ? dayAfterMs(date) : null;
132
+ return [{ index, position: index + 1, title: entry.title, note, name, since, date }];
133
+ });
134
+ }
135
+ /** Waiting bugs — open BUGS.md bugs held by a **Waiting for the next failure …** note, which
136
+ * parks a bug until it fails again. Positions are the 1-based `bug --list` numbers; a bug that
137
+ * is also Refused is left to checkRefusedEntries. Ok when the hold is undisturbed — doctor
138
+ * then only names the bugs and how to release one — and a warn when a held bug's quoted test
139
+ * has failed in a gate or merge check since the day after its note (the note carries only a
140
+ * date, so a same-day failure cannot be told from the one it was written about). A warn is
141
+ * never a fail, and doctor writes nothing to BUGS.md. */
142
+ export function checkWaitingBugs(root) {
143
+ const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
144
+ if (typeof doc !== "string")
145
+ return doc;
146
+ const held = heldWaitingBugs(root);
147
+ const [first, ...rest] = held;
148
+ if (!first)
149
+ return okOutcome("no open bug is waiting for its next failure");
150
+ const sinces = held.flatMap((b) => (b.since !== null && !Number.isNaN(b.since) ? [b.since] : []));
151
+ if (sinces.length > 0) {
152
+ const { events } = readEventsSince(root, sinceWindowMs(sinces));
153
+ const recurred = held.flatMap((b) => {
154
+ if (b.name === null || b.since === null)
155
+ return [];
156
+ const mentions = failureMentions(events, b.name, b.since);
157
+ const newest = mentions[mentions.length - 1];
158
+ return newest ? [{ ...b, newest }] : [];
159
+ });
160
+ const [again, ...others] = recurred;
161
+ if (again) {
162
+ const more = andMore(others, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
163
+ return warnOutcome(`BUGS.md bug #${again.position} "${truncate(again.title, FIX_CLAIM_HEADING_MAX)}" failed again since its Waiting for the next failure note ` +
164
+ `(${formatTimestamp(again.newest.ts)}: ${truncate(again.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
165
+ `record the occurrence under the entry and release it with \`tumwater bug --release ${again.position}\``);
166
+ }
167
+ }
168
+ /* A Waiting hold has no expiry of its own, so a bug whose flake never comes back parks
169
+ * indefinitely; a hold that has outlived STALE_WAIT_DAYS without a recurrence is escalated
170
+ * from the ok below to a warn so the operator is nudged to release or re-hold it. */
171
+ const lapsed = held.flatMap((b) => {
172
+ /* An undated note (no YYYY-MM-DD in its text) has no hold-start marker, and checkStaleBugs
173
+ * skips any bug carrying a waiting note, so without a fallback it would park indefinitely
174
+ * too; the filing stamp anchors it — the hold cannot be younger than the bug it dates from. */
175
+ const anchor = b.date ?? filingDate(b.title);
176
+ if (anchor === null)
177
+ return [];
178
+ const days = ageInDays(anchor);
179
+ return days >= STALE_WAIT_DAYS ? [{ ...b, days }] : [];
180
+ });
181
+ const [firstLapsed, ...restLapsed] = lapsed;
182
+ if (firstLapsed) {
183
+ const more = andMore(restLapsed, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
184
+ return warnOutcome(`BUGS.md bug #${firstLapsed.position} "${truncate(firstLapsed.title, FIX_CLAIM_HEADING_MAX)}" has waited ${firstLapsed.days} d for its next failure without one ` +
185
+ `— its flake likely never comes back, so it parks indefinitely; release it with \`tumwater bug --release ${firstLapsed.position}\`${more}`);
186
+ }
187
+ const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
188
+ return okOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" is held until it fails again ` +
189
+ `(${truncate(first.note, REFUSAL_NOTE_MAX)})${more} — selfcheck lifts the note when it records a new occurrence; ` +
190
+ `release it now with \`tumwater bug --release ${first.position}\``);
191
+ }
192
+ /** The always-on rows behind the dashboard/TUI/`tumwater status` waiting-bug alert: open BUGS.md
193
+ * bugs held by a **Waiting for the next failure …** note whose anchor (the note's `YYYY-MM-DD`,
194
+ * else the heading's filing stamp) is at least STALE_WAIT_DAYS old. Shares the doctor's
195
+ * STALE_WAIT_DAYS threshold and the heldWaitingBugs selection/anchor, so the alert and the
196
+ * doctor's `waiting bugs` row cannot drift on the rule; an undated note-and-heading is skipped.
197
+ * The returned objects carry the 0-based index (drawer address `bugs:<index>`), the 1-based
198
+ * position (`bug --release <position>`), the stamp-stripped title, and the whole-day age.
199
+ * Returned most-days-waited-first, ties in file order, like staleOpenBugs. This deliberately
200
+ * omits `checkWaitingBugs`'s recurrence scan, because that scan reads events back to the oldest
201
+ * hold and is too costly on the status poll path; the doctor still reports a recurrence first. */
202
+ export function staleWaitingBugRows(root, now = Date.now()) {
203
+ return heldWaitingBugs(root)
204
+ .flatMap((b) => {
205
+ const anchor = b.date ?? filingDate(b.title);
206
+ if (anchor === null)
207
+ return [];
208
+ const days = ageInDays(anchor, now);
209
+ return days >= STALE_WAIT_DAYS ? [{ index: b.index, position: b.position, title: stripEntryStamp(b.title), days }] : [];
210
+ })
211
+ .sort((a, b) => b.days - a.days || a.index - b.index);
212
+ }
213
+ /** Regressed fixes — a newest-10 BUGS.md Fixed record whose heading quotes a test name that a
214
+ * gate or merge check has failed on since the day after its fix (the heading carries only a
215
+ * date, so a same-day failure cannot be told from the one the fix answered), unless an Open bug
216
+ * already quotes the same test, which means the regression is filed. A warn, never a fail, and
217
+ * doctor writes nothing to BUGS.md. */
218
+ export function checkRegressedFixes(root) {
219
+ const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
220
+ if (typeof doc !== "string")
221
+ return doc;
222
+ const candidates = fixedEntriesAt(root, doc).slice(0, FIX_CLAIMS_CHECKED).flatMap((entry) => {
223
+ const name = quotedTestName(entry.title);
224
+ const date = lastDate(entry.title, /(\d{4}-\d{2}-\d{2})/);
225
+ if (name === null || date === null)
226
+ return [];
227
+ const since = dayAfterMs(date);
228
+ return Number.isNaN(since) ? [] : [{ title: entry.title, name, date, since }];
229
+ });
230
+ const open = candidates.length > 0 ? openBugEntries(root).map((e) => quotedTestName(e.title)) : [];
231
+ const unfiled = candidates.filter((c) => !open.includes(c.name));
232
+ if (unfiled.length > 0) {
233
+ const { events } = readEventsSince(root, sinceWindowMs(unfiled.map((c) => c.since)));
234
+ const regressed = unfiled.flatMap((c) => {
235
+ const mentions = failureMentions(events, c.name, c.since);
236
+ const newest = mentions[mentions.length - 1];
237
+ return newest ? [{ ...c, newest }] : [];
238
+ });
239
+ const [first, ...rest] = regressed;
240
+ if (first) {
241
+ const more = andMore(rest, (c) => `"${truncate(c.title, FIX_CLAIM_HEADING_MAX)}"`);
242
+ return warnOutcome(`BUGS.md Fixed record "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" quotes a test that failed again after its ${first.date} fix ` +
243
+ `(${formatTimestamp(first.newest.ts)}: ${truncate(first.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
244
+ "file it as a new bug with `tumwater bug`, quoting the test name and citing the Fixed record");
245
+ }
246
+ }
247
+ return okOutcome("no recently Fixed bug's quoted test has failed since its fix");
248
+ }
249
+ /** Refused entries — planned plans and open bugs held by a **Refused …** note. No role clears
250
+ * a Refused note, so the entry stays parked until a human or the director edits it, or an
251
+ * operator releases it with `--release <n>`, and the
252
+ * only other signs are the `[refused]` marks in the backlog listings. Reads the tree at `root`
253
+ * — the primary checkout IS main's tree — like checkFixClaims, in `--list` order (PLANS.md
254
+ * Planned, then BUGS.md Open). A warn, never a fail: a refusal is operator signal, not a broken
255
+ * environment. The warning also counts the planned parts a refused series part holds. */
256
+ export function checkRefusedEntries(root) {
257
+ const planned = plannedPlanEntries(root);
258
+ const parts = plannedPartKeys(planned);
259
+ const sources = [
260
+ { file: "PLANS.md", command: "plan", entries: planned },
261
+ { file: "BUGS.md", command: "bug", entries: openBugEntries(root) },
262
+ ];
263
+ const held = sources.flatMap(({ file, command, entries }) => entries.flatMap((entry, index) => {
264
+ const note = refusalNote(entry.body);
265
+ if (note === null)
266
+ return [];
267
+ const mergeDone = prerequisitesMerged(entry.title, parts);
268
+ const waiting = file === "PLANS.md" ? plansWaitingOn(planned, index).length : 0;
269
+ return [{ file, command, position: index + 1, title: entry.title, note, mergeDone, waiting }];
270
+ }));
271
+ const [first, ...rest] = held;
272
+ if (!first)
273
+ return okOutcome("no planned plan or open bug is held by a Refused note");
274
+ const more = andMore(rest, (e) => `${e.file} ${e.command} #${e.position} "${truncate(e.title, FIX_CLAIM_HEADING_MAX)}"` +
275
+ (e.mergeDone.length > 0 ? " [prerequisites no longer planned]" : "") +
276
+ (e.waiting > 0 ? ` [${e.waiting} part(s) wait on it]` : ""));
277
+ const stale = first.mergeDone.length > 0
278
+ ? `; its prerequisite(s) ${first.mergeDone.map((r) => `${r.series} ${r.part}/${r.of}`).join(", ")} are no longer planned, so the refusal may be stale`
279
+ : "";
280
+ const waiting = first.waiting > 0
281
+ ? `; ${first.waiting} planned part(s) wait on it, directly or through an earlier part`
282
+ : "";
283
+ return warnOutcome(`${first.file} ${first.command} #${first.position} "${first.title}" is held by a Refused note ` +
284
+ `(${truncate(first.note, REFUSAL_NOTE_MAX)})${stale}${waiting}${more} — no role takes a refused entry until it is edited: ` +
285
+ `answer the objection and release the entry with \`tumwater ${first.command} --release ${first.position}\`, ` +
286
+ `or retract it with \`tumwater ${first.command} --cancel ${first.position}\``);
287
+ }