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
@@ -7,20 +7,22 @@ import { newMaintenanceQuotaGateState, pollMaintenanceQuotaGate, } from "./maint
7
7
  import { newBootstrapGateState, pollBootstrapGate, } from "./bootstrap-gates.js";
8
8
  import { newQuietHoursGateState, pollQuietHoursGate, roleQuietHold, } from "../scheduling/quiet-hours.js";
9
9
  import { BYTES_PER_GB, newDiskGateState, pollDiskGate, sampleFreeBytes, } from "./disk-gate.js";
10
- import { pollErrorStorm, pollFailureSpread, pollFleetHold, pollClaudeWindowWarning } from "../fleet/fleet-polls.js";
10
+ import { pollErrorStorm, pollFailureSpread, pollTeamHold, pollClaudeWindowWarning, pollClaudeWindowReserve } from "../team/team-polls.js";
11
11
  import { readClaudeAccount } from "../agent/claude/claude-account.js";
12
- import { ERROR_STORM_QUIET } from "../fleet/error-storm.js";
13
- import { FAILURE_SPREAD_QUIET } from "../fleet/failure-spread.js";
12
+ import { standingHolds } from "../team/team-hold.js";
13
+ import { ERROR_STORM_QUIET } from "../team/error-storm.js";
14
+ import { FAILURE_SPREAD_QUIET } from "../team/failure-spread.js";
14
15
  import { DIRECTOR_ROLE } from "../roles/roles.js";
15
- import { baseRoleOf, loopEnabled } from "../roles/loop-ids.js";
16
- import { queuedLandingRoles } from "../landing/landing-queue.js";
17
- import { logEventBestEffort } from "../events/events.js";
18
- import { assignInfoFieldIfChanged, writeOrchestratorInfoBestEffort } from "../fleet/orchestrator-info.js";
16
+ import { baseRoleOf, instancePauseKey, loopEnabled } from "../roles/loop-ids.js";
17
+ import { queuedMergeRoles } from "../merge/merge-queue.js";
18
+ import { MODEL_TIERS } from "../config/config-schema.js";
19
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
20
+ import { publishInfoFields } from "../team/orchestrator-info.js";
19
21
  import { roleSeamTier } from "../config/config-views.js";
20
- /** A fresh gate family: nothing seen yet, so the first poll of an unpaused, unheld fleet
22
+ /** A fresh gate family: nothing seen yet, so the first poll of an unpaused, unheld team
21
23
  * logs nothing. The budget gate seeds its breaker and derived fallback view from the
22
24
  * startup config; a live reload replaces the config on the next poll. */
