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
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # tumwater
2
2
 
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
+
3
11
  tumwater is an opinionated autonomous development team, built on
4
12
  [pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a team of
5
13
  roles (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
@@ -27,13 +35,16 @@ claude backend: it drives the real CLI through a throwaway repo (a few cents of
27
35
  spend on a subscription) and is not part of `npm test`, which stays green without claude. `npm run
28
36
  smoke:opencode` is the same live check for the opencode backend, driving the real `opencode` CLI
29
37
  against its free DeepSeek tier; it needs an OpenCode V2 install (`brew install opencode`) and is
30
- also outside `npm test`.
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`.
31
41
 
32
42
  ## Status
33
43
 
34
44
  <!-- tumwater:status:start -->
35
- **v0.1.4**: working tumwater. The director and 14 of the 15 roles are enabled by default; `selfcheck`,
36
- which files tumwater bugs from tumwater's own event log, is opt-in (`roles.selfcheck.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`).
37
48
 
38
49
  Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
39
50
  [QUESTIONS.md](QUESTIONS.md) (open questions).
@@ -75,20 +86,22 @@ same list, and `tumwater completion bash|zsh|fish` prints shell completion. Sett
75
86
  `tumwater.json` and edits apply live while the team runs; see [docs/config.md](docs/config.md).
76
87
 
77
88
  **Backends:** any OpenAI-compatible model pi can reach works, chosen by `model` in `tumwater.json`
78
- with a `fallback` that a failing role switches to. `backend: "claude"` runs bare model ids on Claude
79
- Code instead, and a model id that starts with `opencode/` runs on the OpenCode CLI — its free
80
- DeepSeek tier needs no login or API key. Requirements, the fallback, free-ness, and usage-window
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
81
93
  behavior, and how subscription runs are billed (as estimated spend, outside the daily cap) are in
82
94
  [docs/backends.md](docs/backends.md).
83
95
 
84
- **Mixing providers:** a bare model id that starts with `claude-` runs on the claude CLI even
85
- under the default `backend: "pi"`, so one config can mix a Claude strong tier with pi providers:
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:
86
99
 
87
100
  ```json
88
101
  {
89
102
  "model": {
90
103
  "default": "huggingface/deepseek-ai/DeepSeek-V4.1-Flash:deepinfra",
91
- "strong": "claude-opus-5-5"
104
+ "strong": { "backend": "claude", "model": "claude-opus-5-5" }
92
105
  },
93
106
  "fallback": "omlx/Qwen3.8-27B-MLX-oQ4e-mtp"
94
107
  }
@@ -97,10 +110,11 @@ under the default `backend: "pi"`, so one config can mix a Claude strong tier wi
97
110
  The default tier runs on pi through Hugging Face, the strong model jobs (the director, the plan
98
111
  role, the reviewer, and the conflict resolver) run on the claude CLI's included usage, and the fallback runs on pi through
99
112
  oMLX — all concurrently, under each provider's cap (set it with `maxConcurrentPerProvider`;
100
- `tumwater status` shows how many runs each key has going out of its cap). A model id that starts
101
- with `opencode/` routes to the OpenCode CLI under any backend, so its free tier can sit beside pi
102
- and claude in one config. Prefix `anthropic/` (`anthropic/claude-…`)
103
- to keep a Claude model on pi instead (billed per token). See [plans/agent-backends.md §3](plans/agent-backends.md).
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).
104
118
 
105
119
  For how the roles, review gate, scheduling, and updates work, see
