tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,4 +1,5 @@
1
- import { gateRoleConfig, newBudgetGateState, pollBudgetGate, tickOnPair, } from "./budget-gates.js";
1
+ import { gateRoleConfig, newBudgetGateState, pollBudgetGate, runOnPair, subscriptionBackendOf, } from "./budget-gates.js";
2
+ import { backendsInUse } from "./startup-gate.js";
2
3
  import { fallbackDemotion } from "../budget/fallback-breaker.js";
3
4
  import { newPauseGateState, pollPauseGates } from "./pause-gates.js";
4
5
  import { newStreakGateState, pollStreakGate } from "./streak-gate.js";
@@ -7,18 +8,18 @@ import { newMaintenanceQuotaGateState, pollMaintenanceQuotaGate, } from "./maint
7
8
  import { newBootstrapGateState, pollBootstrapGate, } from "./bootstrap-gates.js";
8
9
  import { newQuietHoursGateState, pollQuietHoursGate, roleQuietHold, } from "../scheduling/quiet-hours.js";
9
10
  import { BYTES_PER_GB, newDiskGateState, pollDiskGate, sampleFreeBytes, } from "./disk-gate.js";
10
- import { pollErrorStorm, pollFailureSpread, pollTeamHold, pollClaudeWindowWarning, pollClaudeWindowReserve } from "../team/team-polls.js";
11
- import { readClaudeAccount } from "../agent/claude/claude-account.js";
11
+ import { pollErrorStorm, pollFailureSpread, pollTeamHold, pollClaudeWindowWarning, pollClaudeWindowReserve, windowObservation } from "../team/team-polls.js";
12
+ import { readAccount } from "../agent/subscription-account.js";
12
13
  import { standingHolds } from "../team/team-hold.js";
13
14
  import { ERROR_STORM_QUIET } from "../team/error-storm.js";
14
15
  import { FAILURE_SPREAD_QUIET } from "../team/failure-spread.js";
15
16
  import { DIRECTOR_ROLE } from "../roles/roles.js";
16
- import { baseRoleOf, instancePauseKey, loopEnabled } from "../roles/loop-ids.js";
17
+ import { baseRoleOf, instancePauseKey, runnerEnabled } from "../roles/runner-ids.js";
17
18
  import { queuedMergeRoles } from "../merge/merge-queue.js";
18
19
  import { MODEL_TIERS } from "../config/config-schema.js";
19
20
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
20
21
  import { publishInfoFields } from "../team/orchestrator-info.js";
21
- import { roleSeamTier } from "../config/config-views.js";
22
+ import { configForRole, roleSeamTier, windowReserveFor, withModelOverrides } from "../config/config-views.js";
22
23
  /** A fresh gate family: nothing seen yet, so the first poll of an unpaused, unheld team
23
24
  * logs nothing. The budget gate seeds its breaker and derived fallback view from the
24
25
  * startup config; a live reload replaces the config on the next poll. */
@@ -33,8 +34,8 @@ export function newTeamGateStates(config) {
33
34
  quiet: newQuietHoursGateState(),
34
35
  disk: newDiskGateState(),
35
36
  teamHold: new Map(),
36
- claudeWindowWarned: null,
37
- claudeWindowReserved: null,
37
+ claudeWindowWarned: new Set(),
38
+ claudeWindowReserved: new Set(),
38
39
  errorStorm: ERROR_STORM_QUIET,
39
40
  failureSpread: FAILURE_SPREAD_QUIET,
40
41
  };