23
- export function newFleetGateStates(config) {
25
+ export function newTeamGateStates(config) {
24
26
  return {
25
27
  budget: newBudgetGateState(config),
26
28
  pause: newPauseGateState(),
@@ -30,20 +32,21 @@ export function newFleetGateStates(config) {
30
32
  bootstrap: newBootstrapGateState(),
31
33
  quiet: newQuietHoursGateState(),
32
34
  disk: newDiskGateState(),
33
- fleetHold: new Map(),
35
+ teamHold: new Map(),
34
36
  claudeWindowWarned: null,
37
+ claudeWindowReserved: null,
35
38
  errorStorm: ERROR_STORM_QUIET,
36
39
  failureSpread: FAILURE_SPREAD_QUIET,
37
40
  };
38
41
  }
39
- /** Poll every fleet-wide gate and alarm, advancing `states` in place. */
40
- export function pollFleetGates(states, ctx) {
42
+ /** Poll every team-wide gate and alarm, advancing `states` in place. */
43
+ export function pollTeamGates(states, ctx) {
41
44
  const { root, runners, liveConfig, modelsPath, now, info } = ctx;
42
45
  // Daily cost budget gate (src/gates/budget-gates.ts owns the reads, the edge-triggered
43
46
  // budget_* events, the breaker re-key, and the fallback config view): the orchestrator
44
47
  // owns only the wiring — the demotion publish below and the per-runner assignment at
45
48
  // the bottom of this block.
46
- const { gate, roleConfig, spentUsd, capUsd, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, } = pollBudgetGate(states.budget, {
49
+ const { gate, roleConfig, spentUsd, capUsd, onFallback, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, windowTiers, windowResumed, directorConfig, chain, } = pollBudgetGate(states.budget, {
47
50
  root,
48
51
  states: runners.map((r) => r.state),
49
52
  liveConfig,
@@ -66,36 +69,68 @@ export function pollFleetGates(states, ctx) {
66
69
  }
67
70
  const engagedDemotion = states.budget.engaged !== null ? demotions[states.budget.engaged] : undefined;
68
71
  const budget = { spentUsd, capUsd };
69
- const demotionsChanged = assignInfoFieldIfChanged(info, "fallbackDemotions", demotions);
70
- const engagedChanged = assignInfoFieldIfChanged(info, "fallbackDemoted", engagedDemotion);
71
- const budgetChanged = assignInfoFieldIfChanged(info, "budget", budget);
72
- if (demotionsChanged || engagedChanged || budgetChanged)
73
- writeOrchestratorInfoBestEffort(root, info);
72
+ publishInfoFields(root, info, [
73
+ ["fallbackDemotions", demotions],
74
+ ["fallbackDemoted", engagedDemotion],
75
+ ["budget", budget],
76
+ ["chain", Object.keys(chain).length > 0 ? chain : undefined],
77
+ ]);
78
+ // One fallback_switched per poll in which a tier's pair or step changed (a return to the
79
+ // primary reads `to: null`, reason "available").
80
+ const switched = {};
81
+ const prevChain = states.budget.prevChain;
82
+ for (const tier of MODEL_TIERS) {
83
+ const was = prevChain[tier];
84
+ const cur = chain[tier];
85
+ if (was?.pair === cur?.pair && was?.step === cur?.step)
86
+ continue;
87
+ switched[tier] = { from: was?.pair ?? null, to: cur?.pair ?? null, reason: cur?.reason ?? "available" };
88
+ }
89
+ if (Object.keys(switched).length > 0)
90
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "fallback_switched", tiers: switched });
91
+ states.budget.prevChain = chain;
74
92
  // The director keeps the live config — an explicit human prompt outranks the
75
93
  // autonomous-spend cap (gateRoleConfig). Assigned every poll (not only on transitions)
76
94
  // so a runner created mid-gate, or one left behind by a broken-file poll that skipped
77
95
  // the reload, can never tick on the wrong model.
78
- for (const r of runners)
79
- r.config = gateRoleConfig(r.role, liveConfig, roleConfig);
80
- // The budget reopened: a tick that started on a fallback pair keeps it until it ends, so
81
- // hand those ticks back — abort them resumably (session and worktree edits kept) and
82
- // let their next tick continue the same session on the primary, whose config the
83
- // assignment above already installed (PLANS.md 2026-09-30). Per-tier fallbacks put
84
- // different roles on different pairs (part 5c/8), so the match runs over EVERY pair the
85
- // price-based resolution hands out, not only one. A tick started on the primary keeps
86
- // running, the director is exempt, and the landings (the slot's own runs) are untouched.
87
- // One event per pair names who was handed back, so the resulting aborted ticks read as
88
- // the budget reopening, not as unexplained failures.
89
- if (gateResumed && fallbackPairs.length > 0) {
96
+ for (const r of runners) {
97
+ r.config = gateRoleConfig(r.role, liveConfig, roleConfig, directorConfig);
98
+ // An image reroute must never spend past the daily cap: while the gate has the team on
99
+ // its free fallback pairs, a reroute looks only among those pairs (plans/image-input.md
100
+ // "Reroute target" "Regime"). Set beside the config assignment so the scheduler's regime
101
+ // and the loops' never disagree.
102
+ r.budgetFallbackActive = onFallback;
103
+ }
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
107
+ // installed (PLANS.md 2026-09-30). Per-tier fallbacks put different roles on different pairs
108
+ // (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
110
+ // 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
112
+ // window reset (below the cap): the same hand-back for the pairs the window move used. A window
113
+ // that resets while the cap still holds hands back nothing.
114
+ const handBack = gateResumed || (windowResumed && !budgetActive);
115
+ if (handBack && fallbackPairs.length > 0) {
116
+ // A cap reopen while the window is still used up leaves the window's tiers on their
117
+ // fallback pairs: handing those runs back would abort them to restart on the same pair.
118
+ const stillMoved = Object.keys(windowTiers).flatMap((t) => {
119
+ const p = priceResolved[t].pair;
120
+ return p ? [p] : [];
121
+ });
90
122
  for (const leftPair of fallbackPairs) {
123
+ if (stillMoved.some((p) => p.provider === leftPair.provider && p.model === leftPair.model))
124
+ continue;
91
125
  const matching = runners.filter((r) => r.role !== DIRECTOR_ROLE && tickOnPair(r.tickModel(), leftPair));
92
126
  if (matching.length > 0) {
93
127
  logEventBestEffort(root, {
94
- loop: "harness",
128
+ source: TUMWATER_SOURCE,
95
129
  type: "budget_handback",
96
130
  roles: matching.map((r) => r.role),
97
131
  provider: leftPair.provider,
98
132
  model: leftPair.model,
133
+ ...(gateResumed ? {} : { reason: "window" }),
99
134
  });
100
135
  for (const r of matching)
101
136
  r.handBackTick();
@@ -103,8 +138,8 @@ export function pollFleetGates(states, ctx) {
103
138
  }
104
139
  }
105
140
  // The pause gates (src/gates/pause-gates.ts owns the reads, the edge-triggered pause/resume
106
- // events, and the cross-poll bookkeeping): the operator pause's marker and the per-role
107
- // pause's set, both read fresh per cycle so a marker change lands on the next poll.
141
+ // events, and the cross-poll bookkeeping): the operator pause's marker and the per-role pause's
142
+ // set, both read fresh per cycle so a marker change takes effect on the next poll.
108
143
  const { userPaused, pausedRoles } = pollPauseGates(root, states.pause);
109
144
  // The error-streak circuit breaker (src/gates/streak-gate.ts): a role past ERROR_STREAK_BREAKER
110
145
  // consecutive failed ticks is paused through the same per-role marker the operator's
@@ -112,7 +147,7 @@ export function pollFleetGates(states, ctx) {
112
147
  // director is not exempt; a failing director cannot process prompts anyway. Roles it just
113
148
  // paused fold into this poll's paused-roles view, so the scheduler blocks them on this very
114
149
  // poll instead of the next one.
115
- const streakPaused = pollStreakGate(root, states.streak, runners, pausedRoles);
150
+ const streakPaused = pollStreakGate(root, states.streak, runners, pausedRoles, (role) => instancePauseKey(liveConfig, role));
116
151
  // pollPauseGates advanced its edge-triggered bookkeeping BEFORE the breaker wrote its
117
152
  // marker, so without this fold the NEXT poll would log a generic role_paused for a pause
118
153
  // the breaker already announced as role_streak_paused — one pause, two events, the second
@@ -143,21 +178,20 @@ export function pollFleetGates(states, ctx) {
143
178
  // pause gates' edge bookkeeping never sees it. A live config edit applies on the next poll;
144
179
  // local midnight lifts the verdict by itself.
145
180
  const capPaused = pollRoleCapGate(root, states.cap, runners, liveConfig.maxDailyCostUsdPerRole, now);
146
- // The maintenance allowance (plans/work-ratio.md, part 1b/4): code-maintenance landings in a
147
- // rolling 24 h window are capped at `maintenancePerWorkLanding × work +
148
- // MAINTENANCE_DAILY_FLOOR`.
181
+ // The maintenance allowance (plans/work-ratio.md, part 1b/4): code-maintenance merges in a
182
+ // rolling 24 h window are capped at `maintenancePerWorkMerge × work + MAINTENANCE_DAILY_FLOOR`.
149
183
  // The gate counts the window, adds the maintenance loops already in flight (running a tick or
150
- // holding a queued landing, so six concurrent permits cannot overshoot), and logs the one
184
+ // holding a queued merge, so six concurrent permits cannot overshoot), and logs the one
151
185
  // hold/resume event per transition. Enabled only: a disabled loop runs no tick to count. The
152
186
  // scheduling pass spends the returned headroom one due maintenance loop at a time, beside
153
187
  // capPaused, with the fresh-wake/queued-prompt override the need-based deferral honors.
154
- const queuedRoles = queuedLandingRoles(root);
188
+ const queuedRoles = queuedMergeRoles(root);
155
189
  const maintenanceQuota = pollMaintenanceQuotaGate(root, states.quota, runners.map((r) => ({
156
190
  role: r.role,
157
191
  enabled: loopEnabled(r.config, r.role),
158
192
  running: r.state.running === true,
159
- hasQueuedLanding: queuedRoles.has(r.role),
160
- })), liveConfig.maintenancePerWorkLanding, now);
193
+ hasQueuedMerge: queuedRoles.has(r.role),
194
+ })), liveConfig.maintenancePerWorkMerge, now);
161
195
  // New-project bootstrap (src/gates/bootstrap-gates.ts, plans/work-ratio.md part 2/2): a fresh
162
196
  // project's maintenance loops start no new ticks until `bootstrap.untilPlansDone` plans are
163
197
  // Done. The gate owns the latch write and the one `bootstrap_complete` event; the scheduling
@@ -166,7 +200,7 @@ export function pollFleetGates(states, ctx) {
166
200
  const bootstrapHeld = pollBootstrapGate(root, states.bootstrap, runners, liveConfig, now);
167
201
  const bootstrapActive = states.bootstrap.active;
168
202
  // Per-role quiet hours (src/scheduling/quiet-hours.ts): a role whose own quietHoursPerRole window
169
- // covers `now` starts no new ticks — the fleet window's semantics scoped to one loop, the
203
+ // covers `now` starts no new ticks — the team window's semantics scoped to one loop, the
170
204
  // director exempt like every autonomous gate. Stateless, recomputed per poll exactly like
171
205
  // capPaused: no crossing events, no state — a live config edit applies on the next poll
172
206
  // by construction. A role held by BOTH windows is held once: the scheduling pass folds
@@ -209,8 +243,7 @@ export function pollFleetGates(states, ctx) {
209
243
  held: diskHeld,
210
244
  ...(ctx.reclaim?.lastReclaim ? { lastReclaim: ctx.reclaim.lastReclaim } : {}),
211
245
  };
212
- if (assignInfoFieldIfChanged(info, "disk", disk))
213
- writeOrchestratorInfoBestEffort(root, info);
246
+ publishInfoFields(root, info, [["disk", disk]]);
214
247
  // Each runner as the two provider-failure polls read it (HoldInputs): the role, its two
215
248
  // episodic fields, and the provider its NEXT tick will run on (runProvider — the tier
216
249
  // fallback pair while a model-fallback episode is active, else the role's resolved config;
@@ -218,54 +251,69 @@ export function pollFleetGates(states, ctx) {
218
251
  // read the same list, like holdObservations on the reducer side. Keying on the effective
219
252
  // provider is what lets a storm on the fallback pair form its own hold while the abandoned
220
253
  // primary stays clear (PLANS.md "Model failure fallback, part 1/2").
254
+ //
255
+ // 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
257
+ // the pi author's provider — and the polls' observation mapper files each stamp under that
258
+ // key, falling back to runProvider here for a stamp without one (PLANS.md "Mixed claude +
259
+ // pi fleets, part 2/4"). The spreads below carry the stamps wholesale, provider included.
221
260
  const holdInputs = (rs) => rs.map((r) => ({
222
261
  role: r.role,
223
262
  provider: r.runProvider(now),
224
263
  ...(r.lastRateLimit ? { lastRateLimit: r.lastRateLimit } : {}),
225
264
  ...(r.lastBackendFailure ? { lastBackendFailure: r.lastBackendFailure } : {}),
226
265
  }));
227
- // Fleet-wide failure hold, per provider (src/fleet/fleet-hold.ts, PLANS.md 2026-10-05): once two
228
- // roles' runs have ended on the SAME provider AND failure kind within a short window —
229
- // 429s, or a connection, timeout, 5xx, or model-load backend failure — role loops on THAT
230
- // provider start no new ticks, and the land queue starts no new vet, whose reviewer run
231
- // has no retry and would spend a review strike on the storm — until the hold re-opens at
232
- // its own deadline (Retry-After honored on the rate-limit kind, doubling on a relapse,
233
- // capped). Roles on a healthy provider keep ticking; a hold on the reviewer's provider
234
- // (with the review gate on) still blocks everything, since nothing could land — the
235
- // orchestrator's scheduling and start passes derive that verdict from this map. The
236
- // director's ticks are exempt, as under the budget gate and the operator pause: an
237
- // explicit human prompt outranks an autonomous gate, one director run is not the
238
- // concurrency that sustains a storm, its 429 runs keep the per-run transient retry
239
- // (backend-failure kinds ride this hold alone), and a prompt its tick fails to fulfill
240
- // goes back to the inbox. In-flight ticks finish; NEW ticks are gated at scheduling like
241
- // both siblings, and a role tick already parked in the semaphore meets the same hold at
242
- // its permit (the start gate) and hands its reservation back instead of starting
243
- // into the storm. The verdicts are written back to states.fleetHold — one hold per
244
- // provider, lifted holds kept keyed for their relapse memory but never reading as held —
245
- // and the permit-time closures read the LATEST poll's map there, so a waiter granted its
246
- // permit after a later poll sees that poll's world.
266
+ // Team-wide failure hold, per provider (src/team/team-hold.ts, PLANS.md 2026-10-05): once two
267
+ // 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
270
+ // spend a review strike on the storm — until the hold re-opens at its own deadline (Retry-After
271
+ // 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
273
+ // 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
275
+ // operator pause: an explicit human prompt outranks an autonomous gate, one director run is not
276
+ // 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)
280
+ // and hands its reservation back instead of starting into the storm. The verdicts are written
281
+ // back to states.teamHold — one hold per provider, lifted holds kept keyed for their relapse
282
+ // memory but never reading as held — and the permit-time closures read the LATEST poll's map
283
+ // there, so a waiter granted its permit after a later poll sees that poll's world.
247
284
  const claudeAccount = readClaudeAccount(root, now);
248
- states.fleetHold = pollFleetHold(root, states.fleetHold, holdInputs(runners), now, claudeAccount);
285
+ states.teamHold = pollTeamHold(root, states.teamHold, holdInputs(runners), now, claudeAccount, windowTiers, liveConfig.claudeWindowReserve);
286
+ // The standings hold, published for observers (PLANS.md 2026-10-17 "the dashboards and
287
+ // `tumwater status` name a standing provider failure hold"): `standingHolds` turns the holds
288
+ // map into the per-provider list the status snapshot and the alert banner name, the same
289
+ // omit-when-empty idiom as `chain`/`disk`, so an open team leaves the file untouched and a
290
+ // hold change writes once.
291
+ const providerHolds = standingHolds(states.teamHold);
292
+ publishInfoFields(root, info, [
293
+ ["providerHolds", providerHolds.length > 0 ? providerHolds : undefined],
294
+ ]);
295
+ states.claudeWindowReserved = pollClaudeWindowReserve(root, states.claudeWindowReserved, claudeAccount, now, liveConfig.claudeWindowReserve);
249
296
  // The subscription usage window's warning is observational (the hold above is the gate): the
250
297
  // first allowed_warning logs once per window, and the memory lives beside the holds map.
251
298
  states.claudeWindowWarned = pollClaudeWindowWarning(root, states.claudeWindowWarned, claudeAccount, now);
252
- // Fleet-wide error-storm warning (src/fleet/error-storm.ts): when several roles' tick streaks
299
+ // Team-wide error-storm warning (src/team/error-storm.ts): when several roles' tick streaks
253
300
  // fail consecutively on one shared cause, each role's own "consecutive tick failures"
254
- // warning still fires alone — this adds the one fleet-level warning that names the
301
+ // warning still fires alone — this adds the one team-level warning that names the
255
302
  // cause (and the config knob, when the cause has one) instead of leaving the operator
256
303
  // to diff 14 streak lines. Observational only: it gates nothing, so unlike the failure
257
304
  // hold above there is no re-open event — the members' own recoveries tell that story.
258
305
  states.errorStorm = pollErrorStorm(root, states.errorStorm, runners);
259
- // Fleet-wide wide-shallow storm alarm (src/fleet/failure-spread.ts): when many roles each
306
+ // Team-wide wide-shallow storm alarm (src/team/failure-spread.ts): when many roles each
260
307
  // fail a few times on one provider failure kind, the streak bar needs one role deep,
261
308
  // the error storm needs several roles deep, and the hold needs the failures close
262
309
  // together — this counts raw failures of one kind across roles in a rolling window,
263
- // so a degraded backend that fails the fleet widely and shallowly still names itself.
310
+ // so a degraded backend that fails the team widely and shallowly still names itself.
264
311
  // Observational only, like the error storm: it gates nothing.
265
312
  states.failureSpread = pollFailureSpread(root, states.failureSpread, holdInputs(runners), now);
266
313
  return {
267
314
  gate,
268
315
  roleConfig,
316
+ directorConfig,
269
317
  userPaused,
270
318
  pausedRoles: pausedRolesNow,
271
319
  capPaused,
@@ -1,17 +1,17 @@
1
- /** Work ratio 1a/4 and 1b/4 (plans/work-ratio.md, "Maintenance follows work"): the arithmetic,
2
- * the setting, and the gate behind the maintenance allowance — code-maintenance landings in a
3
- * rolling 24 h window are capped at `maintenancePerWorkLanding × work landings +
4
- * MAINTENANCE_DAILY_FLOOR`, and `pollMaintenanceQuotaGate` puts that verdict in front of the
5
- * scheduler so the enabled maintenance loops stop starting new ticks past it.
1
+ /** Work ratio 1a/4 and 1b/4 (plans/work-ratio.md, "Maintenance follows work"): the arithmetic, the
2
+ * setting, and the gate behind the maintenance allowance — code-maintenance merges in a rolling
3
+ * 24 h window are capped at `maintenancePerWorkMerge × work merges + MAINTENANCE_DAILY_FLOOR`, and
4
+ * `pollMaintenanceQuotaGate` puts that verdict in front of the scheduler so the enabled maintenance
5
+ * loops stop starting new ticks 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
9
9
  * role (plan, steward, observers, custom) is neither. The count is an instant-shaped trailing
10
- * window — `ts >= now − 24 h` — not a local-day bucket, so it cannot reuse the report's
11
- * day-keyed fold cache (src/report/report-data.ts): instead the counter folds only the bytes the
12
- * append-only live log gained since the previous poll (`readCompleteLines`) and keeps the parsed
13
- * merged records in memory, re-reading the window whole only on the first call or after rotation.
14
- * Both readers share `parseEventLine`, so there is no second line parser.
10
+ * window — `ts >= now − 24 h` — not a local-day bucket, so it cannot reuse the report's day-keyed
11
+ * fold cache (src/report/report-fold.ts): instead the counter folds only the bytes the append-only
12
+ * live log gained since the previous poll (`readCompleteLines`) and keeps the parsed merged records
13
+ * in memory, re-reading the window whole only on the first call or after rotation. Both readers
14
+ * share `parseEventLine`, so there is no second line parser.
15
15
  *
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)
@@ -25,18 +25,18 @@ import { finiteNumber } from "../files/json-object.js";
25
25
  import { dayKey } from "../text/datetime.js";
26
26
  import { commitTier } from "../roles/roles.js";
27
27
  import { baseRoleOf } from "../roles/loop-ids.js";
28
- import { logEventBestEffort } from "../events/events.js";
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
- * this many maintenance landings in a rolling day, so hygiene never stops entirely. */
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
33
  const MAINTENANCE_WINDOW_MS = 24 * 60 * 60 * 1000;
34
- /** The pure allowance verdict: `allowance = perWorkLanding × work + MAINTENANCE_DAILY_FLOOR`;
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
37
37
  * dashboards. */
38
38
  export function maintenanceQuota(input) {
39
- const allowance = input.perWorkLanding * input.work + MAINTENANCE_DAILY_FLOOR;
39
+ const allowance = input.perWorkMerge * input.work + MAINTENANCE_DAILY_FLOOR;
40
40
  const used = input.maint + input.inFlight;
41
41
  return { allowance, used, held: used >= allowance };
42
42
  }
@@ -95,15 +95,15 @@ 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
99
- * in flight, and put 1a's verdict in front of the scheduler. Returns the verdict — `allowance`,
100
- * `used` (landed maintenance plus the loops already in flight), and `held` (`used >= allowance`)
101
- * — for the scheduling pass's per-poll admission: it spends the remaining `allowance - used`
102
- * headroom one due maintenance loop at a time, so two due loops cannot start in the same poll.
103
- * Logs exactly one fleet-level `maintenance_quota_hold` when the gate goes from open to held and
104
- * one `maintenance_quota_resumed` when it re-opens — never one per loop. The director is never
98
+ /** Step the gate by one orchestrator poll: count the rolling window, add the maintenance loops in
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
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
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
105
105
  * maintenance-tier, so no exemption is needed. */
106
- export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding, now) {
106
+ export function pollMaintenanceQuotaGate(root, state, observers, perWorkMerge, now) {
107
107
  const { work, maint } = countMaintenanceWindow(root, state.counter, now);
108
108
  let inFlight = 0;
109
109
  for (const o of observers) {
@@ -111,14 +111,14 @@ export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding,
111
111
  continue;
112
112
  if (commitTier(baseRoleOf(o.role)) !== "maintenance")
113
113
  continue;
114
- if (o.running || o.hasQueuedLanding)
114
+ if (o.running || o.hasQueuedMerge)
115
115
  inFlight++;
116
116
  }
117
- const verdict = maintenanceQuota({ work, maint, inFlight, perWorkLanding });
117
+ const verdict = maintenanceQuota({ work, maint, inFlight, perWorkMerge });
118
118
  if (verdict.held !== state.prevHeld) {
119
119
  if (verdict.held) {
120
120
  logEventBestEffort(root, {
121
- loop: "harness",
121
+ source: TUMWATER_SOURCE,
122
122
  type: "maintenance_quota_hold",
123
123
  work,
124
124
  maint,
@@ -127,7 +127,7 @@ export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding,
127
127
  }
128
128
  else {
129
129
  logEventBestEffort(root, {
130
- loop: "harness",
130
+ source: TUMWATER_SOURCE,
131
131
  type: "maintenance_quota_resumed",
132
132
  work,
133
133
  maint,
@@ -138,9 +138,9 @@ export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding,
138
138
  }
139
139
  return verdict;
140
140
  }
141
- /** Count `merged` landings by tier over the rolling 24 h ending `now`, folding only the live
142
- * log's appended bytes since the previous call through `counter`. Returns `{ work, maint }`;
143
- * records that have aged out of the window are dropped from the memo so it stays bounded. */
141
+ /** Count `merged` events by tier over the rolling 24 h ending `now`, folding only the live log's
142
+ * appended bytes since the previous call through `counter`. Returns `{ work, maint }`; records that
143
+ * have aged out of the window are dropped from the memo so it stays bounded. */
144
144
  export function countMaintenanceWindow(root, counter, now) {
145
145
  const cutoff = now - MAINTENANCE_WINDOW_MS;
146
146
  // The log is read through a regular-file guard so a path that is missing or not a regular file
@@ -1,11 +1,11 @@
1
1
  /** The operator pause (`tumwater pause`) and the per-role pause (`tumwater pause --role <id>`), as
2
2
  * the orchestrator polls them each cycle. Extracted from orchestrator.ts's poll loop: the marker
3
3
  * reads, the edge-triggered pause/resume events, and the cross-poll bookkeeping are one concern
4
- * beside the other fleet gates (src/budget/budget.ts's budget gate, src/fleet/fleet-hold.ts's fleet
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 { isFleetPaused, pausedRoles } from "../fleet/fleet-state.js";
7
- import { logEventBestEffort } from "../events/events.js";
8
- /** A fresh pause-gate state: nothing seen paused yet, so the first poll of an unpaused fleet
6
+ import { isTeamPaused, pausedRoles } from "../team/team-state.js";
7
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
8
+ /** A fresh pause-gate state: nothing seen paused yet, so the first poll of an unpaused team
9
9
  * logs nothing. */
10
10
  export function newPauseGateState() {
11
11
  return { prevUserPaused: false, prevPausedRoles: new Set() };
@@ -18,24 +18,24 @@ export function newPauseGateState() {
18
18
  * only NEW ticks 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 fleet. Unlike
22
- * the fleet pause the director is NOT exempt: the operator named the role deliberately, and its
23
- * queued prompts simply wait in the inbox (the same effect the fleet pause has on the director).
21
+ * same persistent marker read fresh per cycle, but one named loop instead of the team. Unlike
22
+ * the team pause the director is NOT exempt: the operator named the role deliberately, and its
23
+ * queued prompts simply wait in the inbox (the same effect the team pause has on the director).
24
24
  * In-flight ticks finish; only NEW ticks are blocked (the gate sits before isEligible). */
25
25
  export function pollPauseGates(root, state) {
26
- const userPaused = isFleetPaused(root);
26
+ const userPaused = isTeamPaused(root);
27
27
  if (userPaused !== state.prevUserPaused) {
28
- logEventBestEffort(root, { loop: "harness", type: userPaused ? "fleet_paused" : "fleet_resumed" });
28
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: userPaused ? "team_paused" : "team_resumed" });
29
29
  state.prevUserPaused = userPaused;
30
30
  }
31
31
  const pausedRolesNow = pausedRoles(root);
32
32
  const pausedRolesSet = new Set(pausedRolesNow);
33
33
  for (const r of pausedRolesNow)
34
34
  if (!state.prevPausedRoles.has(r))
35
- logEventBestEffort(root, { loop: "harness", type: "role_paused", role: r });
35
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_paused", role: r });
36
36
  for (const r of state.prevPausedRoles)
37
37
  if (!pausedRolesSet.has(r))
38
- logEventBestEffort(root, { loop: "harness", type: "role_resumed", role: r });
38
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_resumed", role: r });
39
39
  state.prevPausedRoles = pausedRolesSet;
40
40
  return { userPaused, pausedRoles: pausedRolesSet };
41
41
  }
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** The repo preconditions' shared wording behind the readiness gate. startup-gate.ts answers
2
3
  * with the first unmet precondition (cmdRun and requireReadyRepo fail on it, the self-redeploy
3
4
  * refuses a swap on it) and doctor.ts reports each one individually, but every surface
@@ -7,5 +8,5 @@
7
8
  * the spawn in pi/pi-bin.ts. */
8
9
  export const NOT_A_REPO_MESSAGE = "not a git repository (run `git init` first)";
9
10
  export const NO_COMMITS_MESSAGE = "the repo has no commits yet; `tumwater init` creates the first one";
10
- export const DETACHED_HEAD_MESSAGE = "the repo's primary checkout is detached; check out your main branch first";
11
+ export const DETACHED_HEAD_MESSAGE = "your checkout is detached; check out your main branch first";
11
12
  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)";
@@ -1,7 +1,7 @@
1
1
  /** The per-role daily cost cap (PLANS.md 2026-09-30, part 1/2): a loop whose local-day spend
2
2
  * has reached its own `maxDailyCostUsdPerRole` entry starts no new ticks until the next local
3
- * day or a live config edit — so one runaway role cannot eat the fleet-wide cap and starve
4
- * every other loop for the rest of the day. The fleet budget gate (src/gates/budget-gates.ts)
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)
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
7
  * and nothing can get stuck"): the verdict is recomputed from the loop state every poll, no
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Deliberate differences from the streak gate's shape (src/gates/streak-gate.ts), stated in
11
11
  * the plan so no implementer re-litigates them:
12
- * - This gate writes NOTHING to the shared per-role pause marker (src/fleet/fleet-state.ts's
12
+ * - This gate writes NOTHING to the shared per-role pause marker (src/team/team-state.ts's
13
13
  * pauseRole). The marker is anonymous — after a restart across midnight the harness could
14
14
  * not tell a cap pause from an operator's, and would either refuse to lift the operator's
15
15
  * pause or orphan it. The scheduler blocks the role through the poll's separate `capPaused`
@@ -20,18 +20,19 @@
20
20
  * never silently no-ops a cap.
21
21
  *
22
22
  * Shape, like every gate in the family (src/gates/gate-polls.ts): the pure verdict and the
23
- * edge-triggered bookkeeping live here so they are unit-testable without a fleet, and this
23
+ * edge-triggered bookkeeping live here so they are unit-testable without a team, and this
24
24
  * module owns the only event emission — the orchestrator's poll owns only the wiring. The
25
25
  * in-memory `prev` and `warned` sets are the whole cross-poll memory: a restart with a
26
26
  * still-over-cap role re-logs one `role_cap_paused` on the first poll, and a role already past
27
27
  * 80% re-logs one `role_cap_warning` (the durable-cause honest report the streak-gate doc
28
28
  * accepts). */
29
+ import { groupBy } from "../collections.js";
29
30
  import { budgetWarning, dailyCost, todayStamp } from "../budget/budget.js";
30
- import { logEventBestEffort } from "../events/events.js";
31
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
31
32
  import { DIRECTOR_ROLE } from "../roles/roles.js";
32
33
  import { baseRoleOf } from "../roles/loop-ids.js";
33
34
  /** The stateless verdict: is this role over its own per-role cap as of `now`? True iff a cap
34
- * is configured, it is a finite number above zero (0 disables that role's cap, like the fleet
35
+ * is configured, it is a finite number above zero (0 disables that role's cap, like the team
35
36
  * cap), and today's spend has reached it. The role→cap lookup stays with the caller
36
37
  * (`caps?.[role]`), so part 2/2's observers (src/status/status-data.ts) share this single
37
38
  * definition with the scheduler. */
@@ -48,17 +49,11 @@ export function roleCapPaused(state, cap, now = Date.now()) {
48
49
  * is exempt. Keyed by base role in first-appearance order; each entry carries the group's loop
49
50
  * ids so a caller can log the base (the gate) or flatten the ids (the observer). */
50
51
  export function roleCapVerdicts(runners, caps, now) {
51
- const groups = new Map();
52
- for (const r of runners) {
53
- const base = baseRoleOf(r.role);
54
- if (base === DIRECTOR_ROLE)
55
- continue;
56
- const members = groups.get(base);
57
- if (members)
58
- members.push(r);
59
- else
60
- groups.set(base, [r]);
61
- }
52
+ // Group by base role via the shared Map-of-arrays step (collections.groupBy, which
53
+ // time-spend, history-data, and team-polls also fold), then drop the exempt director's
54
+ // group so it never holds a verdict; first-appearance order is preserved.
55
+ const groups = groupBy(runners, (r) => baseRoleOf(r.role));
56
+ groups.delete(DIRECTOR_ROLE);
62
57
  const verdicts = new Map();
63
58
  for (const [base, members] of groups) {
64
59
  const cap = caps?.[base];
@@ -94,7 +89,7 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
94
89
  const was = state.prev.has(base);
95
90
  if (verdict.over && !was) {
96
91
  logEventBestEffort(root, {
97
- loop: "harness",
92
+ source: TUMWATER_SOURCE,
98
93
  type: "role_cap_paused",
99
94
  role: base,
100
95
  spentUsd: verdict.spentUsd,
@@ -102,9 +97,9 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
102
97
  });
103
98
  }
104
99
  else if (!verdict.over && was) {
105
- logEventBestEffort(root, { loop: "harness", type: "role_cap_resumed", role: base });
100
+ logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_cap_resumed", role: base });
106
101
  }
107
- // The 80% early warning (PLANS.md 2026-10-09), the per-role twin of the fleet's
102
+ // The 80% early warning (PLANS.md 2026-10-09), the per-role twin of the team's
108
103
  // budget_warning and edge-triggered with the same reset-on-below: crossing the threshold
109
104
  // while still under the cap logs one role_cap_warning, a dip back under re-arms it. Warn
110
105
  // only while the cap is open — a poll that jumps straight to the cap already logs its own
@@ -113,7 +108,7 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
113
108
  const warn = !verdict.over && cap !== undefined && budgetWarning({ spentUsd: verdict.spentUsd, capUsd: cap });
114
109
  if (warn && !state.warned.has(base)) {
115
110
  logEventBestEffort(root, {
116
- loop: "harness",
111
+ source: TUMWATER_SOURCE,
117
112
  type: "role_cap_warning",
118
113
  role: base,
119
114
  spentUsd: verdict.spentUsd,