106
120
  [docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "cff6961c92b598be9b21cc49a6faa66a16f11a8c",
3
- "builtAt": 1792409331797,
2
+ "sha": "d36bf79c535e10fe228f058ecc1ca839f3af61b1",
3
+ "builtAt": 1792789212458,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -1,13 +1,12 @@
1
- // vocabulary: plain
2
1
  /** Appending a backend's configured passthrough args to an already-built argv. Shared by the
3
- * claude and opencode argv builders (claude-args.ts and opencode-args.ts), whose passthrough
4
- * handling is otherwise identical, so the two backends reject a reserved flag by the same
5
- * rule and the same message shape. A passthrough arg equal to a flag the harness itself sets
6
- * is appended AFTER the harness's own copy, and both backends' parsers are last-wins, so it
7
- * would silently override the model, the session choice, or the isolation flags — each such
8
- * arg is rejected here with the offending arg named. `moduleName` names the blocking backend
9
- * in the error (e.g. `claudeArgs` or `opencodeArgs`); `reserved` is that backend's
10
- * HARNESS_*_FLAGS set. */
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. */
11
10
  export function appendPassthroughArgs(args, passthrough, reserved, moduleName) {
12
11
  for (const arg of passthrough ?? []) {
13
12
  // Match the flag itself even when it carries its value as `--flag=value`.
@@ -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,8 +28,8 @@ 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, configDir) {
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
34
  return backend.resumable(dir, opts, configDir);
33
35
  return hasResumableSession(dir);
@@ -1,46 +1,53 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * The per-backend capabilities that the config, prompt, watchdog, progress and doctor layers
4
3
  * read (plans/opencode-backend.md "Groundwork: the claude-only branches become backend
5
4
  * traits"). Pure — imports nothing from the runtime registry (agent-backend.ts) — so those
6
5
  * layers can read a backend's traits without pulling the registry in. That is the same reason
7
- * `AGENT_DEFAULT_MODELS` lives in src/config/model-selector.ts.
6
+ * `BACKEND_DEFAULT_MODELS` lives in src/config/model-selector.ts.
8
7
  *
9
8
  * An id with no entry reads as pi, because src/agent/run-agent.ts routes an unregistered id to
10
9
  * the pi backend.
11
10
  */
12
11
  import { HARNESS_CLAUDE_FLAGS } from "./claude/claude-args.js";
13
12
  import { HARNESS_OPENCODE_FLAGS } from "./opencode/opencode-args.js";
13
+ import { HARNESS_CODEX_FLAGS } from "./codex/codex-args.js";
14
14
  import { HARNESS_PI_FLAGS } from "../config/config-schema.js";
15
15
  /** Whether the backend streams tool output. pi streams content-bearing tool updates, so its
16
- * calls are classified per command; claude and opencode send none, so silence carries no hang
17
- * signal and every call is judged by the buffered threshold (plans/agent-backends.md
18
- * "Watchdogs and progress"). Only claude and opencode are false; an unknown id reads as pi. */
19
- export function streamsToolOutput(agent) {
20
- // Only claude and opencode stream no tool output; every other id (including an unregistered
21
- // one) reads as pi, which does stream.
22
- return agent !== "claude" && agent !== "opencode";
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";
23
24
  }
24
25
  /** Whether the backend's model jobs resolve through pi's models.json. True for pi only (and an
25
- * unknown id, which reads as pi); a claude or opencode job reads no pi pricing and needs no
26
- * definitions file. */
27
- export function usesPiModels(agent) {
28
- // Only claude and opencode read no pi models.json; every other id (including an unregistered
29
- // one) reads as pi, which does.
30
- return agent !== "claude" && agent !== "opencode";
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";
31
32
  }
32
33
  /** The reason string config validation prints for a reserved `backends.<id>.args` flag, or
33
- * undefined when the flag is not reserved for that backend. Wraps claude's HARNESS_CLAUDE_FLAGS
34
- * and pi's HARNESS_PI_FLAGS, keeping each backend's reason wording. The flag is the bare name —
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 —
35
37
  * checkReservedArgs splits `--mode=x` on `=` before calling. */
36
- export function reservedPassthroughFlag(agent, flag) {
37
- switch (agent) {
38
+ export function reservedPassthroughFlag(backend, flag) {
39
+ switch (backend) {
38
40
  case "pi":
39
41
  return HARNESS_PI_FLAGS.get(flag);
40
42
  case "claude":
41
43
  return HARNESS_CLAUDE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
42
44
  case "opencode":
43
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;
44
51
  default:
45
52
  // An unknown backend has no known harness argv to clash with, so nothing is reserved.
46
53
  return undefined;
@@ -48,13 +55,23 @@ export function reservedPassthroughFlag(agent, flag) {
48
55
  }
49
56
  /** A seam's model as one readable token: `<provider>/<model>` for a pi seam (and any unknown
50
57
  * id, which reads as pi), the bare model for claude — or "claude backend" when there is none —
51
- * and `opencode/<model>` for opencode, whose ids all read under the opencode namespace. The
52
- * one home of the label, shared by the doctor report and the image-input surface strings so
53
- * they cannot drift. */
54
- export function modelLabel(agent, provider, model) {
55
- if (agent === "claude")
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")
56
63
  return model ?? "claude backend";
57
- if (agent === "opencode")
64
+ if (backend === "opencode")
58
65
  return `opencode/${model ?? "?"}`;
66
+ if (backend === "codex")
67
+ return model ?? "codex default";
59
68
  return `${provider ?? "?"}/${model ?? "?"}`;
60
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,217 +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
17
  import os from "node:os";
13
- import { resolvedAgentConfig } from "../../config/config-views.js";
14
- import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
15
- import { finiteNumber, isJsonObject, parseJsonObject } from "../../files/json-object.js";
16
- import { claudeAccountPath } from "../../paths.js";
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";
17
21
  import { claudeBackend, claudeConfigDir } from "./claude-backend.js";
18
22
  import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
19
- /** True when a failed run ended on a subscription usage window the account reported as
20
- * `rejected` without overage — the signal the team hold throttles on (plans/agent-backends.md
21
- * §8). Such a run is a transient of the world, not a failure of the session: its session and
22
- * worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
23
- * partial work or reset. API-billed claude and overage are budget/fallback matters, not a
24
- * window hold, so they read false; a pi 429 reports no window and also reads false. */
25
- export function subscriptionWindowRejected(run) {
26
- return (run.transientRateLimit &&
27
- run.billing === "subscription" &&
28
- (run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
29
- }
30
- /** The subscription account's used-up usage window, or null: a window reading `rejected` with no
31
- * overage, or (with `includeOverage`, the default) a window whose current reading shows
32
- * `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
33
- * last. With a `reserve` (config `claudeWindowReserve`), a window whose utilization has reached
34
- * its type's fraction counts too, and comes back with `reserve: true`; a rejected or overage
35
- * window outranks a reserve one, so the flag is set only when no window is used up for real.
36
- * The budget gate's window move (pollBudgetGate) reads the default; the team hold
37
- * (claudeWindowObservation) passes `includeOverage: false`, because overage never holds a seam.
38
- * Only subscription billing produces one; a missing account reads null. */
39
- export function claudeWindowUsedUp(account, now, includeOverage = true, reserve) {
40
- if (account?.billing !== "subscription")
41
- return null;
42
- let furthest = null;
43
- let reserved = null;
44
- for (const [type, w] of Object.entries(account.windows)) {
45
- if (typeof w.resetsAt !== "number" || w.resetsAt <= now)
46
- continue;
47
- const used = w.isUsingOverage === true ? includeOverage : w.status === "rejected";
48
- if (used) {
49
- if (furthest === null || w.resetsAt > furthest.resetsAt)
50
- furthest = { type, resetsAt: w.resetsAt };
51
- continue;
52
- }
53
- if (windowReserved(w, reserve?.[type]) && (reserved === null || w.resetsAt > reserved.resetsAt))
54
- reserved = { type, resetsAt: w.resetsAt, reserve: true };
55
- }
56
- return furthest ?? reserved;
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. */
27
+ export function readClaudeAccount(root, now = Date.now()) {
28
+ return readAccount(root, "claude", now);
29
+ }
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. */
36
+ export function writeClaudeAccount(root, account) {
37
+ writeAccount(root, "claude", account);
57
38
  }
58
- /** Has this window's utilization reached its reserve fraction? False without a fraction or a
59
- * reading. */
60
- export function windowReserved(w, fraction) {
61
- return fraction !== undefined && typeof w.utilization === "number" && w.utilization >= fraction;
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). */
43
+ export function observeClaudeAccount(root, run, now = Date.now()) {
44
+ return observeAccount(root, "claude", run, now);
62
45
  }
63
- /** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
64
- * every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
65
- const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
66
- /** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
67
- * type falls back to its words with underscores as spaces. The one home of the window spelling
68
- * the status badge and the role view share. */
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. */
69
57
  export function claudeWindowLabel(type) {
70
- const m = /^(\w+)_(hour|day|week)$/.exec(type);
71
- if (!m)
72
- return type.replace(/_/g, " ");
73
- const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
74
- const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
75
- const word = m[1] ?? "";
76
- return `${words[word] ?? word}${unit}`;
58
+ return windowLabel(type);
77
59
  }
78
- /** The fraction of a subscription usage window consumed (0–1) as a whole-number percent: a
79
- * `0.64` utilization reads `64`. The one home of the fraction→percent conversion, shared by
80
- * the status badge and the doctor's hold reason so the two render a window's fill
81
- * identically (the browser dashboard's own window line is embedded client-side and keeps its
82
- * inline copy). */
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. */
83
62
  export function claudeUtilizationPct(utilization) {
84
- return Math.round(utilization * 100);
63
+ return utilizationPct(utilization);
85
64
  }
86
- /** The account's windows as a list, most constrained first: by status severity, then the
87
- * reported utilization (a missing reading sorts below any number), then the earlier reset, then
88
- * the type name — so a tie always orders the same way. Empty when the account reported none. */
65
+ /** The account's windows as a list, most constrained first — subscription-account.ts's
66
+ * windowEntries, under its historical claude name. */
89
67
  export function claudeWindowEntries(account) {
90
- return Object.entries(account.windows)
91
- .map(([type, w]) => ({ type, ...w }))
92
- .sort((a, b) => {
93
- const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
94
- if (rank !== 0)
95
- return rank;
96
- const util = (b.utilization ?? -1) - (a.utilization ?? -1);
97
- if (util !== 0)
98
- return util;
99
- const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
100
- if (reset !== 0)
101
- return reset;
102
- return a.type.localeCompare(b.type);
103
- });
104
- }
105
- /** Read the persisted account state, or null when it is absent or unreadable. A file without a
106
- * usable top-level `observedAt` reads as absent (the plan's torn-file rule), and a window whose
107
- * `resetsAt` is at or before `now` reads back with `status: "reset"` — the reader, not every
108
- * consumer, decides that the window no longer limits. */
109
- export function readClaudeAccount(root, now = Date.now()) {
110
- const doc = readJsonFile(claudeAccountPath(root));
111
- if (doc === null)
112
- return null;
113
- const billing = doc.billing === "subscription" || doc.billing === "api" ? (doc.billing) : undefined;
114
- const observedAt = finiteNumber(doc.observedAt, Number.NaN);
115
- if (billing === undefined || !Number.isFinite(observedAt))
116
- return null;
117
- const windows = {};
118
- if (isJsonObject(doc.windows)) {
119
- for (const [type, value] of Object.entries(doc.windows)) {
120
- const window = readWindow(value, observedAt, now);
121
- if (window !== undefined)
122
- setWindow(windows, type, window);
123
- }
124
- }
125
- return {
126
- billing,
127
- ...(typeof doc.subscriptionType === "string" ? { subscriptionType: doc.subscriptionType } : {}),
128
- observedAt,
129
- windows,
130
- };
131
- }
132
- /** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
133
- * missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
134
- * status to `reset` and drops the overage flags. */
135
- function readWindow(value, fileObservedAt, now) {
136
- if (!isJsonObject(value) || typeof value.status !== "string")
137
- return undefined;
138
- const observedAt = finiteNumber(value.observedAt, fileObservedAt);
139
- const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
140
- const resets = Number.isFinite(resetsAt);
141
- // utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
142
- // non-finite fraction would render as `Infinity%` in the badges and poison the
143
- // claudeWindowEntries comparator (Infinity - Infinity is NaN).
144
- const utilization = finiteNumber(value.utilization, undefined);
145
- const reset = resets && resetsAt <= now;
146
- const window = {
147
- status: reset ? "reset" : value.status,
148
- ...(resets ? { resetsAt } : {}),
149
- ...(utilization !== undefined ? { utilization } : {}),
150
- // a reset window is no longer in overage: dropping both keeps every reader in agreement
151
- ...(!reset && typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
152
- ...(!reset && value.isUsingOverage === true ? { isUsingOverage: true } : {}),
153
- observedAt,
154
- };
155
- return window;
156
- }
157
- /** Store one window under its `rateLimitType`. A plain `windows[type] = window` loses a type of
158
- * `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's prototype
159
- * instead of adding an own entry, so the window vanishes from every `Object.entries`/`values`
160
- * reader (the display, the hold, and accountUsesOverage). Defining the own property keeps even
161
- * that key — the same inherited-key guard claude-stream's TOOL_NAMES lookup uses. */
162
- function setWindow(windows, type, window) {
163
- Object.defineProperty(windows, type, { value: window, enumerable: true, writable: true, configurable: true });
164
- }
165
- /** Overwrite the account file with `account`, creating the state directory. The orchestrator's
166
- * one writer; a failure to write is the caller's to handle (best-effort at the tick boundary).
167
- * Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write leaves the
168
- * previous file whole, not a torn one the reader heals to "no subscription" — which would
169
- * silently drop the billing answer and the window hold until the next run re-observed them. */
170
- export function writeClaudeAccount(root, account) {
171
- writeJsonAtomic(claudeAccountPath(root), account, true);
172
- }
173
- /** Fold one run's billing and rate-window readings into the account file: the run's `billing`
174
- * overwrites the stored answer, and each reported window replaces the stored reading for its
175
- * `type`, stamped with `observedAt`. A run that reported neither billing nor windows leaves the
176
- * file untouched (and returns whatever was already there). */
177
- export function observeClaudeAccount(root, run, now = Date.now()) {
178
- const existing = readClaudeAccount(root, now);
179
- const billing = run.billing ?? existing?.billing;
180
- const reported = run.rateWindows ?? [];
181
- if (billing === undefined || (run.billing === undefined && reported.length === 0))
182
- return existing;
183
- const windows = { ...(existing?.windows ?? {}) };
184
- for (const w of reported)
185
- setWindow(windows, w.type, windowFromRun(w, now));
186
- const account = {
187
- billing,
188
- ...(existing?.subscriptionType !== undefined ? { subscriptionType: existing.subscriptionType } : {}),
189
- observedAt: now,
190
- windows,
191
- };
192
- writeClaudeAccount(root, account);
193
- return account;
194
- }
195
- /** One run's rate-window reading in the file's shape: unix-seconds `resetsAt` becomes epoch ms,
196
- * and absent optional fields stay absent. */
197
- function windowFromRun(w, now) {
198
- // The write choke point heals utilization like readWindow does, so a non-finite value
199
- // handed straight to observeClaudeAccount can never re-enter the file through a save.
200
- const utilization = finiteNumber(w.utilization, undefined);
201
- return {
202
- status: w.status,
203
- ...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt * 1000 } : {}),
204
- ...(utilization !== undefined ? { utilization } : {}),
205
- ...(typeof w.overageStatus === "string" ? { overageStatus: w.overageStatus } : {}),
206
- ...(w.isUsingOverage === true ? { isUsingOverage: true } : {}),
207
- observedAt: now,
208
- };
209
- }
210
- /** Seed the account file before the first run (plans/agent-backends.md §7): `ANTHROPIC_API_KEY`
211
- * set means claude prefers the key, so billing is API; otherwise `claude auth status` decides —
212
- * a claude.ai OAuth login is a subscription, any other login is API. An existing file is left
213
- * alone, and a probe that cannot run or is not logged in leaves the file absent
214
- * (readClaudeAccount then reads "no subscription"). Returns the account written, or null. */
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. */
215
77
  export function seedClaudeAccount(root, config, now = Date.now()) {
216
78
  const existing = readClaudeAccount(root, now);
217
79
  if (existing !== null)
@@ -221,11 +83,16 @@ export function seedClaudeAccount(root, config, now = Date.now()) {
221
83
  return null;
222
84
  const account = {
223
85
  billing: billing.billing,
224
- ...(billing.subscriptionType !== undefined ? { subscriptionType: billing.subscriptionType } : {}),
86
+ ...(billing.plan !== undefined ? { plan: billing.plan } : {}),
225
87
  observedAt: now,
226
88
  windows: {},
227
89
  };
228
- writeClaudeAccount(root, account);
90
+ try {
91
+ writeClaudeAccount(root, account);
92
+ }
93
+ catch {
94
+ return null;
95
+ }
229
96
  return account;
230
97
  }
231
98
  /** The billing the environment implies, or null when it cannot be decided: an API key wins, a
@@ -242,12 +109,12 @@ function seedBilling(config) {
242
109
  return null;
243
110
  const subscription = claudeAuthUsesSubscription(auth);
244
111
  return subscription
245
- ? { billing: "subscription", ...(auth.subscriptionType !== undefined ? { subscriptionType: auth.subscriptionType } : {}) }
112
+ ? { billing: "subscription", ...(auth.subscriptionType !== undefined ? { plan: auth.subscriptionType } : {}) }
246
113
  : { billing: "api" };
247
114
  }
248
115
  /** Parse `claude auth status` stdout, returning null when it is not a JSON object or does not
249
116
  * report a boolean `loggedIn` — the one home of the read, shared by this module's
250
- * 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
251
118
  * field reads as absent when the reply names something else. */
252
119
  export function parseClaudeAuthStatus(stdout) {
253
120
  const doc = parseJsonObject(stdout);
@@ -261,7 +128,7 @@ export function parseClaudeAuthStatus(stdout) {
261
128
  }
262
129
  /** True when an auth-status reply names claude.ai subscription billing — a subscription type
263
130
  * (its presence alone signals one) or the explicit claude.ai auth method — rather than API
264
- * 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),
265
132
  * which must not disagree about which seams are subscription-billed. */
266
133
  export function claudeAuthUsesSubscription(auth) {
267
134
  return auth.subscriptionType !== undefined || auth.authMethod === "claude.ai";
@@ -270,7 +137,7 @@ export function claudeAuthUsesSubscription(auth) {
270
137
  * short deadline. Null when the probe could not run or its reply was unreadable. */
271
138
  function probeAuthStatus(config) {
272
139
  const resolved = claudeBackend.resolveBin(config);
273
- const configDir = resolvedAgentConfig(config, "claude").configDir;
140
+ const configDir = resolvedBackendConfig(config, "claude").configDir;
274
141
  const env = claudeEnv(process.env, {
275
142
  quietTimeoutSeconds: 0,
276
143
  // The seed probes auth under the same env a real run gets, so a team config dir's login