tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -2,16 +2,21 @@
2
2
  * fallback config view it derives, as the orchestrator polls it each cycle. Extracted from
3
3
  * orchestrator.ts's poll loop: the spend check, the fallback breaker's re-key, the
4
4
  * edge-triggered budget_* events, and the fallback view derivation are one concern beside the
5
- * other fleet gates (src/gates/pause-gates.ts, src/fleet/fleet-hold.ts's fleet hold) — the
5
+ * other team gates (src/gates/pause-gates.ts, src/team/team-hold.ts's team hold) — the
6
6
  * orchestrator owns only the wiring (the demotion publish and the per-runner config assignment). */
7
7
  import { MODEL_TIERS } from "../config/config-schema.js";
8
- import { budgetGate, budgetReached, budgetSpend, budgetWarning, modelPairName, } from "../budget/budget.js";
8
+ import { budgetGate, budgetReached, budgetSpend, budgetWarning, modelPairName, pairDailyCost, } from "../budget/budget.js";
9
9
  import { fallbackServingPair, rekeyFallbackBreakers, } from "../budget/fallback-breaker.js";
10
- import { applyFallbackModel, fallbackPair, resolveTierFallbacks, tierFallbackLabels, tiersResolveDistinctPairs, } from "../config/config-views.js";
10
+ import { applyFallbackModel, fallbackPair, resolveTierFallbacks, tierChainSteps, tierFallbackLabels, tiersResolveDistinctPairs, } from "../config/config-fallbacks.js";
11
11
  import { DIRECTOR_ROLE } from "../roles/roles.js";
12
- import { logEventBestEffort } from "../events/events.js";
12
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
13
+ import { enabledRoleIds } from "../config/config.js";
14
+ import { formatModelSelector, seamAgent } from "../config/model-selector.js";
15
+ import { configForRole, isTierName, reviewConfig, roleSeamTier, withModelOverrides } from "../config/config-views.js";
13
16
  import { piPairFree, readPiProviders, seamFree } from "../pi/pi-models.js";
14
- import { readClaudeAccount } from "../agent/claude/claude-account.js";
17
+ import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
18
+ import { claudeWindowUsedUp, readClaudeAccount } from "../agent/claude/claude-account.js";
19
+ import { dayAt } from "../text/datetime.js";
15
20
  /** A fresh budget-gate poll state: gate open, no fallback breakers yet, no fallback view cached
16
21
  * yet (so the first poll of an unchanged config logs nothing). */
17
22
  export function newBudgetGateState(config) {
@@ -20,10 +25,33 @@ export function newBudgetGateState(config) {
20
25
  breakers: {},
21
26
  engaged: null,
22
27
  from: null,
28
+ fromKey: "",
29
+ prevWindowMoved: false,
30
+ directorFrom: null,
31
+ directorView: config,
32
+ directorKey: "",
23
33
  fallbackConfig: config,
24
34
  warned: false,
35
+ prevChain: {},
25
36
  };
26
37
  }
38
+ /** Is this fallback pair a claude one (the agent namespace in the pair's provider)? */
39
+ function pairIsClaude(p) {
40
+ return p.provider === "claude";
41
+ }
42
+ /** Can the account not take extra usage: some window is rejected, or the newest non-reset
43
+ * window's `overageStatus` is `rejected`. */
44
+ function claudeOverageRefused(account) {
45
+ const live = Object.values(account.windows).filter((w) => w.status !== "reset");
46
+ if (live.some((w) => w.status === "rejected"))
47
+ return true;
48
+ const newest = live.reduce((a, w) => (a === null || w.observedAt > a.observedAt ? w : a), null);
49
+ return newest?.overageStatus === "rejected";
50
+ }
51
+ /** Does this model seam route to the claude backend? */
52
+ function seamIsClaude(seam) {
53
+ return seamAgent(seam) === "claude";
54
+ }
27
55
  /** Does an in-flight tick's captured model pair (LoopRunner.tickModel()) match the fallback
28
56
  * pair a reopening budget gate just left? The orchestrator hands back exactly those ticks
29
57
  * (PLANS.md 2026-09-30): both sides must be present — no fallback pair means no tick ran on
@@ -36,16 +64,18 @@ export function tickOnPair(tickModel, pair) {
36
64
  tickModel.provider === pair.provider &&
37
65
  tickModel.model === pair.model);
38
66
  }
39
- /** The config a runner runs on while the budget gate stands: the director keeps the live
40
- * config — an explicit human prompt outranks the autonomous-spend cap — and every other role
41
- * takes the gate's derived view (pollBudgetGate's returned `roleConfig`). Single home for the
42
- * rule, so the scheduler's per-poll assignment (src/gates/gate-polls.ts) and a throwaway landing
43
- * author's construction (src/landing/landing-vetting.ts's resolveAuthor) cannot drift apart —
44
- * say, an exemption granted to one more role in one copy and not the other. */
45
- export function gateRoleConfig(role, liveConfig, roleConfig) {
46
- return role === DIRECTOR_ROLE ? liveConfig : roleConfig;
67
+ /** The config a runner runs on while the budget gate stands: the director keeps the live config —
68
+ * an explicit human prompt outranks the autonomous-spend cap — except while a used-up window has
69
+ * its own subscription claude model unable to run, when it takes `directorConfig` (pollBudgetGate's
70
+ * view: the tier's fallback pair; omitted means the live config). Every other role takes the gate's
71
+ * derived view (pollBudgetGate's returned `roleConfig`). Single home for the rule, so the
72
+ * scheduler's per-poll assignment (src/gates/gate-polls.ts) and a throwaway merge author's
73
+ * construction (src/merge/merge-verify.ts's resolveAuthor) cannot drift apart — say, an exemption
74
+ * granted to one more role in one copy and not the other. */
75
+ export function gateRoleConfig(role, liveConfig, roleConfig, directorConfig = liveConfig) {
76
+ return role === DIRECTOR_ROLE ? directorConfig : roleConfig;
47
77
  }
48
- /** Poll the daily cost budget gate: once the fleet's spend for the local day has reached
78
+ /** Poll the daily cost budget gate: once the team's spend for the local day has reached
49
79
  * maxDailyCostUsd, role loops either switch to the configured cost-free fallback model and
50
80
  * keep working, or — with no usable one — start no new ticks at all (scheduled, main-moved
51
81
  * wake, or startup). The director is outside both: an explicit human prompt outranks the
@@ -66,20 +96,54 @@ export function pollBudgetGate(state, ctx) {
66
96
  const reviewOn = liveConfig.review.enabled;
67
97
  // Whether the fallback is usable is a live question too: models.json is stat-cached
68
98
  // inside pi-models.ts, so an unchanged catalog costs one stat per poll, and an operator
69
- // who fixes a mistyped model id sees the fleet switch over within a cycle. One snapshot
99
+ // who fixes a mistyped model id sees the team switch over within a cycle. One snapshot
70
100
  // serves both the engaged pair's check and the per-tier resolution below.
71
101
  const providers = readPiProviders(modelsPath);
72
102
  // The persisted billing answer (6a/7): a subscription claude seam is free money, so the
73
103
  // predicates below never move it to a fallback and accept a subscription claude pair as a
74
104
  // budget fallback. One read per poll; an absent file reads as "no subscription".
75
105
  const account = readClaudeAccount(root);
106
+ // The cached opencode models catalog (part 6b/7), seeded at the startup gate and read once
107
+ // per poll so every free-ness verdict below prices opencode seams against the same snapshot.
108
+ // Absent (no cache yet), an opencode seam reads NOT free — the safe direction until the
109
+ // startup seed fills the file in.
110
+ const catalog = readOpencodeCache(root);
76
111
  // The usable predicate's two halves (part 5b/8): pi prices the pair at zero — a pair
77
112
  // routed to a non-pi backend (e.g. `claude/<id>`) is never free, the same predicate the
78
113
  // badge uses — and its breaker entry holds no demotion, tiers sharing a pair share the
79
114
  // verdict. The resolution below deliberately prices alone (the map's keys are the pairs
80
115
  // that would run absent a demotion, so a demotion cannot churn them); part 5c/8 wires the
81
116
  // full predicate into the resolution so a demoted pair re-resolves only the tiers using it.
82
- const pairFreeAt = (p) => piPairFree(providers, p.provider, p.model, account);
117
+ // A used-up subscription window (claudeWindowUsedUp, the rule the team hold reads too): while
118
+ // it stands a subscription claude pair is no fallback — it would meet the same window.
119
+ // `claudeWindowReserve` makes a window past its fraction count as used up for the team; the
120
+ // director's own reading (directorWindowOut) ignores it, as it runs the operator's prompts.
121
+ const windowOut = claudeWindowUsedUp(account, now, true, liveConfig.claudeWindowReserve);
122
+ const directorWindowOut = claudeWindowUsedUp(account, now);
123
+ // A priced step (one pi does not price at zero, or claude on API billing) is usable only
124
+ // while the team is under its cap and the step is under its own `maxDailyCostUsd`, so no cap
125
+ // can be passed; at the cap only free steps remain, as before.
126
+ const pairFreeAt = (p) => {
127
+ if (account?.billing === "subscription" && pairIsClaude(p)) {
128
+ // The first Claude model runs on the subscription only; a later one (extraUsage) may
129
+ // spend extra usage within its cap while no window is rejected or refuses overage.
130
+ if (!p.extraUsage)
131
+ return windowOut === null && piPairFree(providers, p.provider, p.model, account, catalog);
132
+ // Past the reserve this step would spend the reserved quota before reaching extra usage:
133
+ // it waits until a reading shows the account already spending overage.
134
+ if (windowOut?.reserve === true && !Object.values(account.windows).some((w) => w.isUsingOverage === true))
135
+ return false;
136
+ return (!reached &&
137
+ !claudeOverageRefused(account) &&
138
+ (p.maxDailyCostUsd === undefined || pairDailyCost(states, modelPairName(p), now) < p.maxDailyCostUsd));
139
+ }
140
+ if (piPairFree(providers, p.provider, p.model, account, catalog))
141
+ return !(windowOut !== null && pairIsClaude(p));
142
+ if (reached)
143
+ return false;
144
+ return (p.maxDailyCostUsd === undefined ||
145
+ pairDailyCost(states, modelPairName(p), now) < p.maxDailyCostUsd);
146
+ };
83
147
  // The breaker map is keyed by every pair the tiers would run on absent a demotion
84
148
  // (resolveTierFallbacks over the price check alone), re-keyed per pair each poll: a pair's
85
149
  // judgment survives while its subject (pair + cap) is unchanged, and a changed cap, a pair
@@ -90,10 +154,10 @@ export function pollBudgetGate(state, ctx) {
90
154
  // runs on it — and the paused tiers the demotion produces are held by the scheduler's
91
155
  // per-role set, so nobody but the probe ever reaches the demoted pair.
92
156
  const priceResolved = resolveTierFallbacks(liveConfig, pairFreeAt);
93
- // The fleet-level engagement is the price-resolved `default` tier's pair: a map with no
157
+ // The team-level engagement is the price-resolved `default` tier's pair: a map with no
94
158
  // `default` entry still reports the free pair the tier borrows, so state.engaged and the
95
159
  // budget_fallback event name what the gate actually engages instead of reading null while
96
- // the fleet runs one. fallbackReady is exactly a non-null resolved default. rejectedPair is
160
+ // the team runs one. fallbackReady is exactly a non-null resolved default. rejectedPair is
97
161
  // the default tier's own declared pair (fallbackPair's borrow order included) — the pair
98
162
  // the operator asked for when the price check refused it, named by the pause event.
99
163
  const pair = priceResolved.default.pair;
@@ -109,12 +173,22 @@ export function pollBudgetGate(state, ctx) {
109
173
  pairNames.push(n);
110
174
  }
111
175
  }
176
+ // Every price-free step of every tier's chain gets a breaker entry too: a demoted step's
177
+ // successor must have one, or fallbackServingPair would read it as not serving.
178
+ const chainPairs = tierChainSteps(liveConfig)
179
+ .map((s) => s.pair)
180
+ .filter(pairFreeAt);
181
+ for (const p of chainPairs) {
182
+ const n = modelPairName(p);
183
+ if (!pairNames.includes(n))
184
+ pairNames.push(n);
185
+ }
112
186
  state.breakers = rekeyFallbackBreakers(state.breakers, pairNames, liveConfig.maxDailyCostUsd);
113
187
  state.engaged = reached && fallbackReady ? pairName : null;
114
188
  // The demotion-aware resolution (part 5c/8): the same engine over the FULL usable predicate —
115
189
  // priced at zero AND its breaker entry serving — so a demoted pair re-resolves only the tiers
116
190
  // that use it: a demoted strong pair holds the strong roles beside the per-role cap set while
117
- // small and default keep their own pairs, instead of the fleet-wide pause the single-pair
191
+ // small and default keep their own pairs, instead of the team-wide pause the single-pair
118
192
  // gate produced.
119
193
  const usable = (p) => pairFreeAt(p) && fallbackServingPair(state.breakers, modelPairName(p));
120
194
  const servingResolved = resolveTierFallbacks(liveConfig, usable);
@@ -132,7 +206,7 @@ export function pollBudgetGate(state, ctx) {
132
206
  // doubles the page on the poll the cap itself is reached.
133
207
  const warn = gate === "open" && budgetWarning({ spentUsd, capUsd });
134
208
  if (warn && !state.warned) {
135
- logEventBestEffort(root, { loop: "harness", type: "budget_warning", spentUsd, capUsd });
209
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "budget_warning", spentUsd, capUsd });
136
210
  state.warned = true;
137
211
  }
138
212
  else if (!warn) {
@@ -140,7 +214,7 @@ export function pollBudgetGate(state, ctx) {
140
214
  }
141
215
  if (gate !== state.prevGate) {
142
216
  logEventBestEffort(root, {
143
- loop: "harness",
217
+ source: TUMWATER_SOURCE,
144
218
  type: gate === "open" ? "budget_resumed" : gate === "fallback" ? "budget_fallback" : "budget_paused",
145
219
  spentUsd,
146
220
  capUsd,
@@ -180,37 +254,179 @@ export function pollBudgetGate(state, ctx) {
180
254
  });
181
255
  state.prevGate = gate;
182
256
  }
183
- // While the fallback holds, role loops run under the derived view — the free pair
184
- // installed top-level and every per-role/reviewer model override dropped, so no seam can
185
- // reach a priced model. A breaker-demoted fallback keeps the view too: its gate is
186
- // `paused`, but a tick parked in the semaphore when it tripped, the half-open probe, and
187
- // the landings must still run on the free pair — a demotion must never promote them to
188
- // the priced model the cap already spent.
257
+ // While the fallback holds, role loops run under the derived view — the free pair installed
258
+ // top-level and every per-role/reviewer model override dropped, so no seam can reach a priced
259
+ // model. A breaker-demoted fallback keeps the view too: its gate is `paused`, but a tick parked
260
+ // in the semaphore when it tripped, the half-open probe, and the merges must still run on the
261
+ // free pair — a demotion must never promote them to the priced model the cap already spent.
189
262
  const onFallback = reached && pairNames.length > 0;
190
- if (onFallback && state.from !== liveConfig) {
263
+ // A used-up window moves the subscription claude jobs to their tier's fallback below the cap
264
+ // too; with no fallback to move to, nothing changes and the claude hold applies.
265
+ const windowMove = windowOut !== null && pairNames.length > 0;
266
+ // The resolved pairs are part of the key: a priced step crossing its own daily cap changes
267
+ // the resolution without a config or window change, and the view must follow it.
268
+ const resolvedKey = MODEL_TIERS.map((t) => {
269
+ const rp = priceResolved[t].pair;
270
+ return rp ? modelPairName(rp) : "-";
271
+ }).join(",");
272
+ const fromKey = `${windowMove ? "w" : ""}${reached ? "c" : ""}|${resolvedKey}`;
273
+ if ((onFallback || windowMove) && (state.from !== liveConfig || state.fromKey !== fromKey)) {
191
274
  state.from = liveConfig;
192
- state.fallbackConfig = applyFallbackModel(liveConfig, priceResolved, (seam) => seamFree(providers, seam, account));
275
+ state.fromKey = fromKey;
276
+ // A job keeps its own model while that model is available: below the cap every seam but a
277
+ // subscription claude one during a used-up window; at the cap it must also be free.
278
+ const available = (seam) => !(windowOut !== null && seamIsClaude(seam)) && (!reached || seamFree(providers, seam, account, catalog));
279
+ const view = applyFallbackModel(liveConfig, priceResolved, available);
280
+ // A role pinned to subscription claude under a tier that is not claude has no tier pair to fall
281
+ // onto — dropping its override would put it on the tier's priced primary — so it keeps its
282
+ // pin and the claude hold stops it, as before the window move.
283
+ if (windowOut !== null) {
284
+ for (const role of enabledRoleIds(liveConfig)) {
285
+ const tier = roleSeamTier(liveConfig, role);
286
+ const pin = liveConfig.roles[role];
287
+ if (pin && seamIsClaude(configForRole(liveConfig, role)) && !seamIsClaude(withModelOverrides(liveConfig, tier, {})))
288
+ view.roles[role] = pin;
289
+ }
290
+ // A reviewer pinned to subscription claude under such a tier would likewise end up on the
291
+ // tier's priced primary; it goes to the tier's fallback pair instead (a held reviewer would
292
+ // stop the whole team).
293
+ const rm = liveConfig.review.model;
294
+ const reviewTier = isTierName(rm) ? rm : "strong";
295
+ const reviewPair = priceResolved[reviewTier].pair;
296
+ if (reviewPair &&
297
+ seamIsClaude(reviewConfig(liveConfig)) &&
298
+ !seamIsClaude(withModelOverrides(liveConfig, reviewTier, {})))
299
+ view.review = {
300
+ ...view.review,
301
+ provider: undefined,
302
+ model: formatModelSelector({
303
+ provider: reviewPair.provider,
304
+ model: reviewPair.model ?? "",
305
+ thinking: reviewPair.thinking,
306
+ }),
307
+ thinking: undefined,
308
+ };
309
+ }
310
+ state.fallbackConfig = view;
311
+ }
312
+ // The tiers the window move put on a fallback pair: those whose own seam is subscription claude.
313
+ const windowTiers = {};
314
+ if (windowMove) {
315
+ const moved = new Set();
316
+ for (const tier of MODEL_TIERS)
317
+ if (seamIsClaude(withModelOverrides(liveConfig, tier, {})))
318
+ moved.add(tier);
319
+ const labels = tierFallbackLabels(priceResolved);
320
+ for (const tier of MODEL_TIERS)
321
+ if (moved.has(tier) && labels[tier] !== undefined)
322
+ windowTiers[tier] = labels[tier];
323
+ }
324
+ // The director follows availability, not the cap: while the window move stands and its own
325
+ // model job is subscription claude, its tier's resolved pair replaces the live config (only
326
+ // subscription claude jobs are unavailable, so a priced director primary keeps its model over
327
+ // the cap). With no fallback (windowMove false) it stays on the live config and the scheduler
328
+ // holds it on the claude hold.
329
+ const directorMoved = directorWindowOut !== null && pairNames.length > 0 && seamIsClaude(configForRole(liveConfig, DIRECTOR_ROLE));
330
+ if (!directorMoved)
331
+ state.directorFrom = null;
332
+ else if (state.directorFrom !== liveConfig || state.directorKey !== resolvedKey) {
333
+ state.directorFrom = liveConfig;
334
+ state.directorKey = resolvedKey;
335
+ state.directorView = applyFallbackModel(liveConfig, priceResolved, (seam) => !seamIsClaude(seam));
336
+ }
337
+ const windowResumed = state.prevWindowMoved && !windowMove;
338
+ state.prevWindowMoved = windowMove;
339
+ const movedTiers = new Set();
340
+ // The tiers `applyFallbackModel` moved: those whose own seam fails the `available` test above.
341
+ for (const tier of MODEL_TIERS) {
342
+ const seam = withModelOverrides(liveConfig, tier, {});
343
+ const out = (windowMove && seamIsClaude(seam)) || (reached && !seamFree(providers, seam, account, catalog));
344
+ if (out)
345
+ movedTiers.add(tier);
193
346
  }
347
+ const chain = chainInfo({
348
+ config: liveConfig,
349
+ resolved: servingResolved,
350
+ moved: movedTiers,
351
+ breakers: state.breakers,
352
+ states,
353
+ now,
354
+ windowOut,
355
+ account,
356
+ isPriced: (p) => !piPairFree(providers, p.provider, p.model, account, catalog),
357
+ });
194
358
  return {
195
359
  gate,
360
+ chain,
196
361
  onFallback,
197
- roleConfig: onFallback ? state.fallbackConfig : liveConfig,
362
+ roleConfig: onFallback || windowMove ? state.fallbackConfig : liveConfig,
198
363
  spentUsd,
199
364
  capUsd,
200
365
  resumed,
201
- pairs: distinctPairs(priceResolved),
366
+ pairs: distinctPairs(priceResolved, chainPairs),
202
367
  priceResolved,
203
368
  servingResolved,
204
369
  budgetActive: reached,
370
+ windowOut,
371
+ windowTiers,
372
+ windowResumed,
373
+ directorConfig: directorMoved ? state.directorView : liveConfig,
205
374
  };
206
375
  }
376
+ /** The `chain` block: for each tier this poll moved off its primary, the step it runs on and
377
+ * why the position before it cannot run (plans/fallback-chain.md "Events and surfaces").
378
+ * `moved` names the tiers the poll moved; a tier the gate pauses (`pair: null`) gets no entry.
379
+ * The reason is read from the position just before the active step — the previous chain step,
380
+ * or the tier's primary at step 0 — in the order demoted, step-cap, window/reserve, overage,
381
+ * cap. */
382
+ export function chainInfo(a) {
383
+ const out = {};
384
+ const steps = tierChainSteps(a.config);
385
+ const midnight = dayAt(-1, new Date(a.now)).getTime();
386
+ for (const tier of MODEL_TIERS) {
387
+ const r = a.resolved[tier];
388
+ if (!a.moved.has(tier) || r.pair === null)
389
+ continue;
390
+ const own = steps.filter((s) => s.tier === (r.from ?? tier)).map((s) => s.pair);
391
+ const idx = r.step ?? 0;
392
+ const prior = idx > 0 ? own[idx - 1] : null;
393
+ const primaryClaude = seamIsClaude(withModelOverrides(a.config, tier, {}));
394
+ const info = { pair: modelPairName(r.pair), step: idx + 1, of: own.length, reason: "cap" };
395
+ const priorName = prior ? modelPairName(prior) : "";
396
+ if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
397
+ info.reason = "demoted";
398
+ }
399
+ else if (prior?.maxDailyCostUsd !== undefined &&
400
+ pairDailyCost(a.states, priorName, a.now) >= prior.maxDailyCostUsd) {
401
+ info.reason = "step-cap";
402
+ info.returnsAt = midnight;
403
+ }
404
+ else if ((prior !== null && pairIsClaude(prior) || primaryClaude) && a.windowOut !== null) {
405
+ info.reason = a.windowOut.reserve ? "reserve" : "window";
406
+ info.returnsAt = a.windowOut.resetsAt;
407
+ }
408
+ else if (prior && pairIsClaude(prior) && prior.extraUsage && a.account && claudeOverageRefused(a.account)) {
409
+ info.reason = "overage";
410
+ }
411
+ else {
412
+ info.returnsAt = midnight;
413
+ }
414
+ if (r.pair.maxDailyCostUsd !== undefined && a.isPriced(r.pair)) {
415
+ info.spentUsd = pairDailyCost(a.states, modelPairName(r.pair), a.now);
416
+ info.capUsd = r.pair.maxDailyCostUsd;
417
+ }
418
+ out[tier] = info;
419
+ }
420
+ return out;
421
+ }
207
422
  /** The distinct non-null fallback pairs a resolution hands out (tier order, first occurrence
208
423
  * first) — the handback match list: a reopen hands back every in-flight tick running on any of
209
424
  * them (part 5c/8's per-tier handback). */
210
- function distinctPairs(resolved) {
425
+ function distinctPairs(resolved, chainPairs) {
211
426
  const out = [];
212
- for (const tier of MODEL_TIERS) {
213
- const p = resolved[tier].pair;
427
+ // The resolved pairs first, then every other price-free chain step: a run can be on any step.
428
+ const candidates = [...MODEL_TIERS.map((t) => resolved[t].pair), ...chainPairs];
429
+ for (const p of candidates) {
214
430
  if (p !== null && !out.some((q) => modelPairName(q) === modelPairName(p)))
215
431
  out.push(p);
216
432
  }
@@ -1,33 +1,33 @@
1
1
  /** The disk floor's hold (plans/disk-floor.md, "Disk floor 1/4"): when the volume the harness
2
- * worktrees live on runs low on free space, the fleet starts no new work — role ticks, the
3
- * director, landing vets and merges — so a fleet of build-heavy checkouts cannot fill the
4
- * disk. The syscall, the hold's hysteresis, and the edge-triggered `disk_low`/`disk_ok`
5
- * events live here; the orchestrator owns only the wiring, exactly like the quiet-hours and
6
- * pause gates. Part 2/4 reclaims build outputs so the hold rarely engages; this part turns
7
- * "disk full" into "fleet held, with a notification". In-flight work runs on — killing it
8
- * wastes the work without freeing anything a finished run would not free anyway.
2
+ * worktrees live on runs low on free space, the team starts no new work — role ticks, the director,
3
+ * merge verifies and merges — so a team of build-heavy checkouts cannot fill the disk. The syscall,
4
+ * the hold's hysteresis, and the edge-triggered `disk_low`/`disk_ok` events live here; the
5
+ * orchestrator owns only the wiring, exactly like the quiet-hours and pause gates. Part 2/4
6
+ * reclaims build outputs so the hold rarely engages; this part turns "disk full" into "team held,
7
+ * with a notification". In-flight work runs on — killing it wastes the work without freeing
8
+ * anything a finished run would not free anyway.
9
9
  *
10
10
  * The measurement is deliberately language-agnostic: `fs.statfsSync` reports the bytes an
11
11
  * unprivileged user can still write, whatever the project builds. */
12
12
  import fs from "node:fs";
13
13
  import { worktreesDir } from "../paths.js";
14
- import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
14
+ import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
15
15
  /** GB means 10^9 bytes everywhere — in config and in display (plans/disk-floor.md). Exported
16
16
  * so doctor's detail renders the same unit the gate holds on. */
17
17
  export const BYTES_PER_GB = 1_000_000_000;
18
18
  /** How far free space must climb back ABOVE `diskHoldGB` before the hold lifts. Without it a
19
- * fleet hovering at the line would flap in and out of the hold every poll. */
19
+ * team hovering at the line would flap in and out of the hold every poll. */
20
20
  export const DISK_HOLD_HYSTERESIS_GB = 5;
21
- /** The volume the fleet's builds write to: the worktrees dir once it exists, else the root
22
- * (a fleet that has not created a worktree yet measures the checkout's own volume). One home
21
+ /** The volume the team's builds write to: the worktrees dir once it exists, else the root
22
+ * (a team that has not created a worktree yet measures the checkout's own volume). One home
23
23
  * of the choice so the sampler and doctor's detail name the same path. */
24
24
  export function diskVolumePath(root) {
25
25
  const worktrees = worktreesDir(root);
26
26
  return fs.existsSync(worktrees) ? worktrees : root;
27
27
  }
28
- /** Free bytes available to an unprivileged user on the volume the fleet builds on, or null
28
+ /** Free bytes available to an unprivileged user on the volume the team builds on, or null
29
29
  * when statfs throws (an unsupported platform or filesystem). A null sample never holds —
30
- * the fleet keeps running rather than freezing on a measurement it cannot make. */
30
+ * the team keeps running rather than freezing on a measurement it cannot make. */
31
31
  export function sampleFreeBytes(root, statfs = (p) => fs.statfsSync(p)) {
32
32
  try {
33
33
  const stats = statfs(diskVolumePath(root));
@@ -37,14 +37,14 @@ export function sampleFreeBytes(root, statfs = (p) => fs.statfsSync(p)) {
37
37
  return null;
38
38
  }
39
39
  }
40
- /** A fresh gate state: not held, warning unspent — so the first poll of a fleet with room
40
+ /** A fresh gate state: not held, warning unspent — so the first poll of a team with room
41
41
  * logs nothing. */
42
42
  export function newDiskGateState() {
43
43
  return { prevHeld: false, warnedUnmeasurable: false };
44
44
  }
45
45
  /** Poll the disk gate for one orchestrator cycle: with `freeBytes` in hand, decide the hold
46
46
  * (edge-triggered with `DISK_HOLD_HYSTERESIS_GB` of hysteresis) and log exactly one
47
- * `disk_low` / `disk_ok` event per crossing. `holdGB` 0 disables the hold outright: a fleet
47
+ * `disk_low` / `disk_ok` event per crossing. `holdGB` 0 disables the hold outright: a team
48
48
  * with the key set to 0 never holds, and a live edit to 0 lifts an active hold on the next
49
49
  * poll (with its one `disk_ok`). A null sample (statfs threw) logs one `warning` per process
50
50
  * and never holds. `waitForReclaim` defers ENTERING the hold while part 2/4's pressure reclaim
@@ -55,7 +55,7 @@ export function pollDiskGate(root, freeBytes, holdGB, state, waitForReclaim = fa
55
55
  if (freeBytes === null) {
56
56
  if (!state.warnedUnmeasurable) {
57
57
  state.warnedUnmeasurable = true;
58
- warnEventBestEffort(root, "harness", "cannot measure free disk space (statfs failed) — the disk hold is off for this process");
58
+ warnEventBestEffort(root, TUMWATER_SOURCE, "cannot measure free disk space (statfs failed) — the disk hold is off for this process");
59
59
  }
60
60
  state.prevHeld = false;
61
61
  return false;
@@ -71,8 +71,8 @@ export function pollDiskGate(root, freeBytes, holdGB, state, waitForReclaim = fa
71
71
  if (held !== state.prevHeld) {
72
72
  state.prevHeld = held;
73
73
  logEventBestEffort(root, held
74
- ? { loop: "harness", type: "disk_low", freeGB, holdGB }
75
- : { loop: "harness", type: "disk_ok", freeGB });
74
+ ? { source: TUMWATER_SOURCE, type: "disk_low", freeGB, holdGB }
75
+ : { source: TUMWATER_SOURCE, type: "disk_ok", freeGB });
76
76
  }
77
77
  return held;
78
78
  }