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
@@ -0,0 +1,214 @@
1
+ /** The settlement half of a backend agent run (the split of src/agent/run-agent.ts along the
2
+ * same seam the runner-run → runner-outcome split used): everything that happens after the
3
+ * run's live feed — the spawn-`error` result, the `exit` orphan sweep, and the `close`-time
4
+ * flush/reconcile/classify that builds the `PiRunResult`. run-agent.ts keeps the lifecycle
5
+ * (pre-spawn setup, `prepare`, spawn, decoders, stream pipeline, watchdogs, the feed) and
6
+ * delegates its `error`/`exit`/`close` listeners here, so the streaming half stays readable.
7
+ *
8
+ * Each handler takes the run's live state (`RunSettleState`, defined in run-agent.ts and
9
+ * imported here only as a type) and reads what it closes over at the moment its event fires —
10
+ * the live flags it needs are getters on that state. There is no runtime import cycle:
11
+ * run-agent imports these functions at runtime, and this module imports only run-agent's type.
12
+ */
13
+ import { backendBinConfigKey, backendBinSourceLabel } from "../pi/pi-bin.js";
14
+ import { terminateChild } from "../process/process.js";
15
+ import { leftoversSweptMessage, sweepRunMarker } from "../process/run-marker.js";
16
+ /** Prefix of PiRunResult.errorMessage when the agent process never started (no session file
17
+ * is created for such a run). The resolved binary follows it, so a configured agentBin
18
+ * reads "failed to spawn <bin>" rather than blaming an ambient "pi". */
19
+ const SPAWN_ERROR_PREFIX = "failed to spawn";
20
+ /** The spawn-error message for a resolved binary: the default source keeps today's text
21
+ * ("failed to spawn pi: …"); a configured source names the binary and where it came from —
22
+ * the backend's own env var and config key (backendBinConfigKey) — so a wrong
23
+ * `backends.claude.bin` does not read as pi's legacy `agentBin`. */
24
+ function spawnErrorMessage(resolved, errMessage, backend) {
25
+ const source = resolved.source === "default"
26
+ ? ""
27
+ : ` (resolved from ${backendBinSourceLabel(resolved.source, backend.binEnv, backendBinConfigKey(backend.id))})`;
28
+ return `${SPAWN_ERROR_PREFIX} ${resolved.bin}${source}: ${errMessage}`;
29
+ }
30
+ /** The failed result of a run that never started: all counters zero, all flags false. */
31
+ export function setupFailureResult(agent, message) {
32
+ return {
33
+ ok: false,
34
+ finalText: "",
35
+ nothingToDo: false,
36
+ refused: false,
37
+ agent,
38
+ outputTokens: 0,
39
+ promptTokens: 0,
40
+ cacheReadTokens: 0,
41
+ preEditPromptTokens: 0,
42
+ peakContextTokens: 0,
43
+ turns: 0,
44
+ costUsd: 0,
45
+ errorMessage: message,
46
+ timedOut: false,
47
+ timedOutProgressing: false,
48
+ aborted: false,
49
+ quietKilled: false,
50
+ contextExceeded: false,
51
+ transientServerTimeout: false,
52
+ transientPiCrash: false,
53
+ transientRateLimit: false,
54
+ transientBackend: false,
55
+ configError: false,
56
+ finalMessageContentless: false,
57
+ producedAssistantContent: false,
58
+ compacted: false,
59
+ };
60
+ }
61
+ /** Every PiRunResult is built here from what the parser has seen plus how the run ended, so
62
+ * adding a field to PiRunResult touches this single place. The spawn-error path passes only
63
+ * an errorMessage override: at that point no output ever arrived, so the parser holds exactly
64
+ * its initial values — which is precisely what a failed spawn reports. */
65
+ function resultFromParser(state, overrides) {
66
+ const parser = state.parser;
67
+ return {
68
+ ok: false,
69
+ finalText: parser.finalText,
70
+ nothingToDo: parser.declaredNothingToDo,
71
+ refused: parser.refused,
72
+ refusedReason: parser.refusedReason || undefined,
73
+ verdictText: parser.verdictText || undefined,
74
+ agent: state.backend.id,
75
+ outputTokens: parser.outputTokens,
76
+ promptTokens: parser.promptTokens,
77
+ cacheReadTokens: parser.cacheReadTokens,
78
+ preEditPromptTokens: parser.preEditPromptTokens,
79
+ firstEditTurn: parser.firstEditTurn,
80
+ peakContextTokens: parser.peakContextTokens,
81
+ turns: parser.turns,
82
+ costUsd: parser.costUsd,
83
+ stopReason: parser.stopReason,
84
+ errorMessage: undefined,
85
+ timedOut: false,
86
+ timedOutProgressing: false,
87
+ quietKilled: false,
88
+ aborted: state.aborted(),
89
+ contextExceeded: parser.contextExceeded,
90
+ transientServerTimeout: parser.transientServerTimeout,
91
+ transientRateLimit: parser.transientRateLimit,
92
+ transientBackend: parser.transientBackend,
93
+ backendKind: parser.backendFailureKind,
94
+ configError: parser.configError,
95
+ retryAfterSeconds: parser.retryAfterSeconds,
96
+ transientPiCrash: false,
97
+ fallbackClone: state.fallbackClone(),
98
+ finalMessageContentless: parser.finalMessageContentless,
99
+ producedAssistantContent: parser.producedAssistantContent,
100
+ compacted: parser.compacted,
101
+ budgetCapped: parser.budgetCapped,
102
+ resumeMissing: parser.resumeMissing,
103
+ billing: parser.billing,
104
+ rateWindows: parser.rateWindows.length > 0 ? parser.rateWindows : undefined,
105
+ usedOverage: parser.usedOverage,
106
+ ...overrides,
107
+ };
108
+ }
109
+ /** The run's `error` event: the process never spawned. Only the spawn-error cause is set; no
110
+ * output ever arrived, so the parser still holds its initial values (what a failed spawn
111
+ * reports), and `finish` settles the run so the `close` event no-ops. */
112
+ export function settleError(state, err) {
113
+ state.finish(resultFromParser(state, {
114
+ errorMessage: spawnErrorMessage(state.resolved, err.message, state.backend),
115
+ }));
116
+ }
117
+ /** The run's `exit` event: sweep whatever the run (or a tool call it started) left running,
118
+ * however the run ended, a normal exit included, in two passes:
119
+ *
120
+ * 1. The group pi led: pi exits before its orphans — a tool call's `(server &)` or
121
+ * `cd … && server &` is reparented to PID 1 but stays in pi's group — and the harness
122
+ * cannot rely on the model's own cleanup. On 'exit', not 'close': pi has just been reaped,
123
+ * so the pgid still names only its group (never reissued while a member lives), and an
124
+ * orphan holding pi's stdio open dies now instead of holding 'close' — and the run — open.
125
+ * Synchronous, so it adds nothing to the run's resolution; on the kill paths it re-sends a
126
+ * SIGTERM the group already had.
127
+ *
128
+ * 2. The marker sweep: pi 0.87.1's bash tool starts every command detached, in its own
129
+ * process group, so whatever a tool call backgrounds sits in a group whose leader is not
130
+ * pi and pass 1 never reaches it. Every process the run started carries the run's
131
+ * TUMWATER_RUN mark in its environment, so the same-user scan finds them wherever they
132
+ * were reparented. Fire-and-forget: one process-table scan, never awaited — the run
133
+ * resolves on pi's output, and the sweep's own SIGKILL escalation (10 s) is unref'd like
134
+ * signalTree's.
135
+ */
136
+ export function settleExit(state) {
137
+ terminateChild(state.child);
138
+ void sweepRunMarker(state.runMarker)
139
+ .then((n) => {
140
+ const msg = leftoversSweptMessage(state.kind, n, state.wasKilled());
141
+ if (msg)
142
+ state.onLeftoversSwept?.(msg);
143
+ })
144
+ .catch(() => { });
145
+ }
146
+ /** The run's `close` event: the process died and all its streams closed. Flush what the
147
+ * decoders held back, reconcile the backend's post-run work, classify the exit cause, and
148
+ * settle the run's result. */
149
+ export function settleClose(state, code) {
150
+ const { backend, runSpec, invocation, translator, onLine } = state;
151
+ // Flush any bytes the decoder held back at stream end and any unterminated final line so the
152
+ // tail is not lost, then let the translator flush its own pending canonical line(s) — a
153
+ // batching backend's unclosed last assistant message, which a killed run never closes with a
154
+ // later event.
155
+ state.feedCanonical(state.decoder.end(), true);
156
+ for (const canonical of translator.finalize?.() ?? [])
157
+ state.parser.feed(canonical + "\n", onLine);
158
+ // A blind image read on an authoring run deliberately killed it, so its exit is a kill like
159
+ // the watchdogs' — the half-done work is discarded.
160
+ const killed = state.wasKilled();
161
+ // The backend's post-run reconciliation (opencode's authoritative session export), only when
162
+ // the run is not already settled (a failed spawn settles in the 'error' handler before
163
+ // 'close') and was not killed — a killed run's session is half-done and no export should
164
+ // spend its 10 s on it. Its lines are written and parsed like streamed ones, so they reach
165
+ // the raw log and the parser's authoritative fold (run_cost/run_usage).
166
+ if (!state.settled() && !killed) {
167
+ for (const canonical of backend.afterRun?.(runSpec, invocation, translator) ?? []) {
168
+ state.parser.feed(canonical + "\n", onLine);
169
+ }
170
+ }
171
+ const failed = killed ||
172
+ state.parser.stopReason === "error" ||
173
+ (code !== 0 && !state.parser.finalText.trim());
174
+ // The backend classifies the exit's stderr (pi: the transient JSON-parse crash, a
175
+ // provider-wide backend failure, a permanent config error). The runner merges that with the
176
+ // parser's own event-derived flags — one exit has one cause.
177
+ const stderr = state.stderr();
178
+ const cls = backend.classifyExit({ code, stderr, killed });
179
+ const stderrBackend = cls.backendKind;
180
+ const configError = !killed &&
181
+ cls.transientPiCrash !== true &&
182
+ stderrBackend === undefined &&
183
+ (state.parser.configError || cls.configError === true);
184
+ // The blind-read failure names both the file and the model (plans/image-input.md
185
+ // "Failing the run"): take it over any generic exit text so lastError is actionable.
186
+ const blindRead = state.blindRead();
187
+ const errorMessage = blindRead !== undefined
188
+ ? `read ${blindRead}, but ${state.config.model ?? "the model"} cannot read images`
189
+ : state.aborted()
190
+ ? "stopped by tumwater shutdown"
191
+ : state.wd.quietKilled
192
+ ? `killed as hung: no agent progress for over ${state.config.quietTimeoutSeconds}s`
193
+ : state.wd.timedOut
194
+ ? state.wd.timedOutProgressing
195
+ ? `timed out after ${state.wd.timeoutBudgetMs / 1000}s while still making progress — session and worktree edits preserved for resume`
196
+ : `timed out after ${state.wd.timeoutBudgetMs / 1000}s`
197
+ : (state.parser.errorMessage ?? (failed ? stderr.trim().slice(-500) || `the agent exited ${code}` : undefined));
198
+ state.finish(resultFromParser(state, {
199
+ ok: !failed,
200
+ transientPiCrash: cls.transientPiCrash === true,
201
+ transientBackend: state.parser.transientBackend || cls.transientBackend === true,
202
+ backendKind: state.parser.backendFailureKind ?? stderrBackend,
203
+ configError,
204
+ // Kept distinct on purpose: a hung tool call leaves its session and worktree edits
205
+ // intact, so the runner resumes them (quiet_killed) instead of discarding them as an
206
+ // unfulfilled timeout does. A deadline that fired on a run still making progress gets
207
+ // the same treatment (BUGS.md 2026-09-29).
208
+ timedOut: state.wd.timedOut,
209
+ timedOutProgressing: state.wd.timedOut && state.wd.timedOutProgressing,
210
+ quietKilled: state.wd.quietKilled,
211
+ imageBlind: state.parser.imageBlind,
212
+ errorMessage,
213
+ }));
214
+ }
@@ -11,8 +11,9 @@
11
11
  import { spawnSync } from "node:child_process";
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
- import { ensureParentDir, readTextOrNull, statOrNull, terminateTornTail, walkFiles, writeTextAtomic } from "../files/files.js";
14
+ import { daysAgoMs, dirEntryNames, ensureParentDir, readTextOrNull, statOrNull, terminateTornTail, walkFiles, writeTextAtomic } from "../files/files.js";
15
15
  import { isJsonObject } from "../files/json-object.js";
