tumwater 0.1.3 → 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 (482) hide show
  1. package/README.md +77 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +23 -0
  4. package/dist/src/agent/agent-backend.js +9 -7
  5. package/dist/src/agent/backend-traits.js +77 -0
  6. package/dist/src/agent/claude/claude-account.js +81 -161
  7. package/dist/src/agent/claude/claude-args.js +19 -19
  8. package/dist/src/agent/claude/claude-backend.js +58 -48
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +11 -19
  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 +112 -0
  25. package/dist/src/agent/notes-shim.js +40 -0
  26. package/dist/src/agent/opencode/opencode-args.js +60 -0
  27. package/dist/src/agent/opencode/opencode-backend.js +258 -0
  28. package/dist/src/agent/opencode/opencode-catalog.js +210 -0
  29. package/dist/src/agent/opencode/opencode-config.js +193 -0
  30. package/dist/src/agent/opencode/opencode-env.js +35 -0
  31. package/dist/src/agent/opencode/opencode-session-id.js +36 -0
  32. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +16 -33
  35. package/dist/src/agent/run-agent.js +475 -381
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +142 -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 +487 -0
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +384 -0
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +354 -132
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +132 -22
  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 +89 -11
  50. package/dist/src/backlog/backlog-structure.js +271 -125
  51. package/dist/src/backlog/backlog-write.js +121 -471
  52. package/dist/src/backlog/backlog.js +129 -17
  53. package/dist/src/backlog/question-answer.js +345 -0
  54. package/dist/src/baseline/main-baseline.js +157 -101
  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 +164 -55
  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 +41 -0
  61. package/dist/src/{brief.js → brief/brief.js} +38 -33
  62. package/dist/src/budget/budget.js +55 -28
  63. package/dist/src/budget/fallback-breaker.js +31 -31
  64. package/dist/src/build/build-check-detect.js +25 -16
  65. package/dist/src/build/build-check-events.js +87 -22
  66. package/dist/src/build/build-check-report.js +66 -27
  67. package/dist/src/build/build-check-scoped.js +64 -68
  68. package/dist/src/build/build-check.js +17 -15
  69. package/dist/src/build/build-info.js +3 -3
  70. package/dist/src/build/build-stage.js +31 -35
  71. package/dist/src/build/dep-install.js +21 -21
  72. package/dist/src/build/host-sleep.js +1 -1
  73. package/dist/src/change/change-data.js +131 -37
  74. package/dist/src/change/change-render.js +56 -14
  75. package/dist/src/cli/brief-command.js +9 -8
  76. package/dist/src/cli/cli-args.js +133 -28
  77. package/dist/src/cli/cli-command-args.js +82 -21
  78. package/dist/src/cli/cli-editor.js +15 -2
  79. package/dist/src/cli/cli-flag-specs.js +180 -50
  80. package/dist/src/cli/cli-init.js +51 -0
  81. package/dist/src/cli/cli-marker-commands.js +14 -11
  82. package/dist/src/cli/cli-query-commands.js +325 -224
  83. package/dist/src/cli/cli-repo-gate.js +12 -2
  84. package/dist/src/cli/cli-role-command.js +420 -0
  85. package/dist/src/cli/cli-start-summary.js +160 -0
  86. package/dist/src/cli/cli-start.js +453 -0
  87. package/dist/src/cli/completion.js +202 -17
  88. package/dist/src/cli/config-commands.js +51 -25
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +413 -157
  91. package/dist/src/cli/log-commands.js +234 -62
  92. package/dist/src/cli/notify-command.js +33 -18
  93. package/dist/src/cli/question-commands.js +215 -231
  94. package/dist/src/cli.js +154 -49
  95. package/dist/src/collections.js +84 -18
  96. package/dist/src/concurrency/check-permit.js +35 -19
  97. package/dist/src/concurrency/lock.js +22 -17
  98. package/dist/src/concurrency/provider-lease.js +105 -0
  99. package/dist/src/concurrency/semaphore.js +344 -46
  100. package/dist/src/config/config-aliases.js +105 -0
  101. package/dist/src/config/config-editable-keys.js +16 -4
  102. package/dist/src/config/config-fallbacks.js +416 -0
  103. package/dist/src/config/config-field-checks.js +58 -19
  104. package/dist/src/config/config-keys.js +182 -0
  105. package/dist/src/config/config-live.js +113 -22
  106. package/dist/src/config/config-request.js +137 -0
  107. package/dist/src/config/config-schema.js +58 -39
  108. package/dist/src/config/config-selector-checks.js +123 -0
  109. package/dist/src/config/config-validation-agents.js +80 -0
  110. package/dist/src/config/config-validation-model.js +116 -0
  111. package/dist/src/config/config-validation.js +201 -175
  112. package/dist/src/config/config-views.js +202 -346
  113. package/dist/src/config/config-write-core.js +135 -0
  114. package/dist/src/config/config-write.js +219 -391
  115. package/dist/src/config/config.js +102 -81
  116. package/dist/src/config/model-selector.js +44 -18
  117. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  118. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  119. package/dist/src/doctor/doctor-backlog.js +191 -161
  120. package/dist/src/doctor/doctor-check-names.js +81 -0
  121. package/dist/src/doctor/doctor-checks.js +229 -305
  122. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  123. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  124. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  125. package/dist/src/doctor/doctor-holds.js +181 -104
  126. package/dist/src/doctor/doctor-install.js +31 -0
  127. package/dist/src/doctor/doctor-launch-services.js +2 -2
  128. package/dist/src/doctor/doctor-model-checks.js +496 -270
  129. package/dist/src/doctor/doctor-notify.js +5 -7
  130. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  131. package/dist/src/doctor/doctor-orphans.js +189 -69
  132. package/dist/src/doctor/doctor-project-checks.js +303 -0
  133. package/dist/src/doctor/doctor-render.js +36 -9
  134. package/dist/src/doctor/doctor.js +145 -50
  135. package/dist/src/doctor/image-probe.js +257 -0
  136. package/dist/src/doctor/outcome.js +3 -2
  137. package/dist/src/doctor/probe.js +60 -0
  138. package/dist/src/events/event-format.js +363 -155
  139. package/dist/src/events/event-labels.js +123 -0
  140. package/dist/src/events/event-legacy.js +164 -0
  141. package/dist/src/events/event-read.js +74 -38
  142. package/dist/src/events/event-tally.js +16 -0
  143. package/dist/src/events/event-window.js +83 -27
  144. package/dist/src/events/events.js +47 -26
  145. package/dist/src/events/notify.js +85 -35
  146. package/dist/src/failure/failure-cluster.js +23 -20
  147. package/dist/src/failure/failure-data.js +140 -92
  148. package/dist/src/failure/failure-render.js +97 -78
  149. package/dist/src/failure/failure-state-change.js +45 -33
  150. package/dist/src/failure/time-spend.js +166 -82
  151. package/dist/src/files/file-queue.js +16 -4
  152. package/dist/src/files/files.js +129 -41
  153. package/dist/src/files/json-files.js +3 -3
  154. package/dist/src/files/json-object.js +45 -9
  155. package/dist/src/files/stat-cache.js +2 -2
  156. package/dist/src/files/tail.js +2 -2
  157. package/dist/src/gates/bootstrap-gates.js +8 -8
  158. package/dist/src/gates/budget-gates.js +361 -52
  159. package/dist/src/gates/disk-gate.js +18 -18
  160. package/dist/src/gates/gate-polls.js +201 -103
  161. package/dist/src/gates/maintenance-quota.js +34 -37
  162. package/dist/src/gates/pause-gates.js +33 -15
  163. package/dist/src/gates/readiness.js +8 -3
  164. package/dist/src/gates/role-cap-gates.js +23 -28
  165. package/dist/src/gates/startup-gate.js +154 -48
  166. package/dist/src/gates/streak-gate.js +67 -41
  167. package/dist/src/git/commit-message.js +80 -48
  168. package/dist/src/git/git-diff.js +29 -23
  169. package/dist/src/git/git-run.js +87 -46
  170. package/dist/src/git/git.js +114 -92
  171. package/dist/src/git/slot-salvage.js +253 -0
  172. package/dist/src/git/slots-state.js +18 -9
  173. package/dist/src/git/worktree-pool.js +150 -49
  174. package/dist/src/git/worktree-use.js +14 -20
  175. package/dist/src/git/worktree.js +108 -51
  176. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  177. package/dist/src/gui/gui-args.js +88 -21
  178. package/dist/src/gui/gui-backlog-commands.js +297 -0
  179. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  180. package/dist/src/gui/gui-command.js +9 -14
  181. package/dist/src/gui/gui-config-commands.js +462 -0
  182. package/dist/src/gui/gui-endpoint-commands.js +17 -716
  183. package/dist/src/gui/gui-endpoint-common.js +49 -0
  184. package/dist/src/gui/gui-endpoints.js +13 -331
  185. package/dist/src/gui/gui-history-endpoints.js +301 -0
  186. package/dist/src/gui/gui-operator-commands.js +240 -0
  187. package/dist/src/gui/gui-prompt-commands.js +231 -0
  188. package/dist/src/gui/gui-report-endpoints.js +90 -0
  189. package/dist/src/gui/gui-server.js +33 -9
  190. package/dist/src/gui/gui-team-commands.js +164 -0
  191. package/dist/src/gui/http-body.js +17 -1
  192. package/dist/src/history/history-data.js +166 -203
  193. package/dist/src/history/history-queued.js +147 -0
  194. package/dist/src/history/history-render.js +96 -0
  195. package/dist/src/history/history.js +206 -103
  196. package/dist/src/inbox/inbox-attachments.js +94 -39
  197. package/dist/src/inbox/inbox-cancel.js +16 -15
  198. package/dist/src/inbox/inbox-edit.js +10 -10
  199. package/dist/src/inbox/inbox-submit.js +54 -14
  200. package/dist/src/inbox/inbox-top.js +99 -0
  201. package/dist/src/inbox/inbox.js +49 -44
  202. package/dist/src/inbox/pending-prompt.js +27 -27
  203. package/dist/src/inbox/prompt-commands.js +154 -100
  204. package/dist/src/inbox/prompt-not-before.js +2 -2
  205. package/dist/src/init/init-templates.js +15 -14
  206. package/dist/src/init/init.js +35 -29
  207. package/dist/src/merge/backlog-conflicts.js +335 -0
  208. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +100 -93
  209. package/dist/src/merge/merge-cancel.js +53 -0
  210. package/dist/src/merge/merge-check-failures.js +209 -0
  211. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +183 -110
  212. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +6 -5
  213. package/dist/src/merge/merge-drain.js +351 -0
  214. package/dist/src/merge/merge-git.js +403 -0
  215. package/dist/src/merge/merge-pipeline.js +111 -0
  216. package/dist/src/merge/merge-post-review.js +90 -0
  217. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  218. package/dist/src/merge/merge-queue-render.js +113 -0
  219. package/dist/src/merge/merge-queue.js +224 -0
  220. package/dist/src/merge/merge-requeue.js +102 -0
  221. package/dist/src/merge/merge-slot.js +385 -0
  222. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +97 -71
  223. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +151 -82
  224. package/dist/src/merge/merge-top.js +69 -0
  225. package/dist/src/merge/merge-verify.js +214 -0
  226. package/dist/src/notes-cli.js +112 -0
  227. package/dist/src/operator/operator-commands.js +268 -89
  228. package/dist/src/operator/operator-intent.js +130 -82
  229. package/dist/src/operator/operator-requests.js +82 -87
  230. package/dist/src/operator/retire.js +49 -31
  231. package/dist/src/operator/revert.js +179 -0
  232. package/dist/src/orchestrator/orchestrator-launch.js +116 -48
  233. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  234. package/dist/src/orchestrator/orchestrator-scheduling.js +78 -348
  235. package/dist/src/orchestrator/orchestrator.js +173 -361
  236. package/dist/src/orchestrator/retention.js +66 -15
  237. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  238. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  239. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  240. package/dist/src/orchestrator/startup-migration-steps.js +52 -0
  241. package/dist/src/orchestrator/startup-migrations.js +44 -0
  242. package/dist/src/paths.js +228 -75
  243. package/dist/src/pi/command-shape.js +12 -1
  244. package/dist/src/pi/pi-args.js +30 -5
  245. package/dist/src/pi/pi-backend.js +19 -27
  246. package/dist/src/pi/pi-bin.js +64 -67
  247. package/dist/src/pi/pi-event-line.js +17 -1
  248. package/dist/src/pi/pi-failure.js +87 -0
  249. package/dist/src/pi/pi-listed-models.js +53 -0
  250. package/dist/src/pi/pi-models.js +163 -42
  251. package/dist/src/pi/pi-run-result.js +1 -1
  252. package/dist/src/pi/pi-settings.js +86 -0
  253. package/dist/src/pi/pi-stream.js +137 -113
  254. package/dist/src/pi/pi-watchdogs.js +20 -20
  255. package/dist/src/pi-extension/bounded-output.js +25 -8
  256. package/dist/src/pi-extension/context-budget.js +5 -3
  257. package/dist/src/pi-extension/context-shake.js +54 -21
  258. package/dist/src/pi-extension/context-window.js +36 -0
  259. package/dist/src/pi-extension/full-output.js +5 -12
  260. package/dist/src/pi-extension/strip-thinking.js +79 -0
  261. package/dist/src/pi-extension/trim-system-prompt.js +37 -0
  262. package/dist/src/pi-extension/write-guard.js +37 -0
  263. package/dist/src/process/launch-services.js +11 -11
  264. package/dist/src/process/node-bin.js +16 -0
  265. package/dist/src/process/process-group.js +8 -18
  266. package/dist/src/process/process-table.js +1 -1
  267. package/dist/src/process/process.js +48 -9
  268. package/dist/src/process/run-marker.js +42 -7
  269. package/dist/src/process/supervisor.js +47 -35
  270. package/dist/src/prompt/dialect.js +46 -11
  271. package/dist/src/prompt/gate-notes.js +203 -0
  272. package/dist/src/prompt/gate-prompts.js +201 -188
  273. package/dist/src/prompt/notes-backlog.js +181 -0
  274. package/dist/src/prompt/principles.js +1 -1
  275. package/dist/src/prompt/prompt-followup.js +43 -29
  276. package/dist/src/prompt/prompt.js +255 -172
  277. package/dist/src/redeploy/redeploy-policy.js +29 -24
  278. package/dist/src/redeploy/redeploy-probes.js +1 -1
  279. package/dist/src/redeploy/redeploy.js +7 -6
  280. package/dist/src/redeploy/redeployer.js +78 -70
  281. package/dist/src/redeploy/self-reload.js +3 -2
  282. package/dist/src/report/report-data.js +374 -285
  283. package/dist/src/report/report-fold.js +430 -0
  284. package/dist/src/report/report-main-red.js +55 -0
  285. package/dist/src/report/report-render.js +321 -61
  286. package/dist/src/report/report-types.js +1 -0
  287. package/dist/src/report/report.js +26 -9
  288. package/dist/src/request-timeouts.js +2 -2
  289. package/dist/src/review/known-flakes.js +133 -14
  290. package/dist/src/review/post-merge-review.js +106 -0
  291. package/dist/src/review/protected-edits.js +7 -7
  292. package/dist/src/review/review-followup.js +21 -9
  293. package/dist/src/review/review-precheck.js +77 -33
  294. package/dist/src/review/review-run.js +395 -0
  295. package/dist/src/review/review-verdict.js +16 -2
  296. package/dist/src/review/review.js +172 -356
  297. package/dist/src/review/secret-leaks.js +147 -0
  298. package/dist/src/review/suite-rerun.js +3 -3
  299. package/dist/src/roles/legacy-role-ids.js +21 -0
  300. package/dist/src/roles/legacy-role-migration.js +135 -0
  301. package/dist/src/roles/role-catalog.js +172 -137
  302. package/dist/src/roles/role-guidance.js +112 -44
  303. package/dist/src/roles/role-labels.js +9 -0
  304. package/dist/src/roles/role-render.js +221 -28
  305. package/dist/src/roles/role-view.js +244 -33
  306. package/dist/src/roles/roles.js +37 -35
  307. package/dist/src/roles/runner-ids.js +97 -0
  308. package/dist/src/run/acceptance-greps.js +119 -0
  309. package/dist/src/run/feedback-filings.js +51 -0
  310. package/dist/src/{tick → run}/qa-coverage.js +10 -27
  311. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +166 -136
  312. package/dist/src/run/run-detail-data.js +179 -0
  313. package/dist/src/run/run-detail-render.js +98 -0
  314. package/dist/src/run/run-detail.js +168 -0
  315. package/dist/src/run/run-finalize.js +154 -0
  316. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -6
  317. package/dist/src/run/run-prompt.js +304 -0
  318. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +80 -35
  319. package/dist/src/run/run-stage.js +295 -0
  320. package/dist/src/run/run-timing.js +198 -0
  321. package/dist/src/run/run-usage.js +180 -0
  322. package/dist/src/run/run-verdict.js +301 -0
  323. package/dist/src/run/selfcheck-digest.js +40 -0
  324. package/dist/src/run/stage-backlog-checks.js +249 -0
  325. package/dist/src/run/stage-check.js +172 -0
  326. package/dist/src/run/stage-path-checks.js +730 -0
  327. package/dist/src/runner/leftover.js +185 -0
  328. package/dist/src/{loop → runner}/model-fallback.js +13 -12
  329. package/dist/src/runner/revision.js +202 -0
  330. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +10 -10
  331. package/dist/src/runner/runner-outcome.js +107 -0
  332. package/dist/src/runner/runner-pi.js +433 -0
  333. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +34 -28
  334. package/dist/src/runner/runner-run.js +625 -0
  335. package/dist/src/runner/runner-state-load.js +268 -0
  336. package/dist/src/runner/runner-state.js +102 -0
  337. package/dist/src/runner/runner.js +350 -0
  338. package/dist/src/scheduling/backoff.js +64 -54
  339. package/dist/src/scheduling/claims.js +46 -26
  340. package/dist/src/scheduling/once-round.js +30 -30
  341. package/dist/src/scheduling/quiet-hours.js +26 -34
  342. package/dist/src/scheduling/scheduling.js +87 -74
  343. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  344. package/dist/src/status/status-budget.js +123 -0
  345. package/dist/src/status/status-data.js +106 -191
  346. package/dist/src/status/status-inbox.js +47 -0
  347. package/dist/src/status/status-install.js +26 -0
  348. package/dist/src/status/status-polls.js +70 -59
  349. package/dist/src/status/status-roles.js +131 -0
  350. package/dist/src/{fleet → team}/error-storm.js +16 -16
  351. package/dist/src/{fleet → team}/failure-spread.js +10 -10
  352. package/dist/src/{fleet → team}/orchestrator-info.js +97 -25
  353. package/dist/src/{fleet → team}/reclaim.js +9 -9
  354. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +60 -26
  355. package/dist/src/team/team-polls.js +299 -0
  356. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +47 -40
  357. package/dist/src/text/datetime.js +102 -16
  358. package/dist/src/text/format.js +49 -3
  359. package/dist/src/text/markdown.js +8 -5
  360. package/dist/src/text/phrases.js +89 -54
  361. package/dist/src/text/suggest.js +3 -2
  362. package/dist/src/text/text.js +68 -14
  363. package/dist/src/ui/badges.js +165 -93
  364. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  365. package/dist/src/ui/gui/gui-client-boot.js +221 -42
  366. package/dist/src/ui/gui/gui-client-composer.js +105 -27
  367. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  368. package/dist/src/ui/gui/gui-client-drawer.js +226 -130
  369. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  370. package/dist/src/ui/gui/gui-client-history.js +492 -68
  371. package/dist/src/ui/gui/gui-client-labels.js +23 -0
  372. package/dist/src/ui/gui/gui-client-markdown.js +1 -1
  373. package/dist/src/ui/gui/gui-client-model.js +84 -76
  374. package/dist/src/ui/gui/gui-client-operator.js +112 -78
  375. package/dist/src/ui/gui/gui-client-pending.js +248 -41
  376. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  377. package/dist/src/ui/gui/gui-client-report.js +122 -179
  378. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +118 -76
  379. package/dist/src/ui/gui/gui-client-settings.js +180 -20
  380. package/dist/src/ui/gui/gui-client-team.js +252 -0
  381. package/dist/src/ui/gui/gui-client.js +48 -40
  382. package/dist/src/ui/gui/gui-icons.js +0 -2
  383. package/dist/src/ui/gui/gui-page.js +25 -24
  384. package/dist/src/ui/gui/gui-styles.js +46 -38
  385. package/dist/src/ui/progress-data.js +11 -11
  386. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +40 -40
  387. package/dist/src/ui/status-model.js +276 -194
  388. package/dist/src/ui/status-payload.js +227 -62
  389. package/dist/src/ui/status-render.js +196 -108
  390. package/dist/src/ui/team-alerts.js +532 -0
  391. package/dist/src/ui/tone.js +11 -11
  392. package/dist/src/ui/transcript-run.js +58 -0
  393. package/dist/src/ui/transcript-tail.js +28 -19
  394. package/dist/src/ui/transcript.js +85 -38
  395. package/dist/src/ui/tui/tui-backlog.js +132 -21
  396. package/dist/src/ui/tui/tui-editor-answer.js +93 -0
  397. package/dist/src/ui/tui/tui-editor-attach.js +105 -0
  398. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  399. package/dist/src/ui/tui/tui-editor-brief.js +105 -0
  400. package/dist/src/ui/tui/tui-editor-budget.js +159 -0
  401. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  402. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  403. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  404. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  405. package/dist/src/ui/tui/tui-editor-entry.js +219 -0
  406. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  407. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  408. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  409. package/dist/src/ui/tui/tui-editor-interval.js +124 -0
  410. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  411. package/dist/src/ui/tui/tui-editor-notes.js +112 -0
  412. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  413. package/dist/src/ui/tui/tui-editor-pause.js +255 -0
  414. package/dist/src/ui/tui/tui-editor-prompt.js +90 -0
  415. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  416. package/dist/src/ui/tui/tui-editor-queued.js +94 -0
  417. package/dist/src/ui/tui/tui-editor-quiet.js +170 -0
  418. package/dist/src/ui/tui/tui-editor-retire.js +81 -0
  419. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  420. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  421. package/dist/src/ui/tui/tui-editor-search.js +85 -0
  422. package/dist/src/ui/tui/tui-editor-session.js +123 -0
  423. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  424. package/dist/src/ui/tui/tui-editors.js +185 -0
  425. package/dist/src/ui/tui/tui-frame.js +107 -58
  426. package/dist/src/ui/tui/tui-history.js +16 -12
  427. package/dist/src/ui/tui/tui-input.js +56 -60
  428. package/dist/src/ui/tui/tui-keys.js +106 -1825
  429. package/dist/src/ui/tui/tui-pane-activity.js +102 -0
  430. package/dist/src/ui/tui/tui-pane-backlog.js +269 -0
  431. package/dist/src/ui/tui/tui-pane-brief.js +29 -0
  432. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  433. package/dist/src/ui/tui/tui-pane-health.js +174 -0
  434. package/dist/src/ui/tui/tui-pane-history.js +636 -0
  435. package/dist/src/ui/tui/tui-pane-pending.js +369 -0
  436. package/dist/src/ui/tui/tui-pane-queued.js +159 -0
  437. package/dist/src/ui/tui/tui-pane-report.js +154 -0
  438. package/dist/src/ui/tui/tui-pane-session.js +120 -0
  439. package/dist/src/ui/tui/tui-pane-transcript.js +224 -0
  440. package/dist/src/ui/tui/tui-pane.js +125 -44
  441. package/dist/src/ui/tui/tui-panes.js +364 -0
  442. package/dist/src/ui/tui/tui.js +52 -34
  443. package/dist/src/verdict/fix-claim.js +135 -62
  444. package/dist/src/verdict/no-change.js +19 -0
  445. package/dist/src/verdict/refusal.js +80 -24
  446. package/dist/src/verdict/reply-contract.js +51 -28
  447. package/dist/src/version.js +2 -2
  448. package/package.json +5 -2
  449. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  450. package/dist/src/brief-edit.js +0 -42
  451. package/dist/src/cli/cli-run.js +0 -469
  452. package/dist/src/fleet/fleet-polls.js +0 -207
  453. package/dist/src/landing/backlog-conflicts.js +0 -116
  454. package/dist/src/landing/landing-check-failures.js +0 -141
  455. package/dist/src/landing/landing-drain.js +0 -204
  456. package/dist/src/landing/landing-git.js +0 -190
  457. package/dist/src/landing/landing-pipeline.js +0 -96
  458. package/dist/src/landing/landing-queue-render.js +0 -56
  459. package/dist/src/landing/landing-queue.js +0 -149
  460. package/dist/src/landing/landing-slot.js +0 -256
  461. package/dist/src/landing/landing-vetting.js +0 -196
  462. package/dist/src/loop/leftover.js +0 -129
  463. package/dist/src/loop/loop-pi.js +0 -370
  464. package/dist/src/loop/loop-state.js +0 -204
  465. package/dist/src/loop/loop-tick.js +0 -545
  466. package/dist/src/loop/loop.js +0 -315
  467. package/dist/src/loop/revision.js +0 -61
  468. package/dist/src/pi-extension/role-notes.js +0 -40
  469. package/dist/src/roles/loop-ids.js +0 -60
  470. package/dist/src/tick/stage-check.js +0 -302
  471. package/dist/src/tick/telemetry-digest.js +0 -24
  472. package/dist/src/tick/tick-detail-data.js +0 -107
  473. package/dist/src/tick/tick-detail.js +0 -150
  474. package/dist/src/tick/tick-finalize.js +0 -135
  475. package/dist/src/tick/tick-prompt.js +0 -201
  476. package/dist/src/tick/tick-stage.js +0 -223
  477. package/dist/src/tick/tick-timing.js +0 -195
  478. package/dist/src/tick/tick-usage.js +0 -142
  479. package/dist/src/tick/tick-verdict.js +0 -201
  480. package/dist/src/ui/fleet-alerts.js +0 -174
  481. package/dist/src/ui/gui/gui-client-fleet.js +0 -576
  482. package/dist/src/ui/gui/gui-client-sound.js +0 -52
