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
@@ -8,9 +8,9 @@ import { stateSkipReason } from "../loop/loop-state.js";
8
8
  * with a persistent reason (backoff, paused, disabled mid-round, or the director's empty inbox —
9
9
  * nothing a poll can change). The value is the skip reason, for the caller's summary. A role
10
10
  * whose tick is merely in flight is NOT settled — the orchestrator only fires its stop once
11
- * every role is settled AND no tick is in flight AND the land queue has been empty with no
12
- * landing in flight for one full poll cycle (the guard against dropping a
13
- * queued-but-not-yet-started landing: a stop that lands during the shutdown drain drops the
11
+ * every role is settled AND no tick is in flight AND the merge queue has been empty with no
12
+ * merge in flight for one full poll cycle (the guard against dropping a
13
+ * queued-but-not-yet-started merge: a stop that arrives during the shutdown drain drops the
14
14
  * queue entry, so the slot gets one poll to pick the entry up before the stop is trusted).
15
15
  */
16
16
  export class OnceRound {
@@ -24,7 +24,7 @@ export class OnceRound {
24
24
  this.runners = runners;
25
25
  this.active = active;
26
26
  for (const r of runners)
27
- this.tickSnapshots.set(r.role, r.state.ticks);
27
+ this.tickSnapshots.set(r.role, r.state.runs);
28
28
  }
29
29
  /** The runner's pre-round tick baseline. Snapshotted at construction for the startup
30
30
  * runners — and at FIRST SIGHT for a runner appended later: the live config reload
@@ -39,17 +39,17 @@ export class OnceRound {
39
39
  const known = this.tickSnapshots.get(runner.role);
40
40
  if (known !== undefined)
41
41
  return known;
42
- this.tickSnapshots.set(runner.role, runner.state.ticks);
43
- return runner.state.ticks;
42
+ this.tickSnapshots.set(runner.role, runner.state.runs);
43
+ return runner.state.runs;
44
44
  }
45
45
  /** A role is settled once it ran its tick or a settle reason was recorded for it. */
46
46
  isSettled(runner) {
47
- return runner.state.ticks > this.snapshotOf(runner) || this.settled.has(runner.role);
47
+ return runner.state.runs > this.snapshotOf(runner) || this.settled.has(runner.role);
48
48
  }
49
49
  /** Ticks this round advanced the runner past its snapshot — the exit summary's per-role
50
50
  * count, exact even for a role the caller's own pre-round snapshot never named. */
51
51
  ticksRun(runner) {
52
- return runner.state.ticks - this.snapshotOf(runner);
52
+ return runner.state.runs - this.snapshotOf(runner);
53
53
  }
54
54
  /** Record the role's once-round answer (why it ran no tick, or that it deferred). */
55
55
  settle(role, reason) {
@@ -72,14 +72,14 @@ export class OnceRound {
72
72
  return this.settled;
73
73
  }
74
74
  /** True on the SECOND consecutive quiet poll — every role settled, no role or director tick
75
- * in flight, no landing task running, land queue empty — and false otherwise (resetting the
75
+ * in flight, no merge task running, merge queue empty — and false otherwise (resetting the
76
76
  * quiet streak). The orchestrator fires its internal stop when this returns true. */
77
77
  exitReady(flight) {
78
78
  const quiet = this.runners.every((r) => this.isSettled(r)) &&
79
79
  flight.roleTicks === 0 &&
80
80
  flight.directorTicks === 0 &&
81
- flight.landings === 0 &&
82
- flight.queuedLandings === 0;
81
+ flight.merges === 0 &&
82
+ flight.queuedMerges === 0;
83
83
  if (!(this.active && quiet)) {
84
84
  this.idlePolls = 0;
85
85
  return false;
@@ -1,60 +1,53 @@
1
+ // vocabulary: plain
1
2
  /** Quiet hours (plans: "Quiet hours … part 1/2, the gate"): a config-driven daily local-time
2
3
  * window — `"23:00-07:00"` — during which role loops start no new ticks, so an operator can
3
- * put the fleet on a nightly schedule once instead of typing `tumwater pause` every evening.
4
+ * put the team on a nightly schedule once instead of typing `tumwater pause` every evening.
4
5
  * Extracted from the orchestrator's concerns the same way the pause gate is
5
6
  * (src/gates/pause-gates.ts): the parsing, the membership decision, and the edge-triggered
6
7
  * quiet/awake events live here; the orchestrator owns only the wiring. The director is
7
8
  * exempt — a human steering outranks a schedule — exactly as under the budget gate and the
8
9
  * operator pause. In-flight ticks finish; the gate sits before eligibility, so a tick due
9
10
  * inside the window simply starts at window end. */
10
- import { logEventBestEffort } from "../events/events.js";
11
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
11
12
  import { gotSuffix, isNonBlankString } from "../text/text.js";
13
+ /** The error arm of ParsedQuietHours — one home for the `{ ok: false, error }` constructor
14
+ * every parse-failure path of parseQuietHours returns, so the shape cannot drift between
15
+ * the five rejection messages. (The same-named helper in config-write.ts serves the config
16
+ * writers' parallel union; the type here is narrower, so each module owns its own.) */
17
+ function fail(error) {
18
+ return { ok: false, error };
19
+ }
12
20
  /** Parse one `quietHours` value: `"HH:MM-HH:MM"` in local time, 24-hour clock, a wrapping
13
21
  * window (`start > end`) is the overnight case the format exists for. `key` is the config key
14
- * the value came from, named verbatim in every error; it defaults to the fleet-wide
22
+ * the value came from, named verbatim in every error; it defaults to the team-wide
15
23
  * `quietHours`, and the per-role map passes `quietHoursPerRole.<id>` so a rejected entry names
16
24
  * the key the operator actually set rather than a different one. */
17
25
  export function parseQuietHours(value, key = "quietHours") {
18
26
  if (value === undefined || value === null)
19
27
  return { ok: true, window: null };
20
28
  if (typeof value !== "string")
21
- return {
22
- ok: false,
23
- error: `${key} must be a string like "23:00-07:00"${gotSuffix(value)}`,
24
- };
29
+ return fail(`${key} must be a string like "23:00-07:00"${gotSuffix(value)}`);
25
30
  if (value.trim() === "")
26
31
  return { ok: true, window: null }; // empty means off
27
32
  const parts = value.split("-");
28
33
  if (parts.length !== 2)
29
- return {
30
- ok: false,
31
- error: `${key} must be "HH:MM-HH:MM" with exactly one dash, e.g. "23:00-07:00"${gotSuffix(value)}`,
32
- };
34
+ return fail(`${key} must be "HH:MM-HH:MM" with exactly one dash, e.g. "23:00-07:00"${gotSuffix(value)}`);
33
35
  const ends = [];
34
36
  for (const part of parts) {
35
37
  const trimmed = part.trim();
36
38
  const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
37
39
  if (!m)
38
- return {
39
- ok: false,
40
- error: `${key} times must be 24-hour "HH:MM", e.g. "23:00"${gotSuffix(trimmed)}`,
41
- };
40
+ return fail(`${key} times must be 24-hour "HH:MM", e.g. "23:00"${gotSuffix(trimmed)}`);
42
41
  const hours = Number(m[1]);
43
42
  const minutes = Number(m[2]);
44
43
  if (hours > 23 || minutes > 59)
45
- return {
46
- ok: false,
47
- error: `${key} times must be 24-hour "HH:MM" — hours 00-23, minutes 00-59${gotSuffix(trimmed)}`,
48
- };
44
+ return fail(`${key} times must be 24-hour "HH:MM" — hours 00-23, minutes 00-59${gotSuffix(trimmed)}`);
49
45
  ends.push(hours * 60 + minutes);
50
46
  }
51
47
  // parts.length === 2 above, so both ends are present (the non-null assertions are safe).
52
48
  const [startMin, endMin] = [ends[0], ends[1]];
53
49
  if (startMin === endMin)
54
- return {
55
- ok: false,
56
- error: `${key} start and end must differ — a zero-length window means nothing schedulable${gotSuffix(value)}`,
57
- };
50
+ return fail(`${key} start and end must differ — a zero-length window means nothing schedulable${gotSuffix(value)}`);
58
51
  return { ok: true, window: { startMin, endMin } };
59
52
  }
60
53
  /** Is `date` (its LOCAL wall clock — the operator reads the schedule off their own clock)
@@ -68,7 +61,7 @@ export function inQuietHours(window, date) {
68
61
  }
69
62
  return minutes >= window.startMin || minutes < window.endMin; // wraps midnight
70
63
  }
71
- /** A fresh gate state: outside the window (the default for a fleet without quiet hours), so
64
+ /** A fresh gate state: outside the window (the default for a team without quiet hours), so
72
65
  * the first poll of an unset window logs nothing. */
73
66
  export function newQuietHoursGateState() {
74
67
  return { prevIn: false };
@@ -76,16 +69,16 @@ export function newQuietHoursGateState() {
76
69
  /** Poll the quiet-hours gate for one orchestrator cycle — the pause gate's scheduled sibling
77
70
  * (src/gates/pause-gates.ts pollPauseGates): parse the config value fresh (a live edit applies on
78
71
  * the next poll), decide membership for `now`, and log exactly one `quiet_hours_started` /
79
- * `quiet_hours_ended` event (`loop: "harness"`, with the effective window string) per
72
+ * `quiet_hours_ended` event (`source: TUMWATER_SOURCE`, with the effective window string) per
80
73
  * crossing. An unset/off window is simply "outside", so nothing is ever logged for it.
81
- * Returns whether the fleet is inside the window right now. */
74
+ * Returns whether the team is inside the window right now. */
82
75
  export function pollQuietHoursGate(root, quietHours, state, now) {
83
76
  const parsed = parseQuietHours(quietHours);
84
77
  const inNow = parsed.ok && parsed.window !== null && inQuietHours(parsed.window, now);
85
78
  if (inNow !== state.prevIn) {
86
79
  state.prevIn = inNow;
87
80
  logEventBestEffort(root, {
88
- loop: "harness",
81
+ source: TUMWATER_SOURCE,
89
82
  type: inNow ? "quiet_hours_started" : "quiet_hours_ended",
90
83
  window: typeof quietHours === "string" ? quietHours : undefined,
91
84
  });
@@ -93,11 +86,11 @@ export function pollQuietHoursGate(root, quietHours, state, now) {
93
86
  return inNow;
94
87
  }
95
88
  /** Per-role quiet hours (PLANS.md, `quietHoursPerRole`): is `role`'s own window holding at
96
- * `now`? The membership decision reuses the fleet window's own parser and predicate, so a
97
- * per-role window means exactly what the same string means fleet-wide (wrapping included).
89
+ * `now`? The membership decision reuses the team window's own parser and predicate, so a
90
+ * per-role window means exactly what the same string means team-wide (wrapping included).
98
91
  * An absent key, a non-string, or an unparseable value reads as off here — validation is
99
92
  * config-validation.ts's job, not this helper's, and a malformed value must never hold a
100
- * loop the operator did not schedule. Stateless: unlike the fleet-wide gate there is no
93
+ * loop the operator did not schedule. Stateless: unlike the team-wide gate there is no
101
94
  * edge-triggered event — a per-role hold is an anonymous verdict recomputed per poll,
102
95
  * exactly like `capPaused`'s set (role-cap-gates.ts). */
103
96
  export function roleQuietHold(perRole, role, now) {
@@ -108,11 +101,11 @@ export function roleQuietHold(perRole, role, now) {
108
101
  return parsed.ok && parsed.window !== null && inQuietHours(parsed.window, now);
109
102
  }
110
103
  /** The window's end time exactly as the operator wrote it — the text after the dash, trimmed —
111
- * for the surfaces that say "quiet until <end>" (badges.ts's quietBadge, fleet-alerts' quiet
104
+ * for the surfaces that say "quiet until <end>" (badges.ts's quietBadge, team-alerts' quiet
112
105
  * alert). parseQuietHours enforces exactly one dash, so reading the end back out of the string
113
106
  * is well-defined and preserves the operator's spelling instead of reformatting parsed
114
107
  * minutes. The GUI page's client-side script re-derives the same text from the payload's raw
115
- * quietHours field (gui-client-fleet.ts), since browser code cannot import this module. */
108
+ * quietHours field (gui-client-team.ts), since browser code cannot import this module. */
116
109
  export function quietWindowEnd(quietHours) {
117
110
  return quietHours.split("-")[1]?.trim() ?? "";
118
111
  }
@@ -1,12 +1,13 @@
1
1
  /** Tick-scheduling POLICY, split out of orchestrator.ts (the poll-loop runtime that calls it):
2
2
  * pure decisions over a runner's state and the world — when a loop may tick, in what order
3
- * eligible loops take slots, and whether landed work or an open backlog defers idle
3
+ * eligible loops take slots, and whether merged work or an open backlog defers idle
4
4
  * maintenance. No I/O, no process state: every function
5
5
  * takes plain data (a LoopRunner's observable fields, timestamps, commit subjects) so the
6
6
  * policy is unit-tested without running an orchestrator, and a future change to how ticks are
7
- * driven (e.g. the merge queue's async landing, PLANS.md 3/5) reuses these decisions instead of
7
+ * driven (e.g. the merge queue's async merge, PLANS.md 3/5) reuses these decisions instead of
8
8
  * re-deriving them inside the loop. Dependency direction: orchestrator → scheduling; this
9
9
  * module only reads LoopRunner through its public fields and imports no runtime from it. */
10
+ import { hasOutstandingWork } from "../loop/loop-state.js";
10
11
  import { scheduleConfigForRole } from "../config/config-views.js";
11
12
  import { BUGFIX_ROLE, DEFERRABLE_ROLES, DIRECTOR_ROLE, roleTier, } from "../roles/roles.js";
12
13
  import { baseRoleOf, loopEnabled } from "../roles/loop-ids.js";
@@ -56,32 +57,32 @@ export function isEligible(runner, now, mainHead, inboxCount, opts = {}) {
56
57
  // finding nothing slows down, and the multiplication gates the "main moved" wake below
57
58
  // too, which is where the cost actually comes from (the gap check above runs first, so a
58
59
  // scaled role inside its stretched gap neither ticks on schedule nor on a main move).
59
- // One landing in the last ten counted ticks restores the plain gap; error-class results
60
+ // One merge in the last ten counted ticks restores the plain gap; error-class results
60
61
  // never enter the ring (backoff.ts), so a failing backend does not stretch anything;
61
62
  // `--once`, an operator wake, and a queued prompt all bypass the gap check entirely.
62
63
  const mult = yieldMultiplierFor(runner.state);
63
64
  // The same bootstrap rule (scheduleConfigForRole) loop-tick.ts schedules nextRunAt from: during a
64
- // young project's bootstrap, plan runs on the fleet's global clock rather than its 3600 s
65
+ // young project's bootstrap, plan runs on the team's global clock rather than its 3600 s
65
66
  // default. Yield scaling still applies to whatever gap is chosen.
66
- const minGapSeconds = scheduleConfigForRole(runner.config, runner.role, opts.bootstrapActive === true).minTickIntervalSeconds;
67
+ const minGapSeconds = scheduleConfigForRole(runner.config, runner.role, opts.bootstrapActive === true, opts.descriptionIncomplete === true).minRunIntervalSeconds;
67
68
  const minGap = opts.once ? 0 : minGapSeconds * 1000 * mult;
68
- const sinceLast = now - (s.lastTickEndedAt ?? 0);
69
+ const sinceLast = now - (s.lastRunEndedAt ?? 0);
69
70
  // An operator wake newer than the gap window's opening tick overrides the interval
70
71
  // (LoopState.wokenAt): an explicit "try again now" with an empty queue must not silently
71
72
  // wait out the remaining slow clock it ticked inside — a queued prompt carries its own
72
73
  // due-ness in the inbox check above, so it never depends on this stamp surviving.
73
- const woken = s.wokenAt !== undefined && s.wokenAt > (s.lastTickEndedAt ?? 0);
74
+ const woken = s.wokenAt !== undefined && s.wokenAt > (s.lastRunEndedAt ?? 0);
74
75
  if (sinceLast < minGap && !woken)
75
76
  return { run: false };
76
77
  // Once mode's clock override, second half: a raised backoffSeconds means nextRunAt is a
77
78
  // backoff deadline, not the scheduled clock, and is honored; at zero it is the clock a
78
79
  // productive tick scheduled, and the explicit round demand overrides it.
79
80
  if (now >= s.nextRunAt || (opts.once && s.backoffSeconds === 0)) {
80
- return { run: true, reason: s.ticks === 0 ? "startup" : "scheduled" };
81
+ return { run: true, reason: s.runs === 0 ? "startup" : "scheduled" };
81
82
  }
82
83
  // The world changed under a sleeping loop: main moved since its last tick. The stretched
83
84
  // gap above gates this wake too (the gap check runs first), so a role whose recent ticks
84
- // landed nothing does not re-check on every landing — the multiplier's whole point.
85
+ // merged nothing does not re-check on every merge — the multiplier's whole point.
85
86
  if (s.lastMainHead && mainHead && mainHead !== s.lastMainHead) {
86
87
  return { run: true, reason: "main moved" };
87
88
  }
@@ -90,7 +91,7 @@ export function isEligible(runner, now, mainHead, inboxCount, opts = {}) {
90
91
  /** Fair scheduling order for one poll's eligible loops: the director always leads (it runs
91
92
  * the user's prompts), then the work tier (feature/bugfix/plan) before maintenance — a stale-
92
93
  * ticked feature takes a slot over a fresh-ticked steward, because shipping work is what the
93
- * fleet exists to do — and within a tier least-recently-ticked first, so loops alternate
94
+ * team exists to do — and within a tier least-recently-ticked first, so loops alternate
94
95
  * instead of the same ones re-claiming freed slots. Never-run loops tie at zero and the stable
95
96
  * sort keeps them in role-catalog (priority) order. */
96
97
  export function fairOrder(runners) {
@@ -101,20 +102,23 @@ export function fairOrder(runners) {
101
102
  const tier = roleTier(a.role) - roleTier(b.role);
102
103
  if (tier !== 0)
103
104
  return tier;
104
- return (a.state.lastTickEndedAt ?? 0) - (b.state.lastTickEndedAt ?? 0);
105
+ return (a.state.lastRunEndedAt ?? 0) - (b.state.lastRunEndedAt ?? 0);
105
106
  });
106
107
  }
107
- /** Did work land on main since a head? (Need-based prioritization, PLANS.md "Prioritize loops
108
+ /** Did work merge on main since a head? (Need-based prioritization, PLANS.md "Prioritize loops
108
109
  * by need".) A commit counts when its subject starts with `tumwater(feature):`,
109
- * `tumwater(bugfix):`, or `tumwater(director):` — the harness stamps that prefix itself
110
- * (buildCommitMessage), so attribution needs no new metadata; a director commit is user-directed
110
+ * `tumwater(bugfix):`, `tumwater(director):`, or `tumwater(operator):` — the harness stamps
111
+ * that prefix itself (buildCommitMessage), so attribution needs no new metadata; a director
112
+ * commit is user-directed
111
113
  * work, and after a pure-director burst the maintenance roles must resync. Or the subject
112
114
  * carries no `tumwater(` prefix at all: a human commit, where the world changed in a way the
113
- * fleet cannot generate itself. Every other role's landing is markdown or hygiene; waking
115
+ * team cannot generate itself. An operator commit (the stamp the CLI, TUI and dashboard
116
+ * backlog/brief edits write) is the human's own change made through tumwater, so it counts like
117
+ * an unstamped human commit. Every other role's merge is markdown or hygiene; waking
114
118
  * maintenance roles on it is exactly the cascade deferral removes.
115
119
  */
116
- export function workLanded(subjects) {
117
- return subjects.some((subject) => /^tumwater\((feature|bugfix|director)\):/.test(subject) || !subject.startsWith("tumwater("));
120
+ export function workMerged(subjects) {
121
+ return subjects.some((subject) => /^tumwater\((feature|bugfix|director|operator)\):/.test(subject) || !subject.startsWith("tumwater("));
118
122
  }
119
123
  /** How long a due maintenance tick may stay deferred before it is forced to run anyway. The
120
124
  * deferral predicate keys off `lastResult`, which only a completed tick updates, so without a
@@ -133,15 +137,30 @@ export const DEFER_MAX_MS = 3 * 3600 * 1000;
133
137
  function deferralExpired(s, now) {
134
138
  return s.nextRunAt > 0 && now - s.nextRunAt >= DEFER_MAX_MS;
135
139
  }
140
+ /** The plan loop's own deferral: a due tick (no fresh wake, nothing outstanding, main seen
141
+ * before, inside DEFER_MAX_MS) while PLANS.md already holds its eligible-plan target
142
+ * (planBacklogFull). `lastResult` does not matter: the charter would stop either way. */
143
+ function planDeferral(s, planBacklogFull, now) {
144
+ const woken = s.wokenAt !== undefined && s.wokenAt > (s.lastRunEndedAt ?? 0);
145
+ return planBacklogFull &&
146
+ !woken &&
147
+ !hasOutstandingWork(s) &&
148
+ s.lastMainHead !== "" &&
149
+ !deferralExpired(s, now)
150
+ ? "plans-full"
151
+ : null;
152
+ }
136
153
  /** The deferral verdict as its clause, or null when the tick is admitted. The one home of the
137
154
  * predicate (deferTick is this function's null test) and of the clause order: a deferrable
138
- * maintenance role with the backlog open reports "backlog" even when work also landed — the
139
- * backlog clause stands in for the landing verdict, which the caller short-circuits — while the
155
+ * maintenance role with the backlog open reports "backlog" even when work also merged — the
156
+ * backlog clause stands in for the merge verdict, which the caller short-circuits — while the
140
157
  * bugfix-in-search-mode branch ignores the backlog and reports "no-work". */
141
- export function deferTickReason(s, role, workLandedSinceLast, workBacklogOpen, openBugsNow, now) {
158
+ export function deferTickReason(s, role, workMergedSinceLast, workBacklogOpen, openBugsNow, now, planBacklogFull = false) {
142
159
  const base = baseRoleOf(role);
160
+ if (base === "plan")
161
+ return planDeferral(s, planBacklogFull, now);
143
162
  const searchBugfix = base === BUGFIX_ROLE && !openBugsNow;
144
- const due = !(s.wokenAt !== undefined && s.wokenAt > (s.lastTickEndedAt ?? 0)) &&
163
+ const due = !(s.wokenAt !== undefined && s.wokenAt > (s.lastRunEndedAt ?? 0)) &&
145
164
  (DEFERRABLE_ROLES.has(base) || searchBugfix) &&
146
165
  s.lastResult === "no_change" &&
147
166
  s.lastMainHead !== "" &&
@@ -150,9 +169,9 @@ export function deferTickReason(s, role, workLandedSinceLast, workBacklogOpen, o
150
169
  return null;
151
170
  if (!searchBugfix && workBacklogOpen)
152
171
  return "backlog";
153
- return workLandedSinceLast ? null : "no-work";
172
+ return workMergedSinceLast ? null : "no-work";
154
173
  }
155
174
  /** The boolean projection of deferTickReason: does this due maintenance tick defer? */
156
- export function deferTick(s, role, workLandedSinceLast, workBacklogOpen, openBugsNow, now) {
157
- return deferTickReason(s, role, workLandedSinceLast, workBacklogOpen, openBugsNow, now) !== null;
175
+ export function deferTick(s, role, workMergedSinceLast, workBacklogOpen, openBugsNow, now) {
176
+ return deferTickReason(s, role, workMergedSinceLast, workBacklogOpen, openBugsNow, now) !== null;
158
177
  }
@@ -1,23 +1,23 @@
1
- /** The need-based deferral's cached "did qualifying work land on main since <head>?" check,
1
+ /** The need-based deferral's cached "did qualifying work merge on main since <head>?" check,
2
2
  * split out of orchestrator.ts (whose runOrchestrator had grown it as an inline closure over
3
- * the poll-loop state) — it now lives in scheduling/ next to the workLanded policy it caches.
3
+ * the poll-loop state) — it now lives in scheduling/ next to the workMerged policy it caches.
4
4
  * This module adds only the caching layer around it plus the one git query that feeds it.
5
5
  *
6
6
  * Caching rule: verdicts are cached per base head so a
7
- * quiet fleet pays no git cost in steady state. A TRUE verdict is monotone under
8
- * fast-forward-only main movement — once work has landed in (sinceHead, main] it stays
7
+ * quiet team pays no git cost in steady state. A TRUE verdict is monotone under
8
+ * fast-forward-only main movement — once work has merged in (sinceHead, main] it stays
9
9
  * there — so true heads never re-evaluate. A FALSE verdict is valid exactly while main sits at
10
10
  * the head it was checked against: the range cannot grow until main moves, so a cached false
11
11
  * skips the `git log` entirely and re-checks once when main's head changes (a false can flip to
12
12
  * true only on such movement — caching it unconditionally would defer a role forever after the
13
- * very commit that should wake it). Both caches are bounded, so a long-running fleet cannot
13
+ * very commit that should wake it). Both caches are bounded, so a long-running team cannot
14
14
  * grow them unbounded. */
15
15
  import { subjectsBetween } from "../git/git.js";
16
- import { workLanded } from "./scheduling.js";
17
- export class WorkLandedCache {
16
+ import { workMerged } from "./scheduling.js";
17
+ export class WorkMergedCache {
18
18
  root;
19
19
  mainBranch;
20
- workLandedHeads = new Set();
20
+ workMergedHeads = new Set();
21
21
  /** sinceHead -> main head at which "no work" held */
22
22
  noWorkAtHead = new Map();
23
23
  constructor(root, mainBranch) {
@@ -25,7 +25,7 @@ export class WorkLandedCache {
25
25
  this.mainBranch = mainBranch;
26
26
  }
27
27
  async since(sinceHead, mainHeadNow) {
28
- if (this.workLandedHeads.has(sinceHead))
28
+ if (this.workMergedHeads.has(sinceHead))
29
29
  return true;
30
30
  const checkedAt = this.noWorkAtHead.get(sinceHead);
31
31
  // Main has not moved since the last check for this head — the range is unchanged. An empty
@@ -33,13 +33,13 @@ export class WorkLandedCache {
33
33
  if (mainHeadNow !== "" && checkedAt === mainHeadNow)
34
34
  return false;
35
35
  const subjects = await subjectsBetween(this.root, sinceHead, this.mainBranch);
36
- // A range that cannot be evaluated is treated as work landed — conservative: run the tick.
37
- const verdict = subjects === null ? true : workLanded(subjects);
36
+ // A range that cannot be evaluated is treated as work merged — conservative: run the tick.
37
+ const verdict = subjects === null ? true : workMerged(subjects);
38
38
  if (verdict) {
39
39
  this.noWorkAtHead.delete(sinceHead);
40
- if (this.workLandedHeads.size >= 200)
41
- this.workLandedHeads.clear();
42
- this.workLandedHeads.add(sinceHead);
40
+ if (this.workMergedHeads.size >= 200)
41
+ this.workMergedHeads.clear();
42
+ this.workMergedHeads.add(sinceHead);
43
43
  }
44
44
  else if (mainHeadNow !== "") {
45
45
  if (this.noWorkAtHead.size >= 200)
@@ -0,0 +1,115 @@
1
+ // vocabulary: plain
2
+ import { claudeWindowEntries, readClaudeAccount, } from "../agent/claude/claude-account.js";
3
+ import { budgetReached, teamDailyCost, modelPairName, projectCapHit } from "../budget/budget.js";
4
+ import { roleCapVerdicts } from "../gates/role-cap-gates.js";
5
+ import { configForRole, resolverConfig, reviewConfig, roleSeamTier, } from "../config/config-views.js";
6
+ import { resolveTierFallbacks, tierFallbackLabels, tiersResolveDistinctPairs } from "../config/config-fallbacks.js";
7
+ import { enabledRoleIds } from "../config/config.js";
8
+ import { fallbackModelFree, piPairFree, readPiProviders, seamFree, teamModelsFree } from "../pi/pi-models.js";
9
+ import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
10
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
11
+ /** The claude subscription-window block for a poll (see StatusSnapshot.claudeWindow): absent
12
+ * unless the account reads subscription billing with at least one window AND a resolved seam
13
+ * routes to claude. `only` is the free-ness classification the display needs: every paying seam
14
+ * is a subscription claude seam exactly when no other seam is priced — a pi seam judged by
15
+ * piPairFree, any other backend's billing unseen so it counts as paying too. The reviewer and
16
+ * the strong-tier resolver are included, like teamModelsFree, since they can spend. The
17
+ * account is the poll's single readClaudeAccount result, so the window and every piPairFree
18
+ * verdict judge the same reading. */
19
+ function claudeWindowBlock(cfg, providers, account, catalog) {
20
+ if (account?.billing !== "subscription")
21
+ return undefined;
22
+ const windows = claudeWindowEntries(account);
23
+ if (windows.length === 0)
24
+ return undefined;
25
+ const seams = enabledRoleIds(cfg).map((role) => configForRole(cfg, role));
26
+ if (cfg.review.enabled)
27
+ seams.push(reviewConfig(cfg));
28
+ if (seams.length > 0)
29
+ seams.push(resolverConfig(cfg));
30
+ if (!seams.some((s) => s.agent === "claude"))
31
+ return undefined;
32
+ const priced = seams.some((s) => {
33
+ if (s.agent === undefined || s.agent === "pi")
34
+ return !piPairFree(providers, s.provider, s.model, account, catalog);
35
+ // An opencode seam is subscription-paid only when the catalog prices it at zero (part
36
+ // 6b/7); any other agent the account cannot bill is never subscription-paid.
37
+ if (s.agent === "opencode")
38
+ return !seamFree(providers, s, account, catalog);
39
+ return s.agent !== "claude"; // a backend the account cannot bill is never subscription-paid
40
+ });
41
+ const heldUntil = windows
42
+ .filter((w) => w.status === "rejected" && w.isUsingOverage !== true && typeof w.resetsAt === "number")
43
+ .reduce((max, w) => Math.max(max, w.resetsAt), 0) || null;
44
+ return { windows, primary: windows[0], only: !priced, heldUntil };
45
+ }
46
+ /** The spend/billing block for a poll: the one materialized `spentUsd` (the scheduler's own
47
+ * published figure while running — BUGS.md 2026-09-30 — the persisted states' sum otherwise)
48
+ * that the budget object, the burn-rate projection and `free`/`fallback`/`tiers` all read;
49
+ * the per-tier pause set (part 5c/8) and the per-role daily-cap set, each the gate's own
50
+ * verdict over the same last-known-good config and one `now`. Owns the price-aware pair logic
51
+ * (piPairFree, fallbackModelFree, teamModelsFree, resolveTierFallbacks) so it has one home:
52
+ * models.json is read once (readPiProviders) and the claude account once (readClaudeAccount,
53
+ * on the snapshot's own pinned `now`), and every free-ness verdict and the window judge that
54
+ * single reading. The director is exempt from both pause sets, exactly as the gates exempt it. */
55
+ export function collectBudget(root, cfg, loops, roles, running, info, modelsPath, now) {
56
+ // The running orchestrator's published gate figures (read once above, with the pid and the
57
+ // demotion): the scheduler's own sum over its live runner states. Only while running — the
58
+ // exit removes the info file, so a stale file beside a dead pid must not speak for a team
59
+ // whose persisted files are final.
60
+ const publishedSpend = running && info?.budget ? info.budget.spentUsd : null;
61
+ // The one materialized spend the budget block renders and the burn-rate projection both
62
+ // read — the scheduler's published figure while running, the persisted states' sum otherwise.
63
+ const spentUsd = publishedSpend ?? teamDailyCost(loops, now);
64
+ // The persisted billing answer (6a/7), read once for every free-ness verdict below: a
65
+ // subscription claude seam is free, so the badge and the pause set judge it as such.
66
+ const account = readClaudeAccount(root, now);
67
+ const budgetReachedNow = budgetReached({ spentUsd, capUsd: cfg.maxDailyCostUsd });
68
+ const providers = readPiProviders(modelsPath);
69
+ // The cached opencode models catalog (part 6b/7), seeded at the startup gate and read once per
70
+ // poll so every free-ness verdict below prices opencode seams against the same snapshot.
71
+ const catalog = readOpencodeCache(root);
72
+ const publishedDemotions = info?.fallbackDemotions ?? {};
73
+ const usable = (p) => piPairFree(providers, p.provider, p.model, account, catalog) && !(modelPairName(p) in publishedDemotions);
74
+ const servingResolved = resolveTierFallbacks(cfg, usable);
75
+ // The subscription usage window (part 6d/7): one account read per poll serves the block.
76
+ const claudeWindow = claudeWindowBlock(cfg, providers, account, catalog);
77
+ // The per-tier budget pause set (part 5c/8): the same resolution the scheduler's gate folds,
78
+ // over the SAME usable predicate an observer can re-derive — the price check, and the pair
79
+ // not among the running orchestrator's published per-pair demotions. Computed only while the
80
+ // cap is reached — the hold never stands under an open gate — and the director exempt.
81
+ const budgetPausedRoles = budgetReachedNow
82
+ ? roles.filter((r) => r !== DIRECTOR_ROLE && servingResolved[roleSeamTier(cfg, r)].pair === null)
83
+ : [];
84
+ const capPaused = [
85
+ ...roleCapVerdicts(loops.map((l) => ({ role: l.role, state: l })), cfg.maxDailyCostUsdPerRole, now).values(),
86
+ ]
87
+ .filter((v) => v.over)
88
+ .flatMap((v) => v.roleIds);
89
+ return {
90
+ budget: {
91
+ spentUsd,
92
+ capUsd: cfg.maxDailyCostUsd,
93
+ // One projection per poll, from the same spend the field above renders: null exactly
94
+ // when no forecast may be stated (no cap, no spend, cap reached, burn misses midnight).
95
+ capHitAt: projectCapHit({ spentUsd, capUsd: cfg.maxDailyCostUsd }, now),
96
+ free: teamModelsFree(cfg, modelsPath, account, catalog),
97
+ backend: cfg.backend ?? "pi",
98
+ // Null unless the gate could actually engage it (configured AND priced at zero AND not
99
+ // demoted by the running orchestrator's breaker): a fallback the scheduler would refuse
100
+ // must not be advertised as one that will save the team — and a demoted one is refused
101
+ // until its probe serves, so the dashboards read `budget paused` exactly while the
102
+ // scheduler is (BUGS.md 2026-09-20). The pair is the price-aware default tier's
103
+ // resolution, so a map with no `default` entry names the pair the gate actually runs.
104
+ fallback: fallbackModelFree(cfg, modelsPath, account, catalog) && !(running && info?.fallbackDemoted)
105
+ ? (servingResolved.default.pair ?? null)
106
+ : null,
107
+ tiers: budgetReachedNow && tiersResolveDistinctPairs(servingResolved)
108
+ ? tierFallbackLabels(servingResolved)
109
+ : null,
110
+ },
111
+ claudeWindow,
112
+ budgetPausedRoles,
113
+ capPaused,
114
+ };
115
+ }