16
+ import { findRollout } from "./codex/codex-rollout.js";
16
17
  /** The ledger file name every session dir holds. Deliberately a `.jsonl` name so it sits beside
17
18
  * the backend's own session files; readers that count session files must exclude it. */
18
19
  const SESSION_LEDGER_FILE = "sessions.jsonl";
@@ -21,7 +22,7 @@ export function sessionLedgerPath(dir) {
21
22
  return path.join(dir, SESSION_LEDGER_FILE);
22
23
  }
23
24
  /** Append one run's record to `dir`'s ledger, before its spawn. Best-effort: an unwritable
24
- * session dir or a full disk must degrade to "no resumable session" on a later tick, never
25
+ * session dir or a full disk must degrade to "no resumable session" on a later run, never
25
26
  * fail the run it was trying to describe. A crash or power loss mid-append leaves the previous
26
27
  * record without its newline; terminating that torn tail first keeps the next record on its
27
28
  * own line instead of gluing the two into one unparseable line that loses both runs' records
@@ -42,14 +43,8 @@ export function recordSession(dir, record) {
42
43
  * unregistered backend. The ledger file shares the dir and is a `.jsonl` too, so it is
43
44
  * excluded — it is the harness's record of runs, not a session. */
44
45
  export function hasResumableSession(dir) {
45
- try {
46
- return fs
47
- .readdirSync(dir)
48
- .some((f) => f.endsWith(".jsonl") && f !== SESSION_LEDGER_FILE);
49
- }
50
- catch {
51
- return false; // Missing directory — nothing to resume.
52
- }
46
+ // A missing directory reads empty — nothing to resume.
47
+ return dirEntryNames(dir).some((f) => f.endsWith(".jsonl") && f !== SESSION_LEDGER_FILE);
53
48
  }