@@ -42,11 +43,16 @@ export function newTeamGateStates(config) {
42
43
  /** Poll every team-wide gate and alarm, advancing `states` in place. */
43
44
  export function pollTeamGates(states, ctx) {
44
45
  const { root, runners, liveConfig, modelsPath, now, info } = ctx;
46
+ // The per-backend subscription account lookup (part 2e/9): each backend's account is read on
47
+ // demand — the window hold/reserve/warning loop, the window-move and the handback reads each
48
+ // call accounts fresh — so a second subscription backend is judged from its own account, never
49
+ // from claude's.
50
+ const accounts = (backend) => readAccount(root, backend, now);
45
51
  // Daily cost budget gate (src/gates/budget-gates.ts owns the reads, the edge-triggered
46
52
  // budget_* events, the breaker re-key, and the fallback config view): the orchestrator
47
53
  // owns only the wiring — the demotion publish below and the per-runner assignment at
48
54
  // the bottom of this block.
49
- const { gate, roleConfig, spentUsd, capUsd, onFallback, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, windowTiers, windowResumed, directorConfig, chain, } = pollBudgetGate(states.budget, {
55
+ const { gate, roleConfig, spentUsd, capUsd, onFallback, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, windowTiers, windowResumedBackends, directorConfig, chain, } = pollBudgetGate(states.budget, {
50
56
  root,
51
57
  states: runners.map((r) => r.state),
52
58
  liveConfig,
@@ -54,7 +60,7 @@ export function pollTeamGates(states, ctx) {
54
60
  });
55
61
  // Publish what observers cannot derive themselves: every pair's demotion — a per-tier
56
62
  // fallback can demote a pair the legacy single `fallback` never names, and a dashboard that
57
- // only sees the engaged pair's verdict would show a tier's roles ticking while the
63
+ // only sees the engaged pair's verdict would show a tier's roles running while the
58
64
  // scheduler holds them (part 5c/8) — plus the engaged pair's own (the legacy
59
65
  // `fallbackDemoted` field, what `tumwater doctor` and the failure digest read) and the
60
66
  // gate's own spend/cap pair — summed over the runners' live states, which every
@@ -92,26 +98,42 @@ export function pollTeamGates(states, ctx) {
92
98
  // The director keeps the live config — an explicit human prompt outranks the
93
99
  // autonomous-spend cap (gateRoleConfig). Assigned every poll (not only on transitions)
94
100
  // so a runner created mid-gate, or one left behind by a broken-file poll that skipped
95
- // the reload, can never tick on the wrong model.
101
+ // the reload, can never run on the wrong model.
96
102
  for (const r of runners) {
97
103
  r.config = gateRoleConfig(r.role, liveConfig, roleConfig, directorConfig);
98
104
  // An image reroute must never spend past the daily cap: while the gate has the team on
99
105
  // its free fallback pairs, a reroute looks only among those pairs (plans/image-input.md
100
106
  // "Reroute target" "Regime"). Set beside the config assignment so the scheduler's regime
101
- // and the loops' never disagree.
107
+ // and the roles' never disagree.
102
108
  r.budgetFallbackActive = onFallback;
103
109
  }
104
- // The budget reopened: a tick that started on a fallback pair keeps it until it ends, so hand
105
- // those ticks back — abort them resumably (session and worktree edits kept) and let their next
106
- // tick continue the same session on the primary, whose config the assignment above already
110
+ // The budget reopened: a run that started on a fallback pair keeps it until it ends, so hand
111
+ // those runs back — abort them resumably (session and worktree edits kept) and let their next
112
+ // run continue the same session on the primary, whose config the assignment above already
107
113
  // installed (PLANS.md 2026-09-30). Per-tier fallbacks put different roles on different pairs
108
114
  // (part 5c/8), so the match runs over EVERY pair the price-based resolution hands out, not only
109
- // one. A tick started on the primary keeps running, the director is exempt, and the merges (the
115
+ // one. A run started on the primary keeps running, the director is exempt, and the merges (the
110
116
  // slot's own runs) are untouched. One event per pair names who was handed back, so the resulting
111
- // aborted ticks read as the budget reopening, not as unexplained failures. The subscription
117
+ // aborted runs read as the budget reopening, not as unexplained failures. The subscription
112
118
  // window reset (below the cap): the same hand-back for the pairs the window move used. A window
113
119
  // that resets while the cap still holds hands back nothing.
114
- const handBack = gateResumed || (windowResumed && !budgetActive);
120
+ const handBack = gateResumed || (windowResumedBackends.length > 0 && !budgetActive);
121
+ // The window handback names the backend whose window resumed (the entry's goal) — not the
122
+ // handed-back pair's own backend: for claude-only the fallback pair carries no backend (it would
123
+ // read undefined), and with two subscription backends it would name the opposite one. A pair
124
+ // belongs to those resumed backends whose tiers' price-resolved pair it is.
125
+ const resumedBackendSet = new Set(windowResumedBackends);
126
+ const resumedBackendForPair = (leftPair) => {
127
+ for (const tier of MODEL_TIERS) {
128
+ const p = priceResolved[tier]?.pair;
129
+ if (p && p.provider === leftPair.provider && p.model === leftPair.model) {
130
+ const b = subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
131
+ if (b !== null && resumedBackendSet.has(b))
132
+ return b;
133
+ }
134
+ }
135
+ return undefined;
136
+ };
115
137
  if (handBack && fallbackPairs.length > 0) {
116
138
  // A cap reopen while the window is still used up leaves the window's tiers on their
117
139
  // fallback pairs: handing those runs back would abort them to restart on the same pair.
@@ -122,7 +144,7 @@ export function pollTeamGates(states, ctx) {
122
144
  for (const leftPair of fallbackPairs) {
123
145
  if (stillMoved.some((p) => p.provider === leftPair.provider && p.model === leftPair.model))
124
146
  continue;
125
- const matching = runners.filter((r) => r.role !== DIRECTOR_ROLE && tickOnPair(r.tickModel(), leftPair));
147
+ const matching = runners.filter((r) => r.role !== DIRECTOR_ROLE && runOnPair(r.runModelPair(), leftPair));
126
148
  if (matching.length > 0) {
127
149
  logEventBestEffort(root, {
128
150
  source: TUMWATER_SOURCE,
@@ -130,10 +152,12 @@ export function pollTeamGates(states, ctx) {
130
152
  roles: matching.map((r) => r.role),
131
153
  provider: leftPair.provider,
132
154
  model: leftPair.model,
133
- ...(gateResumed ? {} : { reason: "window" }),
155
+ ...(gateResumed
156
+ ? {}
157
+ : { reason: "window", backend: resumedBackendForPair(leftPair) }),
134
158
  });
135
159
  for (const r of matching)
136
- r.handBackTick();
160
+ r.handBackRun();
137
161
  }
138
162
  }
139
163
  }
@@ -142,7 +166,7 @@ export function pollTeamGates(states, ctx) {
142
166
  // set, both read fresh per cycle so a marker change takes effect on the next poll.
143
167
  const { userPaused, pausedRoles } = pollPauseGates(root, states.pause);
144
168
  // The error-streak circuit breaker (src/gates/streak-gate.ts): a role past ERROR_STREAK_BREAKER
145
- // consecutive failed ticks is paused through the same per-role marker the operator's
169
+ // consecutive failed runs is paused through the same per-role marker the operator's
146
170
  // `pause --role` writes — act-on-it where the warn bar and the storm alarms only talk. The
147
171
  // director is not exempt; a failing director cannot process prompts anyway. Roles it just
148
172
  // paused fold into this poll's paused-roles view, so the scheduler blocks them on this very
@@ -157,22 +181,36 @@ export function pollTeamGates(states, ctx) {
157
181
  states.pause.prevPausedRoles.add(role);
158
182
  const pausedRolesNow = streakPaused.length > 0 ? new Set([...pausedRoles, ...streakPaused]) : pausedRoles;
159
183
  // The per-tier budget pause (part 5c/8): a role whose model tier resolved to no usable free
160
- // pair while the cap is reached starts no new ticks — the stateless verdict recomputed every
184
+ // pair while the cap is reached starts no new runs — the stateless verdict recomputed every
161
185
  // poll from the SAME resolution the gate's value came from, beside `capPaused` (the
162
186
  // scheduling pass folds the sets; the director is exempt, as under every autonomous gate).
163
187
  // The demoted pair's probe pierces the set for its own tier's roles only — probeRoles in the
164
188
  // orchestrator's scheduling pass.
165
189
  const budgetPausedRoles = new Set();
166
- if (budgetActive) {
167
- for (const r of runners) {
168
- if (r.role === DIRECTOR_ROLE)
169
- continue;
170
- if (servingResolved[roleSeamTier(liveConfig, r.role)].pair === null)
171
- budgetPausedRoles.add(r.role);
172
- }
190
+ const windowMovedRoles = new Set();
191
+ const movedTiers = Object.keys(windowTiers);
192
+ // The backend whose used-up window moved a given tier: the tier's own seam's backend — the same
193
+ // read budget-gates made to put it in `windowTiers`. A role on a moved tier whose own backend is
194
+ // DIFFERENT (a healthy backend riding the same tier) keeps its pair: only the role whose own
195
+ // backend owns the out window is moved.
196
+ const tierMovedBackend = (tier) => subscriptionBackendOf(withModelOverrides(liveConfig, tier, {}), accounts);
197
+ for (const r of runners) {
198
+ if (r.role === DIRECTOR_ROLE)
199
+ continue;
200
+ const tier = roleSeamTier(liveConfig, r.role);
201
+ // A role pinned to the moved tier's own subscription backend keeps it through the window move
202
+ // (it is not on a pair); a role on the same tier pinning a healthy backend does not.
203
+ if (movedTiers.includes(tier) &&
204
+ subscriptionBackendOf(configForRole(liveConfig, r.role), accounts) === tierMovedBackend(tier))
205
+ windowMovedRoles.add(r.role);
206
+ // Below the cap only a window-moved role can sit on a demoted fallback.
207
+ if (!budgetActive && !windowMovedRoles.has(r.role))
208
+ continue;
209
+ if (servingResolved[tier].pair === null)
210
+ budgetPausedRoles.add(r.role);
173
211
  }
174
- // The per-role daily cost cap (src/gates/role-cap-gates.ts): a loop whose local-day spend has
175
- // reached its own maxDailyCostUsdPerRole entry starts no new ticks — the stateless verdict
212
+ // The per-role daily cost cap (src/gates/role-cap-gates.ts): a role whose local-day spend has
213
+ // reached its own maxDailyCostUsdPerRole entry starts no new runs — the stateless verdict
176
214
  // recomputed every poll, no pause marker written (the marker is anonymous; a cap pause must
177
215
  // never masquerade as an operator's), so this returns its own set beside pausedRoles and the
178
216
  // pause gates' edge bookkeeping never sees it. A live config edit applies on the next poll;
@@ -180,27 +218,26 @@ export function pollTeamGates(states, ctx) {
180
218
  const capPaused = pollRoleCapGate(root, states.cap, runners, liveConfig.maxDailyCostUsdPerRole, now);
181
219
  // The maintenance allowance (plans/work-ratio.md, part 1b/4): code-maintenance merges in a
182
220
  // rolling 24 h window are capped at `maintenancePerWorkMerge × work + MAINTENANCE_DAILY_FLOOR`.
183
- // The gate counts the window, adds the maintenance loops already in flight (running a tick or
221
+ // The gate counts the window, adds the maintenance roles already in flight (running or
184
222
  // holding a queued merge, so six concurrent permits cannot overshoot), and logs the one
185
- // hold/resume event per transition. Enabled only: a disabled loop runs no tick to count. The
186
- // scheduling pass spends the returned headroom one due maintenance loop at a time, beside
223
+ // hold/resume event per transition. Enabled only: a disabled role has no runs to count. The
224
+ // scheduling pass spends the returned headroom one due maintenance role at a time, beside
187
225
  // capPaused, with the fresh-wake/queued-prompt override the need-based deferral honors.
188
226
  const queuedRoles = queuedMergeRoles(root);
189
227
  const maintenanceQuota = pollMaintenanceQuotaGate(root, states.quota, runners.map((r) => ({
190
228
  role: r.role,
191
- enabled: loopEnabled(r.config, r.role),
229
+ enabled: runnerEnabled(r.config, r.role),
192
230
  running: r.state.running === true,
193
231
  hasQueuedMerge: queuedRoles.has(r.role),
194
232
  })), liveConfig.maintenancePerWorkMerge, now);
195
233
  // New-project bootstrap (src/gates/bootstrap-gates.ts, plans/work-ratio.md part 2/2): a fresh
196
- // project's maintenance loops start no new ticks until `bootstrap.untilPlansDone` plans are
234
+ // project's maintenance roles start no new runs until `bootstrap.untilPlansDone` plans are
197
235
  // Done. The gate owns the latch write and the one `bootstrap_complete` event; the scheduling
198
236
  // pass folds the held set in beside capPaused (with the fresh-wake/queued-prompt override the
199
237
  // need-based deferral honors).
200
238
  const bootstrapHeld = pollBootstrapGate(root, states.bootstrap, runners, liveConfig, now);
201
- const bootstrapActive = states.bootstrap.active;
202
239
  // Per-role quiet hours (src/scheduling/quiet-hours.ts): a role whose own quietHoursPerRole window
203
- // covers `now` starts no new ticks — the team window's semantics scoped to one loop, the
240
+ // covers `now` starts no new runs — the team window's semantics scoped to one role, the
204
241
  // director exempt like every autonomous gate. Stateless, recomputed per poll exactly like
205
242
  // capPaused: no crossing events, no state — a live config edit applies on the next poll
206
243
  // by construction. A role held by BOTH windows is held once: the scheduling pass folds
@@ -213,9 +250,9 @@ export function pollTeamGates(states, ctx) {
213
250
  roleQuietHeld.add(r.role);
214
251
  }
215
252
  // Quiet hours (src/scheduling/quiet-hours.ts): the config-driven daily local-time window during
216
- // which role loops start no new ticks — the operator pause's semantics on a schedule.
253
+ // which roles start no new runs — the operator pause's semantics on a schedule.
217
254
  // The config value is read fresh per cycle, so a live edit applies on the next poll;
218
- // exactly one quiet_hours_started/ended event per crossing. In-flight ticks finish;
255
+ // exactly one quiet_hours_started/ended event per crossing. In-flight runs finish;
219
256
  // the gate folds into the same hold site as the pause gates.
220
257
  const quietNow = pollQuietHoursGate(root, liveConfig.quietHours, states.quiet, new Date(now));
221
258
  const freeBytes = (ctx.sampleFree ?? sampleFreeBytes)(root);
@@ -245,7 +282,7 @@ export function pollTeamGates(states, ctx) {
245
282
  };
246
283
  publishInfoFields(root, info, [["disk", disk]]);
247
284
  // Each runner as the two provider-failure polls read it (HoldInputs): the role, its two
248
- // episodic fields, and the provider its NEXT tick will run on (runProvider — the tier
285
+ // episodic fields, and the provider its NEXT run will run on (runProvider — the tier
249
286
  // fallback pair while a model-fallback episode is active, else the role's resolved config;
250
287
  // undefined when pi's default is in charge). Built ONCE per poll so the hold and the spread
251
288
  // read the same list, like holdObservations on the reducer side. Keying on the effective
@@ -253,7 +290,7 @@ export function pollTeamGates(states, ctx) {
253
290
  // primary stays clear (PLANS.md "Model failure fallback, part 1/2").
254
291
  //
255
292
  // Each stamp (lastRateLimit/lastBackendFailure) carries the hold key of the backend that
256
- // ACTUALLY failed — a reviewer run folded through the author's loop stamps `claude`, not
293
+ // ACTUALLY failed — a reviewer run folded through the author's role stamps `claude`, not
257
294
  // the pi author's provider — and the polls' observation mapper files each stamp under that
258
295
  // key, falling back to runProvider here for a stamp without one (PLANS.md "Mixed claude +
259
296
  // pi fleets, part 2/4"). The spreads below carry the stamps wholesale, provider included.
@@ -265,24 +302,32 @@ export function pollTeamGates(states, ctx) {
265
302
  }));
266
303
  // Team-wide failure hold, per provider (src/team/team-hold.ts, PLANS.md 2026-10-05): once two
267
304
  // roles' runs have ended on the SAME provider AND failure kind within a short window — 429s, or a
268
- // connection, timeout, 5xx, or model-load backend failure — role loops on THAT provider start no
269
- // new ticks, and the merge queue starts no new verify, whose reviewer run has no retry and would
305
+ // connection, timeout, 5xx, or model-load backend failure — roles on THAT provider start no
306
+ // new runs, and the merge queue starts no new verify, whose reviewer run has no retry and would
270
307
  // spend a review strike on the storm — until the hold re-opens at its own deadline (Retry-After
271
308
  // honored on the rate-limit kind, doubling on a relapse, capped). Roles on a healthy provider
272
- // keep ticking; a hold on the reviewer's provider (with the review gate on) still blocks
309
+ // keep running; a hold on the reviewer's provider (with the review gate on) still blocks
273
310
  // everything, since nothing could merge — the orchestrator's scheduling and start passes derive
274
- // that verdict from this map. The director's ticks are exempt, as under the budget gate and the
311
+ // that verdict from this map. The director's runs are exempt, as under the budget gate and the
275
312
  // operator pause: an explicit human prompt outranks an autonomous gate, one director run is not
276
313
  // the concurrency that sustains a storm, its 429 runs keep the per-run transient retry
277
- // (backend-failure kinds ride this hold alone), and a prompt its tick fails to fulfill goes back
278
- // to the inbox. In-flight ticks finish; NEW ticks are gated at scheduling like both siblings, and
279
- // a role tick already parked in the semaphore meets the same hold at its permit (the start gate)
314
+ // (backend-failure kinds ride this hold alone), and a prompt its run fails to fulfill goes back
315
+ // to the inbox. In-flight runs finish; NEW runs are gated at scheduling like both siblings, and
316
+ // a role run already parked in the semaphore meets the same hold at its permit (the start gate)
280
317
  // and hands its reservation back instead of starting into the storm. The verdicts are written
281
318
  // back to states.teamHold — one hold per provider, lifted holds kept keyed for their relapse
282
319
  // memory but never reading as held — and the permit-time closures read the LATEST poll's map
283
320
  // there, so a waiter granted its permit after a later poll sees that poll's world.
284
- const claudeAccount = readClaudeAccount(root, now);
285
- states.teamHold = pollTeamHold(root, states.teamHold, holdInputs(runners), now, claudeAccount, windowTiers, liveConfig.claudeWindowReserve);
321
+ const windowAccountReads = backendsInUse(liveConfig).map((backend) => [backend, accounts(backend)]);
322
+ // The subscription-window hold, reserve and warning, read per backend in use (part 2e/9): a
323
+ // second subscription backend gets its own hold, reserve and warning memory keyed by its own
324
+ // `backend`, so claude's roles, hold and events are untouched by another backend's window.
325
+ // One account read per backend feeds both the hold observations and the reserve/warning polls;
326
+ // a backend whose account implies no hold contributes no observation.
327
+ const windowObservations = windowAccountReads
328
+ .map(([backend, account]) => windowObservation(backend, account, now, windowReserveFor(liveConfig, backend)))
329
+ .filter((o) => o !== null);
330
+ states.teamHold = pollTeamHold(root, states.teamHold, holdInputs(runners), now, windowObservations, windowTiers);
286
331
  // The standings hold, published for observers (PLANS.md 2026-10-17 "the dashboards and
287
332
  // `tumwater status` name a standing provider failure hold"): `standingHolds` turns the holds
288
333
  // map into the per-provider list the status snapshot and the alert banner name, the same
@@ -292,12 +337,17 @@ export function pollTeamGates(states, ctx) {
292
337
  publishInfoFields(root, info, [
293
338
  ["providerHolds", providerHolds.length > 0 ? providerHolds : undefined],
294
339
  ]);
295
- states.claudeWindowReserved = pollClaudeWindowReserve(root, states.claudeWindowReserved, claudeAccount, now, liveConfig.claudeWindowReserve);
296
- // The subscription usage window's warning is observational (the hold above is the gate): the
297
- // first allowed_warning logs once per window, and the memory lives beside the holds map.
298
- states.claudeWindowWarned = pollClaudeWindowWarning(root, states.claudeWindowWarned, claudeAccount, now);
299
- // Team-wide error-storm warning (src/team/error-storm.ts): when several roles' tick streaks
300
- // fail consecutively on one shared cause, each role's own "consecutive tick failures"
340
+ // The reserve and warning polls, one per subscription backend in use: each logs its own
341
+ // events keyed by its own backend (the `backend:type:resetsAt` keys), so claude's own reserve
342
+ // and warning memory is unaffected by another backend's crossings (part 2e/9).
343
+ for (const [backend, account] of windowAccountReads) {
344
+ states.claudeWindowReserved = pollClaudeWindowReserve(root, states.claudeWindowReserved, account, now, backend, windowReserveFor(liveConfig, backend));
345
+ // The subscription usage window's warning is observational (the hold above is the gate): the
346
+ // first allowed_warning logs once per window, and the memory lives beside the holds map.
347
+ states.claudeWindowWarned = pollClaudeWindowWarning(root, states.claudeWindowWarned, account, now, backend);
348
+ }
349
+ // Team-wide error-storm warning (src/team/error-storm.ts): when several roles' run streaks
350
+ // fail consecutively on one shared cause, each role's own "consecutive run failures"
301
351
  // warning still fires alone — this adds the one team-level warning that names the
302
352
  // cause (and the config knob, when the cause has one) instead of leaving the operator
303
353
  // to diff 14 streak lines. Observational only: it gates nothing, so unlike the failure
@@ -319,11 +369,11 @@ export function pollTeamGates(states, ctx) {
319
369
  capPaused,
320
370
  maintenanceQuota,
321
371
  bootstrapHeld,
322
- bootstrapActive,
323
372
  budgetPausedRoles,
324
373
  servingResolved,
325
374
  priceResolved,
326
375
  budgetActive,
376
+ windowMovedRoles,
327
377
  roleQuietHeld,
328
378
  quietNow,
329
379
  diskHeld,
@@ -2,7 +2,7 @@
2
2
  * setting, and the gate behind the maintenance allowance — code-maintenance merges in a rolling
3
3
  * 24 h window are capped at `maintenancePerWorkMerge × work merges + MAINTENANCE_DAILY_FLOOR`, and
4
4
  * `pollMaintenanceQuotaGate` puts that verdict in front of the scheduler so the enabled maintenance
5
- * loops stop starting new ticks past it.
5
+ * roles stop starting new runs past it.
6
6
  *
7
7
  * The window counts `merged` events by `commitTier` (src/roles/roles.ts): feature/bugfix/director
8
8
  * are the work count, the code-maintenance roles plus readme the maintenance count, and every other
@@ -16,21 +16,21 @@
16
16
  * The verdict is pure and lives here beside the count so the scheduler and every status reader
17
17
  * share one definition; `pollMaintenanceQuotaGate` is the one writer (the edge-triggered events)
18
18
  * and the one reader the scheduler holds from. */
19
- import { sameStatIdentity, statOrNull } from "../files/files.js";
19
+ import { sameStatIdentity, statIdentityOrZero, statOrNull } from "../files/files.js";
20
20
  import { readCompleteLines } from "../files/tail.js";
21
21
  import { eventsLogPath } from "../paths.js";
22
22
  import { readWindowEvents } from "../events/event-window.js";
23
23
  import { eventRole, parseEventLine } from "../events/event-read.js";
24
24
  import { finiteNumber } from "../files/json-object.js";
25
- import { dayKey } from "../text/datetime.js";
25
+ import { dayKey, DAY_MS } from "../text/datetime.js";
26
26
  import { commitTier } from "../roles/roles.js";
27
- import { baseRoleOf } from "../roles/loop-ids.js";
27
+ import { baseRoleOf } from "../roles/runner-ids.js";
28
28
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
29
29
  /** The fixed test-independent part of the allowance: a project whose backlog is empty still gets
30
30
  * this many maintenance merges in a rolling day, so hygiene never stops entirely. */
31
31
  export const MAINTENANCE_DAILY_FLOOR = 12;
32
32
  /** The rolling window length: 24 h in ms. */
33
- const MAINTENANCE_WINDOW_MS = 24 * 60 * 60 * 1000;
33
+ const MAINTENANCE_WINDOW_MS = DAY_MS;
34
34
  /** The pure allowance verdict: `allowance = perWorkMerge × work + MAINTENANCE_DAILY_FLOOR`;
35
35
  * `used = maint + inFlight`; held while `used >= allowance`. No I/O — the counts and the
36
36
  * in-flight figure come from the caller, so the same verdict serves the scheduler and the
@@ -95,13 +95,13 @@ function seed(counter, root, cutoff, stat) {
95
95
  export function newMaintenanceQuotaGateState() {
96
96
  return { counter: newMaintenanceWindowCounter(), prevHeld: false };
97
97
  }
98
- /** Step the gate by one orchestrator poll: count the rolling window, add the maintenance loops in
98
+ /** Step the gate by one orchestrator poll: count the rolling window, add the maintenance roles in
99
99
  * flight, and put 1a's verdict in front of the scheduler. Returns the verdict — `allowance`, `used`
100
- * (merged maintenance plus the loops already in flight), and `held` (`used >= allowance`) — for the
100
+ * (merged maintenance plus the roles already in flight), and `held` (`used >= allowance`) — for the
101
101
  * scheduling pass's per-poll admission: it spends the remaining `allowance - used` headroom one due
102
- * maintenance loop at a time, so two due loops cannot start in the same poll. Logs exactly one
102
+ * maintenance role at a time, so two due roles cannot start in the same poll. Logs exactly one
103
103
  * team-level `maintenance_quota_hold` when the gate goes from open to held and one
104
- * `maintenance_quota_resumed` when it re-opens — never one per loop. The director is never
104
+ * `maintenance_quota_resumed` when it re-opens — never one per role. The director is never
105
105
  * maintenance-tier, so no exemption is needed. */
106
106
  export function pollMaintenanceQuotaGate(root, state, observers, perWorkMerge, now) {
107
107
  const { work, maint } = countMaintenanceWindow(root, state.counter, now);
@@ -150,10 +150,7 @@ export function countMaintenanceWindow(root, counter, now) {
150
150
  // mid-poll (replaced between the stat and the read), so the folding itself is guarded too.
151
151
  const stat = statOrNull(eventsLogPath(root));
152
152
  const readable = stat?.isFile() ? stat : null;
153
- const size = readable?.size ?? 0;
154
- const mtimeMs = readable?.mtimeMs ?? 0;
155
- const dev = readable?.dev ?? 0;
156
- const ino = readable?.ino ?? 0;
153
+ const { size, mtimeMs, dev, ino } = statIdentityOrZero(readable);
157
154
  const unchanged = sameStatIdentity({ dev, ino, mtimeMs, size }, { dev: counter.dev, ino: counter.ino, mtimeMs: counter.mtimeMs, size: counter.offset });
158
155
  if (readable) {
159
156
  try {
@@ -3,7 +3,7 @@
3
3
  * reads, the edge-triggered pause/resume events, and the cross-poll bookkeeping are one concern
4
4
  * beside the other team gates (src/budget/budget.ts's budget gate, src/team/team-hold.ts's team
5
5
  * hold) — the orchestrator owns only the wiring. */
6
- import { isTeamPaused, pausedRoles } from "../team/team-state.js";
6
+ import { standingRolePause, standingTeamPause } from "../team/team-state.js";
7
7
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
8
8
  /** A fresh pause-gate state: nothing seen paused yet, so the first poll of an unpaused team
9
9
  * logs nothing. */
@@ -14,28 +14,46 @@ export function newPauseGateState() {
14
14
  * human intent instead of spend. The marker is persistent state (presence means paused until
15
15
  * `resume` removes it), so one existsSync per cycle reads it fresh. The director is exempt for
16
16
  * the same reason as under the budget gate — a human typing prompts outranks an operator gate
17
- * (queued prompts simply wait in the inbox if full silence is wanted). In-flight ticks finish;
18
- * only NEW ticks are blocked, because the gate sits before isEligible.
17
+ * (queued prompts simply wait in the inbox if full silence is wanted). In-flight runs finish;
18
+ * only NEW runs are blocked, because the gate sits before isEligible.
19
19
  *
20
20
  * Per-role pause (`tumwater pause --role <id>`): the operator pause's narrower sibling — the
21
- * same persistent marker read fresh per cycle, but one named loop instead of the team. Unlike
21
+ * same persistent marker read fresh per cycle, but one named role instead of the team. Unlike
22
22
  * the team pause the director is NOT exempt: the operator named the role deliberately, and its
23
23
  * queued prompts simply wait in the inbox (the same effect the team pause has on the director).
24
- * In-flight ticks finish; only NEW ticks are blocked (the gate sits before isEligible). */
24
+ * In-flight runs finish; only NEW runs are blocked (the gate sits before isEligible).
25
+ * A timed role pause's `role_paused` events carry its shared deadline as `until`:
26
+ * `standingRolePause` returns it only while the set is non-empty and an expired marker reads as
27
+ * empty, so a logged `until` is the live deadline. */
25
28
  export function pollPauseGates(root, state) {
26
- const userPaused = isTeamPaused(root);
29
+ const marker = standingTeamPause(root);
30
+ const userPaused = marker !== null;
27
31
  if (userPaused !== state.prevUserPaused) {
28
- logEventBestEffort(root, { source: TUMWATER_SOURCE, type: userPaused ? "team_paused" : "team_resumed" });
32
+ logEventBestEffort(root, marker
33
+ ? {
34
+ source: TUMWATER_SOURCE,
35
+ type: "team_paused",
36
+ ...(marker.until !== undefined ? { until: marker.until } : {}),
37
+ ...(marker.reason ? { reason: marker.reason } : {}),
38
+ }
39
+ : {
40
+ source: TUMWATER_SOURCE,
41
+ type: "team_resumed",
42
+ ...(state.prevTeamUntil !== undefined && state.prevTeamUntil <= Date.now() ? { expired: true } : {}),
43
+ });
29
44
  state.prevUserPaused = userPaused;
30
45
  }
31
- const pausedRolesNow = pausedRoles(root);
46
+ state.prevTeamUntil = marker?.until;
47
+ const { roles: pausedRolesNow, until: rolesUntil } = standingRolePause(root);
48
+ const rolesExpired = state.prevRolesUntil !== undefined && state.prevRolesUntil <= Date.now();
32
49
  const pausedRolesSet = new Set(pausedRolesNow);
33
50
  for (const r of pausedRolesNow)
34
51
  if (!state.prevPausedRoles.has(r))
35
- logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_paused", role: r });
52
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_paused", role: r, ...(rolesUntil !== undefined ? { until: rolesUntil } : {}) });
36
53
  for (const r of state.prevPausedRoles)
37
54
  if (!pausedRolesSet.has(r))
38
- logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_resumed", role: r });
55
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_resumed", role: r, ...(rolesExpired ? { expired: true } : {}) });
39
56
  state.prevPausedRoles = pausedRolesSet;
57
+ state.prevRolesUntil = rolesUntil;
40
58
  return { userPaused, pausedRoles: pausedRolesSet };
41
59
  }
@@ -1,12 +1,16 @@
1
- // vocabulary: plain
2
1
  /** The repo preconditions' shared wording behind the readiness gate. startup-gate.ts answers
3
- * with the first unmet precondition (cmdRun and requireReadyRepo fail on it, the self-redeploy
2
+ * with the first unmet precondition (cmdStart and requireReadyRepo fail on it, the self-redeploy
4
3
  * refuses a swap on it) and doctor.ts reports each one individually, but every surface
5
4
  * describes the same problems — so the wording lives here once and cannot drift.
6
5
  * `GIT_MISSING_MESSAGE` (git/git-run.ts) is the sibling for a machine with no git binary; the
7
- * agent-binary preconditions (resolveAgentBin, findAgentBinary, piMissingMessage) live beside
6
+ * agent-binary preconditions (resolveBackendBin, findBackendBinary, piMissingMessage) live beside
8
7
  * the spawn in pi/pi-bin.ts. */
9
8
  export const NOT_A_REPO_MESSAGE = "not a git repository (run `git init` first)";
10
9
  export const NO_COMMITS_MESSAGE = "the repo has no commits yet; `tumwater init` creates the first one";
11
10
  export const DETACHED_HEAD_MESSAGE = "your checkout is detached; check out your main branch first";
12
11
  export const NOT_INITIALIZED_MESSAGE = "not initialized (run `tumwater init <prompt>` first — or a bare `tumwater init` when the project brief (TUMWATER.md or README.md) already carries the prompt)";
12
+ /** What a command run inside a team workspace (`<root>/.tumwater/worktrees/<slot>`) says in place
13
+ * of NOT_INITIALIZED_MESSAGE: the workspace is no project of its own, the team root is. */
14
+ export function teamWorkspaceMessage(teamRoot) {
15
+ return `this is a tumwater workspace of the team at ${teamRoot}, not a project — run tumwater from ${teamRoot} (\`cd ${teamRoot}\`)`;
16
+ }
@@ -1,10 +1,10 @@
1
- /** The per-role daily cost cap (PLANS.md 2026-09-30, part 1/2): a loop whose local-day spend
2
- * has reached its own `maxDailyCostUsdPerRole` entry starts no new ticks until the next local
1
+ /** The per-role daily cost cap (PLANS.md 2026-09-30, part 1/2): a role whose local-day spend
2
+ * has reached its own `maxDailyCostUsdPerRole` entry starts no new runs until the next local
3
3
  * day or a live config edit — so one runaway role cannot eat the team-wide cap and starve
4
- * every other loop for the rest of the day. The team budget gate (src/gates/budget-gates.ts)
4
+ * every other role for the rest of the day. The team budget gate (src/gates/budget-gates.ts)
5
5
  * bounds the SUM; this bounds each member. The stateless verdict follows that gate's own rule
6
6
  * (budgetPaused's doc: "resume is stateless, so … crossing midnight flips it on the next cycle
7
- * and nothing can get stuck"): the verdict is recomputed from the loop state every poll, no
7
+ * and nothing can get stuck"): the verdict is recomputed from the runner state every poll, no
8
8
  * marker is written, and local midnight lifts it by itself.
9
9
  *
10
10
  * Deliberate differences from the streak gate's shape (src/gates/streak-gate.ts), stated in
@@ -30,7 +30,7 @@ import { groupBy } from "../collections.js";
30
30
  import { budgetWarning, dailyCost, todayStamp } from "../budget/budget.js";
31
31
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
32
32
  import { DIRECTOR_ROLE } from "../roles/roles.js";
33
- import { baseRoleOf } from "../roles/loop-ids.js";
33
+ import { baseRoleOf } from "../roles/runner-ids.js";
34
34
  /** The stateless verdict: is this role over its own per-role cap as of `now`? True iff a cap
35
35
  * is configured, it is a finite number above zero (0 disables that role's cap, like the team
36
36
  * cap), and today's spend has reached it. The role→cap lookup stays with the caller
@@ -44,9 +44,9 @@ export function roleCapPaused(state, cap, now = Date.now()) {
44
44
  /** The grouped, stateless per-role cap verdict — the single definition the scheduler's gate
45
45
  * (`pollRoleCapGate`) and the snapshot's `capPaused` observer (src/status/status-data.ts) both
46
46
  * fold, so the hold an operator sees cannot diverge from the one the scheduler enforces.
47
- * Instance loop ids share their base role's cap and its SUMMED spend (`feature` and `feature-2`
47
+ * Instance role ids share their base role's cap and its SUMMED spend (`feature` and `feature-2`
48
48
  * at $0.60 each cross a `maxDailyCostUsdPerRole.feature` cap of $1 together), and the director
49
- * is exempt. Keyed by base role in first-appearance order; each entry carries the group's loop
49
+ * is exempt. Keyed by base role in first-appearance order; each entry carries the group's role
50
50
  * ids so a caller can log the base (the gate) or flatten the ids (the observer). */
51
51
  export function roleCapVerdicts(runners, caps, now) {
52
52
  // Group by base role via the shared Map-of-arrays step (collections.groupBy, which
@@ -80,7 +80,7 @@ export function newRoleCapGateState() {
80
80
  export function pollRoleCapGate(root, state, runners, caps, now) {
81
81
  // Grouping and the verdict itself live in roleCapVerdicts, shared with the snapshot's
82
82
  // `capPaused` observer (src/status/status-data.ts) so the two cannot diverge. The returned
83
- // set holds the paused loop ids so the scheduler can keep checking `capPaused.has(runner.role)`
83
+ // set holds the paused role ids so the scheduler can keep checking `capPaused.has(runner.role)`
84
84
  // unchanged.
85
85
  const verdicts = roleCapVerdicts(runners, caps, now);
86
86
  const paused = new Set();