package/README.md CHANGED
@@ -1,14 +1,23 @@
1
1
  # tumwater
2
2
 
3
- tumwater is an opinionated autonomous development harness built on
4
- [pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a fleet of
5
- role-driven loops (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
3
+ > ### ⚠️ Warning: tumwater eats tokens
4
+ >
5
+ > tumwater runs models continuously, all day, on every role. A busy team can burn through
6
+ > **on the order of a billion tokens a day or more**. On pay-per-token API keys that is real,
7
+ > unbounded money; on subscription backends it burns through your plan's usage windows just as
8
+ > fast. Set `maxDailyCostUsd` before you start, and watch actual spend with `tumwater report`
9
+ > and `tumwater status`.
10
+
11
+ tumwater is an opinionated autonomous development team, built on
12
+ [pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a team of
13
+ roles (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
6
14
  one small, reviewed commit at a time. It came from wanting to write only the brief and let a team
7
- of always-on specialists do the rest: each loop owns one concern, lands one change per tick,
15
+ of always-on specialists do the rest: each role owns one concern, merges one change per run,
8
16
  sleeps when it has nothing to do, and wakes when main moves. All project state lives in the
9
- local git repo, and no remote is ever touched.
17
+ local git repo, and no remote is ever touched. The words used here (team, role, run, merge) are
18
+ defined in [docs/glossary.md](docs/glossary.md).
10
19
 
11
- ![The tumwater web dashboard running tumwater's own fleet: a sidebar with the project, its fleet status, the Fleet, History, Usage, Failures, and Settings views, today's spend against the cap, and the pause control; an alert that the running build is behind main; the director prompt box; today's progress (loops in flight, commits landed, ticks, open backlog); and the loops grouped by what they are doing, each with its status, current work or last result, spend, and controls](docs/gui.png)
20
+ ![The tumwater web dashboard running tumwater's own team: a sidebar with the project, its team status, and the Team, History, Usage, Failures, and Settings views, with today's spend against the cap and the pause control; the director prompt box; and today's progress (roles running, commits merged, runs, open backlog) above the roles grouped by what they are doing, each with its status, current work or last result, spend, and controls](docs/gui.png)
12
21
 
13
22
  ## Install
14
23
 
@@ -23,13 +32,19 @@ instead: `npm install && npm run build && npm link`.
23
32
 
24
33
  With `claude` installed and logged in, `npm run smoke:claude` is an opt-in live check of the
25
34
  claude backend: it drives the real CLI through a throwaway repo (a few cents of API-equivalent
26
- spend on a subscription) and is not part of `npm test`, which stays green without claude.
35
+ spend on a subscription) and is not part of `npm test`, which stays green without claude. `npm run
36
+ smoke:opencode` is the same live check for the opencode backend, driving the real `opencode` CLI
37
+ against its free DeepSeek tier; it needs an OpenCode V2 install (`brew install opencode`) and is
38
+ also outside `npm test`. `npm run smoke:codex` is the same live check for the codex backend,
39
+ driving the real `codex` CLI against a logged-in ChatGPT subscription; it needs a Codex install
40
+ and is also outside `npm test`.
27
41
 
28
42
  ## Status
29
43
 
30
44
  <!-- tumwater:status:start -->
31
- **v0.1.3**: working harness. The director and 14 of the 15 roles are enabled by default; `telemetry`,
32
- which files harness bugs from tumwater's own event log, is opt-in (`roles.telemetry.enabled`).
45
+ **v0.3.0**: working tumwater. The director and 14 of the 16 roles are enabled by default; `selfcheck`,
46
+ which files tumwater bugs from tumwater's own event log, and `feedback`, which files them as GitHub
47
+ issues on znation/tumwater, are opt-in (`roles.selfcheck.enabled`, `roles.feedback.enabled`).
33
48
 
34
49
  Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
35
50
  [QUESTIONS.md](QUESTIONS.md) (open questions).
@@ -41,40 +56,74 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
41
56
  cd your-project # a new or existing directory
42
57
  tumwater init "Build a tiny markdown-to-html converter CLI in Python."
43
58
  # --template, --file, and --adopt pick a starting point (see docs/cli.md)
44
- tumwater run # start the loops (Ctrl+C to stop)
45
- tumwater run --gui # ... and serve the browser dashboard at http://127.0.0.1:7180
46
- tumwater run --for 2h # run for a bounded window, then drain, summarize, and exit
47
- tumwater run --once # one round of ticks for cron/CI; exits 3 if a loop's tick failed
59
+ tumwater start # start the team (Ctrl+C to stop)
60
+ tumwater start --gui # ... and serve the browser dashboard at http://127.0.0.1:7180
61
+ tumwater start --tui # ... and attach the terminal TUI in the same window
62
+ tumwater start --for 2h # run for a bounded window, then finish current work, summarize, and exit
63
+ tumwater start --once # one round of runs for cron/CI; exits 3 if a run failed
48
64
  ```
49
65
 
50
66
  A fresh or empty project is seeded with `"bootstrap": {"untilPlansDone": 5}`, which holds the
51
- maintenance loops until 5 plans are done; remove it from `tumwater.json` to end that early.
67
+ maintenance roles until 5 plans are done; remove it from `tumwater.json` to end that early.
52
68
 
53
- From another terminal, `tumwater tui` or the browser dashboard watches the fleet, `tumwater status`
54
- and `tumwater logs -f` check it, `tumwater brief --file <path>` replaces the initial prompt,
55
- `tumwater prompt "<text>"` queues a request for the director, `tumwater plan` and `tumwater bug`
56
- file backlog entries, and `tumwater pause`, `resume`, `wake`, and `stop` control the loops.
57
- `tumwater doctor` audits the fleet and `tumwater report` shows usage and cost. `gui
69
+ From another terminal, `tumwater tui` or the browser dashboard watches the team, `tumwater status`
70
+ and `tumwater logs -f` check it, `tumwater brief --file <path>` replaces the brief,
71
+ `tumwater prompt "<text>"` sends a message to the director's inbox, `tumwater plan` and
72
+ `tumwater bug` file backlog entries, `tumwater revert <sha>` undoes one merged change on main,
73
+ and `tumwater pause`, `resume`, `wake`, and `stop` control the roles. `tumwater doctor` audits the team and `tumwater report` shows usage and cost. `gui
58
74
  --all-interfaces` exposes the dashboard, and with it the director prompt, to your whole network,
59
75
  so pair it with `--token <secret>`.
60
76
 
77
+ `tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
78
+ model cannot read images, the command refuses the attachment up front (naming the fix) unless
79
+ the role reroutes a blind read to a vision model, in which case it says where the message will
80
+ run; `tumwater doctor` probes each claimed image-input model once (cached 7 days, `--reprobe` to
81
+ force) and the dashboards and `tumwater role <id>` say which roles read images and where a blind
82
+ run reroutes.
83
+
61
84
  Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
62
- same list, and `tumwater completion bash|zsh` prints shell completion. Settings live in
63
- `tumwater.json` and edits apply live while the fleet runs; see [docs/config.md](docs/config.md).
85
+ same list, and `tumwater completion bash|zsh|fish` prints shell completion. Settings live in
86
+ `tumwater.json` and edits apply live while the team runs; see [docs/config.md](docs/config.md).
64
87
 
65
88
  **Backends:** any OpenAI-compatible model pi can reach works, chosen by `model` in `tumwater.json`
66
- with a `fallback` that a failing role switches to. `agent: "claude"` runs bare selectors on Claude
67
- Code instead. Requirements, the fallback and usage-window behavior, and how subscription runs are
68
- billed (as notional spend, outside the daily cap) are in [docs/backends.md](docs/backends.md).
89
+ with a `fallback` that a failing role switches to. `backend` beside `model` picks who runs it:
90
+ `"backend": "claude"` runs a model id on Claude Code, `"backend": "opencode"` on the OpenCode CLI — its free
91
+ DeepSeek tier needs no login or API key — and `"backend": "codex"` on the Codex CLI's ChatGPT subscription
92
+ (no API key; its two usage windows behave like a claude subscription's). Requirements, the fallback, free-ness, and usage-window
93
+ behavior, and how subscription runs are billed (as estimated spend, outside the daily cap) are in
94
+ [docs/backends.md](docs/backends.md).
95
+
96
+ **Mixing providers:** `model` says which model, `backend` says which program runs it (`pi`, the
97
+ default, `claude`, `opencode` or `codex`). A tier entry, a role, a review and a fallback step each take an
98
+ object with both, so one config can mix a Claude strong tier with pi providers:
99
+
100
+ ```json
101
+ {
102
+ "model": {
103
+ "default": "huggingface/deepseek-ai/DeepSeek-V4.1-Flash:deepinfra",
104
+ "strong": { "backend": "claude", "model": "claude-opus-5-5" }
105
+ },
106
+ "fallback": "omlx/Qwen3.8-27B-MLX-oQ4e-mtp"
107
+ }
108
+ ```
109
+
110
+ The default tier runs on pi through Hugging Face, the strong model jobs (the director, the plan
111
+ role, the reviewer, and the conflict resolver) run on the claude CLI's included usage, and the fallback runs on pi through
112
+ oMLX — all concurrently, under each provider's cap (set it with `maxConcurrentPerProvider`;
113
+ `tumwater status` shows how many runs each key has going out of its cap). An OpenCode tier is
114
+ `{ "backend": "opencode", "model": "deepseek-v4.1-flash-free" }`, so its free tier can sit beside pi
115
+ and claude in one config; a codex tier is `{ "backend": "codex", "model": "gpt-6.1-sol" }`, so a ChatGPT
116
+ subscription can cover the team's default tier beside pi and claude. A Claude model that should stay on pi (billed per token) is the plain
117
+ `anthropic/claude-…` selector. See [plans/agent-backends.md §3](plans/agent-backends.md).
69
118
 
70
- For how the loops, review gate, scheduling, and self-redeploy work, see
119
+ For how the roles, review gate, scheduling, and updates work, see
71
120
  [docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
72
121
  code against human-written open source, see [docs/code-metrics.md](docs/code-metrics.md).
73
122
 
74
- ## Appendix: initial prompt
123
+ ## Appendix: the brief
75
124
 
76
- The brief this repository was started from, kept for history. The harness still reads it from
77
- between the markers below on every tick.
125
+ The brief this repository was started from, kept for history. tumwater still reads the brief from
126
+ between the markers below on every run.
78
127
 
79
128
  <!-- tumwater:prompt:start -->
80
129
  Idea: agentic harness
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "e44157e8609091b848898ab0e370e0995d931404",
3
- "builtAt": 1791912860508,
2
+ "sha": "d36bf79c535e10fe228f058ecc1ca839f3af61b1",
3
+ "builtAt": 1792789212458,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -0,0 +1,23 @@
1
+ /** Appending a backend's configured passthrough args to an already-built argv. Shared by the
2
+ * claude, opencode and codex argv builders (claude-args.ts, opencode-args.ts and codex-args.ts),
3
+ * whose passthrough handling is otherwise identical, so the three backends reject a reserved
4
+ * flag by the same rule and the same message shape. A passthrough arg equal to a flag the
5
+ * harness itself sets is appended AFTER the harness's own copy, and all three backends' parsers
6
+ * are last-wins, so it would silently override the model, the session choice, or the isolation
7
+ * flags — each such arg is rejected here with the offending arg named. `moduleName` names the
8
+ * blocking backend in the error (e.g. `claudeArgs`, `opencodeArgs` or `codexArgs`); `reserved`
9
+ * is that backend's HARNESS_*_FLAGS set. */
10
+ export function appendPassthroughArgs(args, passthrough, reserved, moduleName) {
11
+ for (const arg of passthrough ?? []) {
12
+ // Match the flag itself even when it carries its value as `--flag=value`.
13
+ const flag = arg.split("=", 1)[0];
14
+ if (reserved.has(flag)) {
15
+ // Name the reserved flag (the arg may carry it as `--flag=value`, so `flag` here is the
16
+ // normalized token) and the fix, so the operator knows exactly which passthrough to drop
17
+ // rather than which of several harness-set flags the arg collided with.
18
+ throw new Error(`${moduleName}: passthrough arg ${JSON.stringify(arg)} repeats a flag tumwater sets (` +
19
+ `${flag} is reserved) — drop it from your passthrough args`);
20
+ }
21
+ args.push(arg);
22
+ }
23
+ }
@@ -1,8 +1,10 @@
1
1
  /** The backend seam (plans/agent-backends.md "The seam"): what one non-interactive agent
2
2
  * run needs from its backend — how to build the command, how to translate its native stdout
3
3
  * into tumwater's canonical event stream, and how to classify its exit. src/agent/run-agent.ts
4
- * owns everything backend-neutral (spawn, marker, watchdogs, abort, group kill and marker
5
- * sweep, stderr capture); a backend registered here supplies the rest. src/agent/run-agent.ts
4
+ * owns everything backend-neutral (spawn, marker, watchdogs, abort, stderr capture); the
5
+ * post-run settlement — the group-kill and marker sweeps on exit, the spawn-error result and
6
+ * the close-time reconcile/classify that builds the `PiRunResult` — lives in
7
+ * src/agent/run-settle.ts. A backend registered here supplies the rest. src/agent/run-agent.ts
6
8
  * registers the pi and claude adapters here.
7
9
  *
8
10
  * The registry is keyed by id and fed by explicit registration (src/agent/run-agent.ts
@@ -10,7 +12,7 @@
10
12
  * real one. */
11
13
  import { hasResumableSession } from "./session-ledger.js";
12
14
  /** The backend id used when a config names none: pi, the harness's default backend. */
13
- export const DEFAULT_AGENT = "pi";
15
+ export const DEFAULT_BACKEND = "pi";
14
16
  const registry = new Map();
15
17
  /** Register one backend under its id, replacing any previous entry with the same id. */
16
18
  export function registerAgentBackend(backend) {
@@ -18,7 +20,7 @@ export function registerAgentBackend(backend) {
18
20
  }
19
21
  /** The backend registered under `id`, or undefined when none is — the caller decides whether
20
22
  * an unknown id is a hard error. */
21
- export function agentBackend(id) {
23
+ export function backendById(id) {
22
24
  return registry.get(id);
23
25
  }
24
26
  /** Whether the backend for `opts.agent` can resume a session in `dir` for a run of
@@ -26,9 +28,9 @@ export function agentBackend(id) {
26
28
  * "Sessions"), so a claude author or gate run resumes through its ledger id instead of being
27
29
  * read as having no session. Falls back to the backend-neutral session-file check when the
28
30
  * backend is unregistered or supplies no `resumable` hook. */
29
- export function agentSessionResumable(dir, opts) {
30
- const backend = agentBackend(opts.agent);
31
+ export function backendSessionResumable(dir, opts, configDir) {
32
+ const backend = backendById(opts.agent);
31
33
  if (backend?.resumable)
32
- return backend.resumable(dir, opts);
34
+ return backend.resumable(dir, opts, configDir);
33
35
  return hasResumableSession(dir);
34
36
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The per-backend capabilities that the config, prompt, watchdog, progress and doctor layers
3
+ * read (plans/opencode-backend.md "Groundwork: the claude-only branches become backend
4
+ * traits"). Pure — imports nothing from the runtime registry (agent-backend.ts) — so those
5
+ * layers can read a backend's traits without pulling the registry in. That is the same reason
6
+ * `BACKEND_DEFAULT_MODELS` lives in src/config/model-selector.ts.
7
+ *
8
+ * An id with no entry reads as pi, because src/agent/run-agent.ts routes an unregistered id to
9
+ * the pi backend.
10
+ */
11
+ import { HARNESS_CLAUDE_FLAGS } from "./claude/claude-args.js";
12
+ import { HARNESS_OPENCODE_FLAGS } from "./opencode/opencode-args.js";
13
+ import { HARNESS_CODEX_FLAGS } from "./codex/codex-args.js";
14
+ import { HARNESS_PI_FLAGS } from "../config/config-schema.js";
15
+ /** Whether the backend streams tool output. pi streams content-bearing tool updates, so its
16
+ * calls are classified per command; claude, opencode and codex send none, so silence carries no
17
+ * hang signal and every call is judged by the buffered threshold (plans/agent-backends.md
18
+ * "Watchdogs and progress"). Only claude, opencode and codex are false; an unknown id reads as
19
+ * pi. */
20
+ export function streamsToolOutput(backend) {
21
+ // Only claude, opencode and codex stream no tool output; every other id (including an
22
+ // unregistered one) reads as pi, which does stream.
23
+ return backend !== "claude" && backend !== "opencode" && backend !== "codex";
24
+ }
25
+ /** Whether the backend's model jobs resolve through pi's models.json. True for pi only (and an
26
+ * unknown id, which reads as pi); a claude, opencode or codex job reads no pi pricing and needs
27
+ * no definitions file. */
28
+ export function usesPiModels(backend) {
29
+ // Only claude, opencode and codex read no pi models.json; every other id (including an
30
+ // unregistered one) reads as pi, which does.
31
+ return backend !== "claude" && backend !== "opencode" && backend !== "codex";
32
+ }
33
+ /** The reason string config validation prints for a reserved `backends.<id>.args` flag, or
34
+ * undefined when the flag is not reserved for that backend. Wraps the HARNESS_*_FLAGS sets for
35
+ * claude (HARNESS_CLAUDE_FLAGS), opencode (HARNESS_OPENCODE_FLAGS), codex (HARNESS_CODEX_FLAGS)
36
+ * and pi (HARNESS_PI_FLAGS), keeping each backend's reason wording. The flag is the bare name —
37
+ * checkReservedArgs splits `--mode=x` on `=` before calling. */
38
+ export function reservedPassthroughFlag(backend, flag) {
39
+ switch (backend) {
40
+ case "pi":
41
+ return HARNESS_PI_FLAGS.get(flag);
42
+ case "claude":
43
+ return HARNESS_CLAUDE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
44
+ case "opencode":
45
+ return HARNESS_OPENCODE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
46
+ case "codex":
47
+ // The standalone-flag screen; the `-c <key>=…` / `--config <key>=…` config-key screen is
48
+ // arity-aware (a `-c` token and its key are different argv elements), so config-validation
49
+ // owns it against HARNESS_CODEX_CONFIG_KEYS (codex-args.ts) rather than here.
50
+ return HARNESS_CODEX_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
51
+ default:
52
+ // An unknown backend has no known harness argv to clash with, so nothing is reserved.
53
+ return undefined;
54
+ }
55
+ }
56
+ /** A seam's model as one readable token: `<provider>/<model>` for a pi seam (and any unknown
57
+ * id, which reads as pi), the bare model for claude — or "claude backend" when there is none —
58
+ * `opencode/<model>` for opencode, whose ids all read under the opencode namespace, and the
59
+ * bare model for codex — or "codex default" when there is none. The one home of the label,
60
+ * shared by the doctor report and the image-input surface strings so they cannot drift. */
61
+ export function modelLabel(backend, provider, model) {
62
+ if (backend === "claude")
63
+ return model ?? "claude backend";
64
+ if (backend === "opencode")
65
+ return `opencode/${model ?? "?"}`;
66
+ if (backend === "codex")
67
+ return model ?? "codex default";
68
+ return `${provider ?? "?"}/${model ?? "?"}`;
69
+ }
70
+ /** A backend id's display name as the status surfaces show it: title-cased
71
+ * (`id.charAt(0).toUpperCase() + id.slice(1)`, so `"claude"` → `"Claude"`, `"codex"` →
72
+ * `"Codex"`). The one home of the spelling the subscription window's per-backend block label
73
+ * (part 3a/9) and the later display parts share, so a backend's name cannot drift between
74
+ * them. */
75
+ export function backendDisplayName(id) {
76
+ return id.charAt(0).toUpperCase() + id.slice(1);
77
+ }
@@ -1,172 +1,79 @@
1
- /** The claude account state: which billing pays for claude runs, and the newest subscription
1
+ /** The claude account state: the claude backend's subscription account (src/agent/
2
+ * subscription-account.ts) — which billing pays for claude runs, and the newest subscription
2
3
  * usage-window reading each run reported (plans/agent-backends.md §7 "Billing" and §8 "The
3
4
  * subscription usage window"). Persisted at `.tumwater/state/claude-account.json`, written only
4
5
  * by the orchestrator process, so the sibling parts that throttle or display the subscription
5
6
  * window (6b/7–6e/7) read one answer instead of re-deriving it per surface.
6
7
  *
7
- * The file is a cache, never a source of truth: a run that reports billing overwrites it, and a
8
- * missing or unreadable file reads as "no subscription". `resetsAt` is stored as epoch
9
- * milliseconds (the run parser reports unix seconds; observeClaudeAccount converts once here),
10
- * so consumers compare it to `Date.now()` directly. */
8
+ * Everything here is a thin wrapper over the generic per-backend store (found
9
+ * grounds/codex-backend.md "Groundwork: the subscription account is per backend", item 1),
10
+ * specialized to the backend id "claude", plus the claude-only seed (auth status, the API-key
11
+ * and third-party-provider billing checks) that no other backend shares. The file is a cache,
12
+ * never a source of truth: a run that reports billing overwrites it, and a missing or
13
+ * unreadable file reads as "no subscription". `resetsAt` is stored as epoch milliseconds (the
14
+ * run parser reports unix seconds; the store converts once), so consumers compare it to
15
+ * `Date.now()` directly. */
11
16
  import { spawnSync } from "node:child_process";
12
- import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
13
- import { finiteNumber, isJsonObject, parseJsonObject } from "../../files/json-object.js";
14
- import { claudeAccountPath } from "../../paths.js";
15
- import { claudeBackend } from "./claude-backend.js";
17
+ import os from "node:os";
18
+ import { resolvedBackendConfig } from "../../config/config-views.js";
19
+ import { parseJsonObject } from "../../files/json-object.js";
20
+ import { observeAccount, readAccount, subscriptionWindowRejected, utilizationPct, windowEntries, windowLabel, windowReserved, windowUsedUp, writeAccount, } from "../subscription-account.js";
21
+ import { claudeBackend, claudeConfigDir } from "./claude-backend.js";
16
22
  import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
17
- /** True when a failed run ended on a subscription usage window the account reported as
18
- * `rejected` without overage — the signal the fleet hold throttles on (plans/agent-backends.md
19
- * §8). Such a run is a transient of the world, not a failure of the session: its session and
20
- * worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
21
- * partial work or reset. API-billed claude and overage are budget/fallback matters, not a
22
- * window hold, so they read false; a pi 429 reports no window and also reads false. */
23
- export function subscriptionWindowRejected(run) {
24
- return (run.transientRateLimit &&
25
- run.billing === "subscription" &&
26
- (run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
27
- }
28
- /** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
29
- * every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
30
- const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
31
- /** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
32
- * type falls back to its words with underscores as spaces. The one home of the window spelling
33
- * the status badge and the role view share. */
34
- export function claudeWindowLabel(type) {
35
- const m = /^(\w+)_(hour|day|week)$/.exec(type);
36
- if (!m)
37
- return type.replace(/_/g, " ");
38
- const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
39
- const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
40
- const word = m[1] ?? "";
41
- return `${words[word] ?? word}${unit}`;
42
- }
43
- /** The account's windows as a list, most constrained first: by status severity, then the
44
- * reported utilization (a missing reading sorts below any number), then the earlier reset, then
45
- * the type name — so a tie always orders the same way. Empty when the account reported none. */
46
- export function claudeWindowEntries(account) {
47
- return Object.entries(account.windows)
48
- .map(([type, w]) => ({ type, ...w }))
49
- .sort((a, b) => {
50
- const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
51
- if (rank !== 0)
52
- return rank;
53
- const util = (b.utilization ?? -1) - (a.utilization ?? -1);
54
- if (util !== 0)
55
- return util;
56
- const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
57
- if (reset !== 0)
58
- return reset;
59
- return a.type.localeCompare(b.type);
60
- });
61
- }
62
- /** Read the persisted account state, or null when it is absent or unreadable. A file without a
63
- * usable top-level `observedAt` reads as absent (the plan's torn-file rule), and a window whose
64
- * `resetsAt` is at or before `now` reads back with `status: "reset"` — the reader, not every
65
- * consumer, decides that the window no longer limits. */
23
+ /** Read the persisted claude account state, or null when it is absent or unreadable. A file
24
+ * without a usable top-level `observedAt` reads as absent (the plan's torn-file rule), and a
25
+ * window whose `resetsAt` is at or before `now` reads back with `status: "reset"` — the
26
+ * reader, not every consumer, decides that the window no longer limits. */
66
27
  export function readClaudeAccount(root, now = Date.now()) {
67
- const doc = readJsonFile(claudeAccountPath(root));
68
- if (doc === null)
69
- return null;
70
- const billing = doc.billing === "subscription" || doc.billing === "api" ? doc.billing : undefined;
71
- const observedAt = finiteNumber(doc.observedAt, Number.NaN);
72
- if (billing === undefined || !Number.isFinite(observedAt))
73
- return null;
74
- const windows = {};
75
- if (isJsonObject(doc.windows)) {
76
- for (const [type, value] of Object.entries(doc.windows)) {
77
- const window = readWindow(value, observedAt, now);
78
- if (window !== undefined)
79
- setWindow(windows, type, window);
80
- }
81
- }
82
- return {
83
- billing,
84
- ...(typeof doc.subscriptionType === "string" ? { subscriptionType: doc.subscriptionType } : {}),
85
- observedAt,
86
- windows,
87
- };
88
- }
89
- /** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
90
- * missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
91
- * status to `reset`. */
92
- function readWindow(value, fileObservedAt, now) {
93
- if (!isJsonObject(value) || typeof value.status !== "string")
94
- return undefined;
95
- const observedAt = finiteNumber(value.observedAt, fileObservedAt);
96
- const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
97
- const resets = Number.isFinite(resetsAt);
98
- // utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
99
- // non-finite fraction would render as `Infinity%` in the badges and poison the
100
- // claudeWindowEntries comparator (Infinity - Infinity is NaN).
101
- const utilization = finiteNumber(value.utilization, undefined);
102
- const window = {
103
- status: resets && resetsAt <= now ? "reset" : value.status,
104
- ...(resets ? { resetsAt } : {}),
105
- ...(utilization !== undefined ? { utilization } : {}),
106
- ...(typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
107
- ...(value.isUsingOverage === true ? { isUsingOverage: true } : {}),
108
- observedAt,
109
- };
110
- return window;
28
+ return readAccount(root, "claude", now);
111
29
  }
112
- /** Store one window under its `rateLimitType`. A plain `windows[type] = window` loses a type of
113
- * `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's prototype
114
- * instead of adding an own entry, so the window vanishes from every `Object.entries`/`values`
115
- * reader (the display, the hold, and accountUsesOverage). Defining the own property keeps even
116
- * that key — the same inherited-key guard claude-stream's TOOL_NAMES lookup uses. */
117
- function setWindow(windows, type, window) {
118
- Object.defineProperty(windows, type, { value: window, enumerable: true, writable: true, configurable: true });
119
- }
120
- /** Overwrite the account file with `account`, creating the state directory. The orchestrator's
121
- * one writer; a failure to write is the caller's to handle (best-effort at the tick boundary).
122
- * Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write leaves the
123
- * previous file whole, not a torn one the reader heals to "no subscription" — which would
124
- * silently drop the billing answer and the window hold until the next run re-observed them. */
30
+ /** Overwrite the claude account file with `account`, creating the state directory. The
31
+ * orchestrator's one writer; a failure to write is the caller's to handle (best-effort at the
32
+ * run boundary). Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write
33
+ * leaves the previous file whole, not a torn one the reader heals to "no subscription" — which
34
+ * would silently drop the billing answer and the window hold until the next run re-observed
35
+ * them. */
125
36
  export function writeClaudeAccount(root, account) {
126
- writeJsonAtomic(claudeAccountPath(root), account, true);
37
+ writeAccount(root, "claude", account);
127
38
  }
128
- /** Fold one run's billing and rate-window readings into the account file: the run's `billing`
129
- * overwrites the stored answer, and each reported window replaces the stored reading for its
130
- * `type`, stamped with `observedAt`. A run that reported neither billing nor windows leaves the
131
- * file untouched (and returns whatever was already there). */
39
+ /** Fold one run's billing and rate-window readings into the claude account file: the run's
40
+ * `billing` overwrites the stored answer, and each reported window replaces the stored reading
41
+ * for its `type`, stamped with `observedAt`. A run that reported neither billing nor windows
42
+ * leaves the file untouched (and returns whatever was already there). */
132
43
  export function observeClaudeAccount(root, run, now = Date.now()) {
133
- const existing = readClaudeAccount(root, now);
134
- const billing = run.billing ?? existing?.billing;
135
- const reported = run.rateWindows ?? [];
136
- if (billing === undefined || (run.billing === undefined && reported.length === 0))
137
- return existing;
138
- const windows = { ...(existing?.windows ?? {}) };
139
- for (const w of reported)
140
- setWindow(windows, w.type, windowFromRun(w, now));
141
- const account = {
142
- billing,
143
- ...(existing?.subscriptionType !== undefined ? { subscriptionType: existing.subscriptionType } : {}),
144
- observedAt: now,
145
- windows,
146
- };
147
- writeClaudeAccount(root, account);
148
- return account;
44
+ return observeAccount(root, "claude", run, now);
149
45
  }
150
- /** One run's rate-window reading in the file's shape: unix-seconds `resetsAt` becomes epoch ms,
151
- * and absent optional fields stay absent. */
152
- function windowFromRun(w, now) {
153
- // The write choke point heals utilization like readWindow does, so a non-finite value
154
- // handed straight to observeClaudeAccount can never re-enter the file through a save.
155
- const utilization = finiteNumber(w.utilization, undefined);
156
- return {
157
- status: w.status,
158
- ...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt * 1000 } : {}),
159
- ...(utilization !== undefined ? { utilization } : {}),
160
- ...(typeof w.overageStatus === "string" ? { overageStatus: w.overageStatus } : {}),
161
- ...(w.isUsingOverage === true ? { isUsingOverage: true } : {}),
162
- observedAt: now,
163
- };
46
+ /** The subscription account's used-up usage window, or null — see subscription-account.ts's
47
+ * windowUsedUp for the rule. */
48
+ export function claudeWindowUsedUp(account, now, includeOverage = true, reserve) {
49
+ return windowUsedUp(account, now, includeOverage, reserve);
50
+ }
51
+ /** Re-export of the per-backend helpers under their historical claude names for callers that
52
+ * predate the per-backend store (budget gates, team holds, the doctor, the role view, the
53
+ * status poll). */
54
+ export { subscriptionWindowRejected, windowReserved };
55
+ /** The window type as a compact label — subscription-account.ts's windowLabel, under its
56
+ * historical claude name. */
57
+ export function claudeWindowLabel(type) {
58
+ return windowLabel(type);
59
+ }
60
+ /** The fraction of a subscription usage window consumed as a whole-number percent —
61
+ * subscription-account.ts's utilizationPct, under its historical claude name. */
62
+ export function claudeUtilizationPct(utilization) {
63
+ return utilizationPct(utilization);
164
64
  }
165
- /** Seed the account file before the first run (plans/agent-backends.md §7): `ANTHROPIC_API_KEY`
166
- * set means claude prefers the key, so billing is API; otherwise `claude auth status` decides —
167
- * a claude.ai OAuth login is a subscription, any other login is API. An existing file is left
168
- * alone, and a probe that cannot run or is not logged in leaves the file absent
169
- * (readClaudeAccount then reads "no subscription"). Returns the account written, or null. */
65
+ /** The account's windows as a list, most constrained first — subscription-account.ts's
66
+ * windowEntries, under its historical claude name. */
67
+ export function claudeWindowEntries(account) {
68
+ return windowEntries(account);
69
+ }
70
+ /** Seed the claude account file before the first run (plans/agent-backends.md §7):
71
+ * `ANTHROPIC_API_KEY` set means claude prefers the key, so billing is API; otherwise
72
+ * `claude auth status` decides — a claude.ai OAuth login is a subscription, any other login is
73
+ * API. An existing file is left alone, and a probe that cannot run or is not logged in leaves
74
+ * the file absent (readClaudeAccount then reads "no subscription"). Returns the account
75
+ * written, or null. A failed write also returns null and never throws, so the first claude
76
+ * run's observeClaudeAccount fills the file in. */
170
77
  export function seedClaudeAccount(root, config, now = Date.now()) {
171
78
  const existing = readClaudeAccount(root, now);
172
79
  if (existing !== null)
@@ -176,11 +83,16 @@ export function seedClaudeAccount(root, config, now = Date.now()) {
176
83
  return null;
177
84
  const account = {
178
85
  billing: billing.billing,
179
- ...(billing.subscriptionType !== undefined ? { subscriptionType: billing.subscriptionType } : {}),
86
+ ...(billing.plan !== undefined ? { plan: billing.plan } : {}),
180
87
  observedAt: now,
181
88
  windows: {},
182
89
  };
183
- writeClaudeAccount(root, account);
90
+ try {
91
+ writeClaudeAccount(root, account);
92
+ }
93
+ catch {
94
+ return null;
95
+ }
184
96
  return account;
185
97
  }
186
98
  /** The billing the environment implies, or null when it cannot be decided: an API key wins, a
@@ -197,12 +109,12 @@ function seedBilling(config) {
197
109
  return null;
198
110
  const subscription = claudeAuthUsesSubscription(auth);
199
111
  return subscription
200
- ? { billing: "subscription", ...(auth.subscriptionType !== undefined ? { subscriptionType: auth.subscriptionType } : {}) }
112
+ ? { billing: "subscription", ...(auth.subscriptionType !== undefined ? { plan: auth.subscriptionType } : {}) }
201
113
  : { billing: "api" };
202
114
  }
203
115
  /** Parse `claude auth status` stdout, returning null when it is not a JSON object or does not
204
116
  * report a boolean `loggedIn` — the one home of the read, shared by this module's
205
- * probeAuthStatus and doctor's claudeAuthProbe (doctor-model-checks.ts). An optional string
117
+ * probeAuthStatus and doctor's claudeAuthProbe (doctor-claude-checks.ts). An optional string
206
118
  * field reads as absent when the reply names something else. */
207
119
  export function parseClaudeAuthStatus(stdout) {
208
120
  const doc = parseJsonObject(stdout);
@@ -216,7 +128,7 @@ export function parseClaudeAuthStatus(stdout) {
216
128
  }
217
129
  /** True when an auth-status reply names claude.ai subscription billing — a subscription type
218
130
  * (its presence alone signals one) or the explicit claude.ai auth method — rather than API
219
- * billing. Shared by seedBilling below and doctor's checkClaudeAuth (doctor-model-checks.ts),
131
+ * billing. Shared by seedBilling below and doctor's checkClaudeAuth (doctor-claude-checks.ts),
220
132
  * which must not disagree about which seams are subscription-billed. */
221
133
  export function claudeAuthUsesSubscription(auth) {
222
134
  return auth.subscriptionType !== undefined || auth.authMethod === "claude.ai";
@@ -225,7 +137,15 @@ export function claudeAuthUsesSubscription(auth) {
225
137
  * short deadline. Null when the probe could not run or its reply was unreadable. */
226
138
  function probeAuthStatus(config) {
227
139
  const resolved = claudeBackend.resolveBin(config);
228
- const env = claudeEnv(process.env, { quietTimeoutSeconds: 0 });
140
+ const configDir = resolvedBackendConfig(config, "claude").configDir;
141
+ const env = claudeEnv(process.env, {
142
+ quietTimeoutSeconds: 0,
143
+ // The seed probes auth under the same env a real run gets, so a team config dir's login
144
+ // is what it reports (plans/notes-command.md "Keeping funes out of team runs").
145
+ ...(configDir !== undefined
146
+ ? { configDir: claudeConfigDir(process.env, os.homedir(), configDir) }
147
+ : {}),
148
+ });
229
149
  try {
230
150
  const result = spawnSync(resolved.bin, ["auth", "status"], {
231
151
  env,