54
49
  /** Every valid record in `dir`'s ledger, oldest first. A missing ledger reads as no records;
55
50
  * a torn, malformed, or shape-invalid line is skipped rather than poisoning the rest (a crash
@@ -136,9 +131,11 @@ export function sessionResumable(dir, opts, sessionFileExists) {
136
131
  /** Drop ledger lines older than `days` from every session dir under `sessionsRoot`. The ledger
137
132
  * file's mtime keeps it young while its role runs, so pruneOldFiles never ages it out; trimming
138
133
  * its lines is what actually bounds it, under the same window the session files prune by.
139
- * Returns the number of lines removed; a missing root or unreadable dir reads as nothing to do. */
134
+ * Returns the number of lines removed; a missing root or unreadable dir reads as nothing to do.
135
+ * A ledger that cannot be rewritten (a full disk, a read-only dir) is left as it was and not
136
+ * counted, because this pass runs in the orchestrator's poll loop, where a throw ends the team. */
140
137
  export function pruneSessionLedgers(sessionsRoot, days) {
141
- const cutoff = Date.now() - days * 24 * 3600 * 1000;
138
+ const cutoff = daysAgoMs(days);
142
139
  let pruned = 0;
143
140
  walkFiles(sessionsRoot, (file, entry) => {
144
141
  if (!entry.isFile() || entry.name !== SESSION_LEDGER_FILE)
@@ -148,22 +145,29 @@ export function pruneSessionLedgers(sessionsRoot, days) {
148
145
  return;
149
146
  const lines = text.split("\n").filter((line) => line !== "");
150
147
  const kept = [];
148
+ let dropped = 0;
151
149
  for (const line of lines) {
152
150
  let at;
153
151
  try {
154
152
  at = JSON.parse(line).at;
155
153
  }
156
154
  catch {
157
- pruned += 1; // Malformed line: no timestamp to keep, so it ages out.
155
+ dropped += 1; // Malformed line: no timestamp to keep, so it ages out.
158
156
  continue;
159
157
  }
160
158
  if (typeof at === "number" && at >= cutoff)
161
159
  kept.push(line);
162
160
  else
163
- pruned += 1;
161
+ dropped += 1;
164
162
  }
165
163
  if (kept.length !== lines.length) {
166
- writeTextAtomic(file, kept.length > 0 ? kept.join("\n") + "\n" : "");
164
+ try {
165
+ writeTextAtomic(file, kept.length > 0 ? kept.join("\n") + "\n" : "");
166
+ }
167
+ catch {
168
+ return;
169
+ }
170
+ pruned += dropped;
167
171
  }
168
172
  });
169
173
  return pruned;
@@ -180,7 +184,7 @@ const SESSION_DELETE_TIMEOUT_MS = 10_000;
180
184
  * reported success, and prunes spill files with the same window. The caller (retention) runs
181
185
  * this BEFORE pruneSessionLedgers trims the lines, so the full ledger is still in view. */
182
186
  export function pruneOpencodeSessions(sessionsRoot, days, opts) {
183
- const cutoff = Date.now() - days * 24 * 3600 * 1000;
187
+ const cutoff = daysAgoMs(days);
184
188
  let deleted = 0;
185
189
  if (opts.bin !== undefined) {
186
190
  walkFiles(sessionsRoot, (file) => {
@@ -223,3 +227,69 @@ export function pruneOpencodeSessions(sessionsRoot, days, opts) {
223
227
  }
224
228
  return deleted;
225
229
  }
230
+ /** Delete each Codex session the ledger records as expired, with one
231
+ * `codex delete --force <id>` each (bounded), plus the rollout-file fallback. Codex's sessions
232
+ * live under `CODEX_HOME` (part 8/9), OUTSIDE `.tumwater/` and outside the session dirs this
233
+ * pass walks, so trimming the ledger line alone would orphan them; the CLI's `delete --force`
234
+ * is the one way to reach them headless (design finding 21: without `--force` Codex refuses to
235
+ * delete without a terminal). Best-effort and backend-specific, kept out of the generic
236
+ * `pruneSessionLedgers` line pass: it returns the deletes that reported success. Whether or not
237
+ * a delete succeeded the session's own rollout file is removed as the fallback (the OpenCode
238
+ * spill-file fallback's twin), so the ledger line and rollout do not outlive the window. The
239
+ * caller (retention) runs this BEFORE pruneSessionLedgers trims the lines, so the full ledger
240
+ * is still in view. */
241
+ export function pruneCodexSessions(sessionsRoot, days, opts) {
242
+ const cutoff = daysAgoMs(days);
243
+ let deleted = 0;
244
+ // Codex reuses a resumed session's UUID, so one id can carry several ledger records — each
245
+ // resume appends a fresh line with the same id (design §4 resumes by id). Only an id whose
246
+ // EVERY record is expired is a session safe to trim: an older expired record beside a
247
+ // still-fresh resume must not `delete` the live session or unlink its rollout, so the live
248
+ // ids are gathered first and every candidate id is checked against them.
249
+ const live = new Set();
250
+ const expired = new Set();
251
+ walkFiles(sessionsRoot, (file) => {
252
+ if (!file.endsWith(SESSION_LEDGER_FILE))
253
+ return;
254
+ for (const record of readSessions(path.dirname(file))) {
255
+ if (record.agent !== "codex")
256
+ continue;
257
+ (record.at >= cutoff ? live : expired).add(record.id);
258
+ }
259
+ });
260
+ for (const id of expired) {
261
+ if (live.has(id))
262
+ continue;
263
+ if (opts.bin !== undefined) {
264
+ try {
265
+ const res = spawnSync(opts.bin, ["delete", "--force", id], {
266
+ env: opts.env,
267
+ timeout: SESSION_DELETE_TIMEOUT_MS,
268
+ stdio: "ignore",
269
+ });
270
+ if (res.status === 0)
271
+ deleted += 1;
272
+ }
273
+ catch {
274
+ // Best effort: an unwritable env or a missing binary must not fail the prune.
275
+ }
276
+ }
277
+ // Fallback: unlink the home's rollout for this session so neither the ledger line nor
278
+ // its file outlives the window. A real `codex delete` removes the rollout itself, so a
279
+ // successful delete has nothing left to unlink; a failed or skipped delete does, and
280
+ // this is what trims it.
281
+ const home = opts.env?.CODEX_HOME;
282
+ if (home !== undefined) {
283
+ const rollout = findRollout(home, id);
284
+ if (rollout !== null) {
285
+ try {
286
+ fs.unlinkSync(rollout);
287
+ }
288
+ catch {
289
+ // Best effort: a vanished or unwritable rollout is not a failure.
290
+ }
291
+ }
292
+ }
293
+ }
294
+ return deleted;
295
+ }
@@ -0,0 +1,220 @@
1
+ /** The per-backend subscription account state: which billing pays for a backend's runs, and
2
+ * the newest subscription usage-window reading each run reported (plans/agent-backends.md §7
3
+ * "Billing" and §8 "The subscription usage window"). Persisted at
4
+ * `.tumwater/state/<backend>-account.json` (plan design grounds/codex-backend.md "Groundwork:
5
+ * the subscription account is per backend", item 1), written only by the orchestrator process,
6
+ * so the sibling parts that throttle or display the subscription window read one answer instead
7
+ * of re-deriving it per surface.
8
+ *
9
+ * The file is a cache, never a source of truth: a run that reports billing overwrites it, and a
10
+ * missing or unreadable file reads as "no subscription". `resetsAt` is stored as epoch
11
+ * milliseconds (the run parser reports unix seconds; observeAccount converts once here), so
12
+ * consumers compare it to `Date.now()` directly. The claude account (src/agent/claude/
13
+ * claude-account.ts) is this module specialized to the "claude" backend: for its file the
14
+ * logical `plan` field is stored under the historical key `subscriptionType`, so claude's file
15
+ * keeps today's shape. */
16
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
17
+ import { finiteNumber, isJsonObject, setOwn } from "../files/json-object.js";
18
+ import { accountPath } from "../paths.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 runs and overage are budget/fallback matters, not a
24
+ * window hold, so they read false; a backend that reports no window also reads false (a pi
25
+ * 429, for example). */
26
+ export function subscriptionWindowRejected(run) {
27
+ return (run.transientRateLimit &&
28
+ run.billing === "subscription" &&
29
+ (run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
30
+ }
31
+ /** The subscription account's used-up usage window, or null: a window reading `rejected` with
32
+ * no overage, or (with `includeOverage`, the default) a window whose current reading shows
33
+ * `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
34
+ * last. With a `reserve` (config `backends.<id>.windowReserve`), a window whose utilization
35
+ * has reached its type's fraction counts too, and comes back with `reserve: true`; a rejected
36
+ * or overage window outranks a reserve one, so the flag is set only when no window is used up
37
+ * for real. The budget gate's window move (pollBudgetGate) reads the default; the team hold
38
+ * (windowObservation) passes `includeOverage: false`, because overage never holds a
39
+ * seam. Only subscription billing produces one; a missing account reads null. */
40
+ export function windowUsedUp(account, now, includeOverage = true, reserve) {
41
+ if (account?.billing !== "subscription")
42
+ return null;
43
+ let furthest = null;
44
+ let reserved = null;
45
+ for (const [type, w] of Object.entries(account.windows)) {
46
+ if (typeof w.resetsAt !== "number" || w.resetsAt <= now)
47
+ continue;
48
+ const used = w.isUsingOverage === true ? includeOverage : w.status === "rejected";
49
+ if (used) {
50
+ if (furthest === null || w.resetsAt > furthest.resetsAt)
51
+ furthest = { type, resetsAt: w.resetsAt };
52
+ continue;
53
+ }
54
+ if (windowReserved(w, reserve?.[type]) && (reserved === null || w.resetsAt > reserved.resetsAt))
55
+ reserved = { type, resetsAt: w.resetsAt, reserve: true };
56
+ }
57
+ return furthest ?? reserved;
58
+ }
59
+ /** Has this window's utilization reached its reserve fraction? False without a fraction or a
60
+ * reading. */
61
+ export function windowReserved(w, fraction) {
62
+ return fraction !== undefined && typeof w.utilization === "number" && w.utilization >= fraction;
63
+ }
64
+ /** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
65
+ * every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
66
+ const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
67
+ /** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
68
+ * type falls back to its words with underscores as spaces. The one home of the window spelling
69
+ * the status badge and the role view share. */
70
+ export function windowLabel(type) {
71
+ const m = /^(\w+)_(hour|day|week)$/.exec(type);
72
+ if (!m)
73
+ return type.replace(/_/g, " ");
74
+ const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
75
+ const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
76
+ const word = m[1] ?? "";
77
+ return `${words[word] ?? word}${unit}`;
78
+ }
79
+ /** The fraction of a subscription usage window consumed (0–1) as a whole-number percent: a
80
+ * `0.64` utilization reads `64`. The one home of the fraction→percent conversion, shared by
81
+ * the status badge, the doctor's hold reason and the event formatter so they render a window's
82
+ * fill identically (the browser dashboard's own window line is embedded client-side and keeps
83
+ * its inline copy). */
84
+ export function utilizationPct(utilization) {
85
+ return Math.round(utilization * 100);
86
+ }
87
+ /** Order two window entries by the windowEntries rule: status severity, then the reported
88
+ * utilization (a missing reading sorts below any number), then the earlier reset, then the
89
+ * type name. Exported so a list of per-backend blocks can be ordered by their primary entry
90
+ * the same way windowEntries orders one account's windows (part 3a/9). */
91
+ export function compareWindowEntries(a, b) {
92
+ const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
93
+ if (rank !== 0)
94
+ return rank;
95
+ const util = (b.utilization ?? -1) - (a.utilization ?? -1);
96
+ if (util !== 0)
97
+ return util;
98
+ const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
99
+ if (reset !== 0)
100
+ return reset;
101
+ return a.type.localeCompare(b.type);
102
+ }
103
+ /** The account's windows as a list, most constrained first (by compareWindowEntries). Empty
104
+ * when the account reported none. */
105
+ export function windowEntries(account) {
106
+ return Object.entries(account.windows)
107
+ .map(([type, w]) => ({ type, ...w }))
108
+ .sort((a, b) => compareWindowEntries(a, b));
109
+ }
110
+ /** Read the persisted account state for a backend, or null when it is absent or unreadable. A
111
+ * file without a usable top-level `observedAt` reads as absent (the plan's torn-file rule),
112
+ * and a window whose `resetsAt` is at or before `now` reads back with `status: "reset"` — the
113
+ * reader, not every consumer, decides that the window no longer limits. */
114
+ export function readAccount(root, backend, now = Date.now()) {
115
+ const doc = readJsonFile(accountPath(root, backend));
116
+ if (doc === null)
117
+ return null;
118
+ const billing = doc.billing === "subscription" || doc.billing === "api" ? (doc.billing) : undefined;
119
+ const observedAt = finiteNumber(doc.observedAt, Number.NaN);
120
+ if (billing === undefined || !Number.isFinite(observedAt))
121
+ return null;
122
+ const windows = {};
123
+ if (isJsonObject(doc.windows)) {
124
+ for (const [type, value] of Object.entries(doc.windows)) {
125
+ const window = readWindow(value, observedAt, now);
126
+ if (window !== undefined)
127
+ setWindow(windows, type, window);
128
+ }
129
+ }
130
+ return {
131
+ billing,
132
+ ...(typeof doc.subscriptionType === "string" ? { plan: doc.subscriptionType } : {}),
133
+ observedAt,
134
+ windows,
135
+ };
136
+ }
137
+ /** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
138
+ * missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
139
+ * status to `reset` and drops the overage flags. */
140
+ function readWindow(value, fileObservedAt, now) {
141
+ if (!isJsonObject(value) || typeof value.status !== "string")
142
+ return undefined;
143
+ const observedAt = finiteNumber(value.observedAt, fileObservedAt);
144
+ const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
145
+ const resets = Number.isFinite(resetsAt);
146
+ // utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
147
+ // non-finite fraction would render as `Infinity%` in the badges and poison the
148
+ // windowEntries comparator (Infinity - Infinity is NaN).
149
+ const utilization = finiteNumber(value.utilization, undefined);
150
+ const reset = resets && resetsAt <= now;
151
+ const window = {
152
+ status: reset ? "reset" : value.status,
153
+ ...(resets ? { resetsAt } : {}),
154
+ ...(utilization !== undefined ? { utilization } : {}),
155
+ // a reset window is no longer in overage: dropping both keeps every reader in agreement
156
+ ...(!reset && typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
157
+ ...(!reset && value.isUsingOverage === true ? { isUsingOverage: true } : {}),
158
+ observedAt,
159
+ };
160
+ return window;
161
+ }
162
+ /** Store one window under its `rateLimitType`. A plain `windows[type] = window` loses a type
163
+ * of `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's
164
+ * prototype instead of adding an own entry, so the window vanishes from every
165
+ * Object.entries/values reader. Defining the own property keeps even that key — the same
166
+ * inherited-key guard claude-stream's TOOL_NAMES lookup uses. */
167
+ function setWindow(windows, type, window) {
168
+ setOwn(windows, type, window);
169
+ }
170
+ /** Overwrite a backend's account file with `account`, creating the state directory. The
171
+ * orchestrator's one writer; a failure to write is the caller's to handle (best-effort at the
172
+ * run boundary). Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write
173
+ * leaves the previous file whole, not a torn one the reader heals to "no subscription" — which
174
+ * would silently drop the billing answer and the window hold until the next run re-observed
175
+ * them. The claude file's `plan` is serialized under its historical `subscriptionType` key. */
176
+ export function writeAccount(root, backend, account) {
177
+ writeJsonAtomic(accountPath(root, backend), {
178
+ billing: account.billing,
179
+ ...(account.plan !== undefined ? { subscriptionType: account.plan } : {}),
180
+ observedAt: account.observedAt,
181
+ windows: account.windows,
182
+ }, true);
183
+ }
184
+ /** Fold one run's billing and rate-window readings into a backend's account file: the run's
185
+ * `billing` overwrites the stored answer, and each reported window replaces the stored reading
186
+ * for its `type`, stamped with `observedAt`. A run that reported neither billing nor windows
187
+ * leaves the file untouched (and returns whatever was already there). */
188
+ export function observeAccount(root, backend, run, now = Date.now()) {
189
+ const existing = readAccount(root, backend, now);
190
+ const billing = run.billing ?? existing?.billing;
191
+ const reported = run.rateWindows ?? [];
192
+ if (billing === undefined || (run.billing === undefined && reported.length === 0))
193
+ return existing;
194
+ const windows = { ...(existing?.windows ?? {}) };
195
+ for (const w of reported)
196
+ setWindow(windows, w.type, windowFromRun(w, now));
197
+ const account = {
198
+ billing,
199
+ ...(existing?.plan !== undefined ? { plan: existing.plan } : {}),
200
+ observedAt: now,
201
+ windows,
202
+ };
203
+ writeAccount(root, backend, account);
204
+ return account;
205
+ }
206
+ /** One run's rate-window reading in the file's shape: unix-seconds `resetsAt` becomes epoch ms,
207
+ * and absent optional fields stay absent. */
208
+ function windowFromRun(w, now) {
209
+ // The write choke point heals utilization like readWindow does, so a non-finite value
210
+ // handed straight to observeAccount can never re-enter the file through a save.
211
+ const utilization = finiteNumber(w.utilization, undefined);
212
+ return {
213
+ status: w.status,
214
+ ...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt * 1000 } : {}),
215
+ ...(utilization !== undefined ? { utilization } : {}),
216
+ ...(typeof w.overageStatus === "string" ? { overageStatus: w.overageStatus } : {}),
217
+ ...(w.isUsingOverage === true ? { isUsingOverage: true } : {}),
218
+ observedAt: now,
219
+ };
220
+ }