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
@@ -1,107 +1,108 @@
1
+ // vocabulary: plain
1
2
  import { DIRECTOR_ROLE } from "../roles/roles.js";
2
- import { baseRoleOf, instanceIndex } from "../roles/loop-ids.js";
3
+ import { baseRoleOf } from "../roles/loop-ids.js";
4
+ import { heldByMainRed } from "../status/status-polls.js";
3
5
  import { ERROR_STREAK_WARN, QUIET_KILL_RESUME_LIMIT } from "../tick/tick-apply.js";
4
6
  import { readLiveProgress } from "./progress-data.js";
5
7
  import { quietWindowEnd } from "../scheduling/quiet-hours.js";
6
8
  import { humanSeconds, secondsUntil } from "../text/datetime.js";
7
9
  import { duration, inFlightDetail, inFlightLabel, progressKind, tickProgress, workingDetail, } from "./tick-progress-model.js";
8
- import { landingChanges } from "../landing/landing-slot.js";
9
- /** The snapshot's in-flight landing record (merge queue 4/5) filtered to one role: this
10
- * role's change record while the pipeline holds it — vetting or merging it (`landing`), or
11
- * keeping it for the merge slot (`vetted`) — and null otherwise, including once the merge is
10
+ import { mergeChanges, MERGE_STAGE_LABELS } from "../merge/merge-slot.js";
11
+ /** The snapshot's in-flight merge record (merge queue 4/5) filtered to one role: this
12
+ * role's change record while the pipeline holds it — verifying or merging it (`merge`), or
13
+ * keeping it for the merge slot (`approved`) — and null otherwise, including once the merge is
12
14
  * `done` with it (a change that reached its outcome must not keep a live label while the rest of
13
- * its stack lands — BUGS.md 2026-09-23). An older generation's one-change marker reads as one
14
- * `landing` record (landingChanges). The single home of the "is this role landing" filter —
15
- * every phase call site derives its `landing` argument through this so the filtering cannot
15
+ * its stack merges — BUGS.md 2026-09-23). An older generation's one-change marker reads as one
16
+ * `merge` record (mergeChanges). The single home of the "is this role merge" filter —
17
+ * every phase call site derives its `merge` argument through this so the filtering cannot
16
18
  * drift between them. */
17
- export function landingForRole(landQueue, role) {
18
- if (!landQueue.inFlight)
19
+ export function mergeForRole(mergeQueue, role) {
20
+ if (!mergeQueue.inFlight)
19
21
  return null;
20
- const change = landingChanges(landQueue.inFlight).find((c) => c.role === role);
22
+ const change = mergeChanges(mergeQueue.inFlight).find((c) => c.role === role);
21
23
  return change && change.status !== "done"
22
- ? { status: change.status, startedAt: change.startedAt, stage: change.stage }
24
+ ? { status: change.status, startedAt: change.startedAt, stage: change.stage, stageSince: change.stageSince }
23
25
  : null;
24
26
  }
25
- /** How each landing stage names itself in the landing cell. */
26
- const LANDING_STAGE_LABELS = {
27
- rebasing: "rebasing",
28
- "check-wait": "waiting for a check slot",
29
- "build-check": "build check",
30
- reviewing: "reviewing",
31
- "resolve-wait": "waiting to resolve conflicts",
32
- resolving: "resolving conflicts",
33
- merging: "merging",
34
- };
35
- /** The work text a row shows in place of live progress while it is idle or landing
27
+ /** The work text a row shows in place of live progress while it is idle or merge
36
28
  * (plans/parallel-work-instances.md "Observability", part 6/7): the claim's heading when the
37
- * instance holds one, otherwise — for an idle extra instance that was skipped for want of a
38
- * free entry — `idle — no unclaimed <plans|bugs>`. Returns null for a row with neither, so the
39
- * state cell keeps its plain phase. An extra instance is skipped without touching its backoff,
40
- * so the `queued` phase is the tell that the skip, not a sleep or a gate, left it idle. */
41
- export function idleClaimText(s, phase) {
42
- if (s.claim)
43
- return s.claim.title;
44
- if (phase === "queued" && instanceIndex(s.role) >= 2)
45
- return `idle — no unclaimed ${baseRoleOf(s.role) === "bugfix" ? "bugs" : "plans"}`;
46
- return null;
29
+ * instance holds one. Returns null otherwise, so the state cell keeps its plain phase — an
30
+ * idle instance's phase label (`idle: no unclaimed plans`) carries its own reason. */
31
+ export function idleClaimText(s, _phase) {
32
+ return s.claim ? s.claim.title : null;
47
33
  }
48
34
  /** The loop's state label — the one precedence ladder shared by the status table and both
49
35
  * dashboards (renderStatus, statusPayload). First match wins: a stopped harness; an in-flight
50
- * landing (the role's tick already ended "queued" while its change lands, so the marker
36
+ * merge (the role's tick already ended "queued" while its change merges, so the marker
51
37
  * supplies the label); a running tick ("working", or "reviewing" while the review gate holds
52
- * it — in-flight work finishes through both fleet gates); the director's "waiting for
38
+ * it — in-flight work finishes through both team gates); the director's "waiting for
53
39
  * prompts" (exempt from those gates); user pause; budget pause; a red main ("main red"); an
54
- * error or quiet-kill streak at its threshold ("failing"); sleep; then "queued".
40
+ * error or quiet-kill streak at its threshold ("failing"); sleep; then the scheduling
41
+ * poll's recorded skip (`idle: …` / `deferred (…)`, from LoopState.skipReason), else
42
+ * "queued".
55
43
  * Pass `root` so an in-flight tick expands into live detail (elapsed · turn · ctx · tool);
56
44
  * without it a working loop shows plain "working". `live`, when given, is the frame's
57
45
  * precomputed tail (see workingDetail) — it skips the log read; without it an in-flight tick
58
- * reads on its own. `budgetPaused` marks the fleet's daily cost budget as reached: idle role
46
+ * reads on its own. `budgetPaused` marks the team's daily cost budget as reached: idle role
59
47
  * loops show `budget paused` instead of their sleep/queue state — that is why they are not
60
48
  * ticking. `userPaused` marks an operator pause (`tumwater pause` marker present): idle role
61
49
  * loops show `paused`, checked before the budget gate because user intent is more specific
62
50
  * than spend state — while both hold, "paused" tells the operator what to do (`resume`).
63
- * `landing`, when given for this role, is its change's record in the snapshot's in-flight
64
- * landing — build it with landingForRole so the marker's role filter lives in one place. A
65
- * change the pipeline holds but is not working on reads `vetted, awaiting merge`, with no
66
- * elapsed since nothing of its own is running: its vet approved it and it waits for the merge
67
- * slot. (A queued change whose vet is still parked for a permit has no record: it reads
51
+ * `merge`, when given for this role, is its change's record in the snapshot's in-flight
52
+ * merge — build it with mergeForRole so the marker's role filter lives in one place. A
53
+ * change the pipeline holds but is not working on reads `approved, awaiting merge`, with no
54
+ * elapsed since nothing of its own is running: its verification approved it and it waits for the
55
+ * merge slot. (A queued change whose verification is still parked for a permit has no record:
56
+ * it reads
68
57
  * plainly queued.)
69
58
  * `mainCheck`, when given (loopRowCells passes the snapshot's), lets the main-red label
70
59
  * retire itself between ticks: main's newest merge-scope check passing AFTER this loop's red
71
60
  * tick ended means main is green now — the loop's `main_red` verdict is stale until its next
72
61
  * tick, which sleep, a full slot pool, a pause, or a spent budget can defer for hours
73
62
  * (BUGS.md 2026-09-30), so the label falls through to the ordinary idle state and the
74
- * fleetAlerts banner follows. A check that is newer but failed keeps the label (main is
63
+ * teamAlerts banner follows. A check that is newer but failed keeps the label (main is
75
64
  * genuinely red); so does one older than the red tick or with no known pass at all — the
76
65
  * blockage stands until the loop's own next tick re-verdicts it.
77
- * Otherwise its elapsed is the CHANGE's own landing (its record's startedAt — from its vet's
66
+ * Otherwise its elapsed is the CHANGE's own merge (its record's startedAt — from its verification's
78
67
  * start, never another change's), never the authoring tick's,
79
- * and its stage scopes the cell to the phase the landing is actually in: `reviewing` carries
80
- * the reviewer run's live detail exactly as a reviewing tick does (`landing 3m · reviewing ·
68
+ * and its stage scopes the cell to the phase the merge is actually in: `reviewing` carries
69
+ * the reviewer run's live detail exactly as a reviewing tick does (`merge 3m · reviewing ·
81
70
  * turn 2 · ctx 18.0k · bash npm test`), and `resolving` the conflict resolver's the same way;
82
- * `build-check`, `resolve-wait` and `merging` render just the stage (`landing 1m · build
71
+ * `build-check`, `resolve-wait` and `merging` render just the stage (`merge 1m · build
83
72
  * check`) and never read the log — no reviewer or resolver is running then, and the log's
84
73
  * newest run is typically a finished one (the author's, a previous review) whose turns are
85
74
  * stale and whose silence would read as a false stall; a record with no stage (an older
86
75
  * writer mid-upgrade) keeps the bare elapsed label. */
87
- export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, landing, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold) {
76
+ export function onErrorStreak(s) {
77
+ // An error streak at or past the warning threshold (tick-apply.ts's ERROR_STREAK_WARN), or
78
+ // a quiet-kill streak at the give-up threshold (BUGS.md 2026-09-18): the exact condition
79
+ // that makes loopPhase read "failing", split out so the team alert (team-alerts.ts) can
80
+ // key its red "failing tick after tick" banner on the streak rather than on the rendered
81
+ // phase — an in-flight tick hides the streak behind a `working`/`merge` label (loopPhase
82
+ // ranks running work above failing), but the alert must stay up while the loop's next tick
83
+ // runs and clear only when the streak resets (BUGS.md 2026-10-14).
84
+ return ((s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN ||
85
+ (s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT));
86
+ }
87
+ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, merge, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold, permits, pausedUntilMs) {
88
88
  if (!orchestratorRunning)
89
89
  return "stopped";
90
- // Merge queue 4/5 — the marker-driven landing label: only a role whose change record was
91
- // passed gets it (callers derive it via landingForRole), and a stopped harness never
92
- // shows it — a dead fleet's marker is stale by definition.
93
- if (landing) {
94
- if (landing.status === "vetted")
95
- return "vetted, awaiting merge";
96
- const head = landing.startedAt === undefined ? "landing" : `landing ${duration(Date.now() - landing.startedAt)}`;
97
- if (!landing.stage)
90
+ // Merge queue 4/5 — the marker-driven merge label: only a role whose change record was
91
+ // passed gets it (callers derive it via mergeForRole), and a stopped harness never
92
+ // shows it — a dead team's marker is stale by definition.
93
+ if (merge) {
94
+ if (merge.status === "approved")
95
+ return "approved, waiting to merge";
96
+ const head = merge.startedAt === undefined ? "merging" : `merging ${duration(Date.now() - merge.startedAt)}`;
97
+ if (!merge.stage)
98
98
  return head;
99
- const staged = `${head} · ${LANDING_STAGE_LABELS[landing.stage]}`;
100
- if (landing.stage !== "reviewing" && landing.stage !== "resolving")
99
+ const base = `${head} · ${MERGE_STAGE_LABELS[merge.stage]}`;
100
+ const staged = merge.stageSince === undefined ? base : `${base} ${duration(Date.now() - merge.stageSince)}`;
101
+ if (merge.stage !== "reviewing" && merge.stage !== "resolving")
101
102
  return staged;
102
103
  // The reviewer writes the role's own log from its leased pooled slot, and the conflict
103
104
  // resolver from the merge worktree, both as gate runs (loop-pi.ts): the GATE accumulator,
104
- // read here rather than taken from `live` — the landing role's tick has ended, so the
105
+ // read here rather than taken from `live` — the merge role's tick has ended, so the
105
106
  // frame's per-running-loop tail is not this run's (callers pass null for it). Each run's
106
107
  // `tumwater_run` marker resets that accumulator, so neither shows the other's counts.
107
108
  return inFlightDetail(staged, root ? readLiveProgress(root, s.role, "gate") : null);
@@ -109,10 +110,22 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
109
110
  if (s.running) {
110
111
  // A parked waiter is reserved against double-scheduling but holds no maxConcurrent permit
111
112
  // and runs no pi yet: render its true state and keep it OUT of the active set (isActivePhase)
112
- // so the operator can count active rows against the cap — a landing visibly counts, a
113
+ // so the operator can count active rows against the cap — a merge visibly counts, a
113
114
  // parked waiter visibly does not (BUGS.md 2026-09-24).
114
- if (s.parkedSince)
115
- return `awaiting slot ${duration(Date.now() - s.parkedSince)}`;
115
+ if (s.parkedSince) {
116
+ const parkedTime = duration(Date.now() - s.parkedSince);
117
+ // The parked waiter's provider key, named only while that key is the binding limit in
118
+ // the published permit snapshot (plans/provider-concurrency.md §8): the key is at its
119
+ // cap, so THIS waiter is queued on its provider, not on the team total — `awaiting
120
+ // omlx slot` tells the operator exactly which cap is full. A role parked on the total
121
+ // (no key, or an uncapped one) reads today's `awaiting slot` as before.
122
+ if (s.parkedOn !== undefined &&
123
+ permits !== undefined &&
124
+ isBindingLimit(permits, s.parkedOn)) {
125
+ return `waiting for ${s.parkedOn} ${parkedTime}`;
126
+ }
127
+ return `waiting to start ${parkedTime}`;
128
+ }
116
129
  // The director bypasses maxConcurrent (`usesSlot` is false for it), so its in-flight tick
117
130
  // is a state of its own: a distinct label that `isActivePhase`'s cap-mirroring set
118
131
  // excludes while still carrying the live detail, so its work stays visible without
@@ -135,32 +148,43 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
135
148
  return root ? workingDetail(root, s, live) : "working";
136
149
  }
137
150
  if (s.role === DIRECTOR_ROLE)
138
- return "waiting for prompts"; // exempt from both gates
139
- if (userPaused)
151
+ return "ready for messages"; // exempt from both gates
152
+ if (userPaused) {
153
+ // A timed per-role pause (`pause --role <id> --for`): the marker's `until` gives the
154
+ // operator a countdown, mirroring the team header badge and the GUI loop-list's
155
+ // `· resumes in <s>` — an indefinite per-role pause or a team-wide pause (deadline
156
+ // absent) keeps today's bare `paused`, so the surfaces cannot drift.
157
+ if (pausedUntilMs !== undefined && pausedUntilMs > Date.now()) {
158
+ return `paused · resumes in ${humanSeconds(Math.max(0, Math.round((pausedUntilMs - Date.now()) / 1000)))}`;
159
+ }
160
+ // An absent deadline (indefinite per-role pause, team-wide pause) or an expired one
161
+ // (the marker team-state already reads as unpaused) keeps today's bare `paused` — never
162
+ // a stale `resumes in 0s`.
140
163
  return "paused";
164
+ }
141
165
  // The loop's OWN daily cap: the more specific spend
142
- // state, so it names itself ahead of the fleet's budget pause — the ladder's existing
166
+ // state, so it names itself ahead of the team's budget pause — the ladder's existing
143
167
  // "user intent is more specific than spend state" argument extended one level down.
144
168
  if (capPaused)
145
- return "cap paused";
146
- // The loop's OWN quiet-hours window (PLANS.md, quietHoursPerRole): the fleet gate's own
169
+ return "waiting: daily cap spent";
170
+ // The loop's OWN quiet-hours window (PLANS.md, quietHoursPerRole): the team gate's own
147
171
  // wording — `quiet until <end>` — scoped to the one loop the operator scheduled. Shown
148
- // after the cap and before the fleet's budget pause: the loop's own schedule is more
172
+ // after the cap and before the team's budget pause: the loop's own schedule is more
149
173
  // specific than either.
150
174
  if (quietHold)
151
175
  return `quiet until ${quietWindowEnd(quietHold)}`;
152
176
  if (budgetPaused)
153
- return "budget paused";
154
- // The disk floor (plans/disk-floor.md, part 4/4): the fleet starts no new work below the
155
- // floor, so an idle loop reads "disk hold" — the fleet-wide gate's own wording, ranked with
177
+ return "waiting: budget spent";
178
+ // The disk floor (plans/disk-floor.md, part 4/4): the team starts no new work below the
179
+ // floor, so an idle loop reads "disk hold" — the team-wide gate's own wording, ranked with
156
180
  // the budget pause (LOOP_RANK_RULES). In-flight ticks keep their live detail above, exactly
157
181
  // as under every autonomous gate.
158
182
  if (diskHeld)
159
- return "disk hold";
183
+ return "waiting: low disk";
160
184
  // New-project bootstrap (plans/work-ratio.md, part 2/2): the idle loop is held until enough
161
185
  // plans are Done. The literal label names the reason, like the gates above.
162
186
  if (bootstrapHeld)
163
- return "held: bootstrap";
187
+ return "waiting: bootstrap";
164
188
  // The maintenance allowance's own hold (plans/work-ratio.md, part 1b/4): the label carries the
165
189
  // figures so the operator sees how far over the window is; the string is preformatted by the
166
190
  // snapshot's holder, like roleQuietPaused's window.
@@ -168,18 +192,18 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
168
192
  return maintenanceQuotaHold;
169
193
  // Main's own suite is known red at this loop's last tick: code-producing loops are blocked
170
194
  // from authoring until main is green (the red-main baseline check). Shown before sleep/queue
171
- // because it explains why the loop keeps waking and landing nothing; self-correcting, since
195
+ // because it explains why the loop keeps waking and merge nothing; self-correcting, since
172
196
  // each blocked tick re-records main_red while red and a green wake overwrites lastResult.
173
197
  // Except a green wake that queues a change: `queued` is in flight, not a completed result,
174
- // so it leaves the main_red pair in place until its landing resolves (tick-apply.ts's
198
+ // so it leaves the main_red pair in place until its merge resolves (tick-apply.ts's
175
199
  // applyTickOutcome) — its stashed summary is the tell that the loop's latest tick got past
176
200
  // the red-main gate, so main was green then and "main red" would be stale.
177
201
  // Except too when main's newest merge-scope check PASSED after this loop's red tick ended:
178
- // the fleet-level verdict is fresher than the loop's own, so the label is stale until the
202
+ // the team-level verdict is fresher than the loop's own, so the label is stale until the
179
203
  // loop's next tick — which can be hours away (BUGS.md 2026-09-30). A check older than the
180
204
  // red tick, a failed or skipped one, or an unknown tick end all keep the label.
181
- if (s.lastResult === "main_red" && s.queuedSummary === undefined && !mainCheckRecovered(mainCheck, s))
182
- return "main red";
205
+ if (heldByMainRed(s, mainCheck))
206
+ return "main's check is failing";
183
207
  // An error streak at or past the warning threshold (tick-apply.ts's ERROR_STREAK_WARN): the
184
208
  // loop is retrying the same failure on the error ladder, and the operator must see
185
209
  // "failing" — not a sleepy label — while it is stuck (BUGS.md 2026-09-15). The streak
@@ -188,40 +212,62 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
188
212
  // backend must still read "failing" (BUGS.md 2026-09-21). Self-clearing: the first
189
213
  // healthy tick resets the streak, and each retry re-arms it while the environment stays
190
214
  // broken.
191
- if ((s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN)
192
- return "failing";
193
- // A quiet-kill streak at the give-up threshold reads "failing" too (BUGS.md 2026-09-18):
194
- // the loop is retrying a session the backend will not schedule, and before this it looked
195
- // exactly like a sleeping loop while it burned an hour of slot time per tick.
196
- if (s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT)
215
+ if (onErrorStreak(s))
197
216
  return "failing";
217
+ // The resting state cell (sleeping / idle / deferred / queued): it is the only label that
218
+ // names what a loop has been RETURNING, so the consecutive no-change streak rides on it
219
+ // (tick-apply.ts applyTickOutcome's noChangeStreak, persisted in LoopState). A loop that
220
+ // keeps waking and returning nothing to do reads " · no change ×N" instead of an ordinary
221
+ // idle — the harness premise's silent no-op failure mode, otherwise indistinguishable from a
222
+ // healthy idle loop. Observational only: a legitimately idle maintenance loop with an empty
223
+ // backlog can hold a long streak and that is expected, so it never warns (unlike
224
+ // consecutiveErrors/quietKillStreak). N=1 and an absent field read 0 and render the label
225
+ // byte-identically; the error/quiet-kill/merge/paused cells return earlier and never carry
226
+ // the suffix. `noChangeStreak` is on the state accumulator, so its type is on the full
227
+ // LoopState.
228
+ let cell;
198
229
  if (s.nextRunAt > Date.now()) {
199
230
  // The loop is sleeping *now* until nextRunAt: show the remaining sleep duration
200
231
  // ("for 30m"), not a future start ("in 30m"). Floor at 1s so a sub-second remainder
201
232
  // never renders as "sleeping (for now)".
202
233
  const remain = Math.max(1, secondsUntil(s.nextRunAt, Date.now()));
203
- return `sleeping (for ${humanSeconds(remain)})`;
234
+ cell = `sleeping (for ${humanSeconds(remain)})`;
204
235
  }
205
- return "queued";
206
- }
207
- /** Has main's newest merge-scope check retired a loop's stale `main_red` verdict? Only a
208
- * PASSED check stamped strictly after the loop's red tick ended counts — the fleet-level
209
- * green is newer evidence than the loop's own red (BUGS.md 2026-09-30). A failed or skipped
210
- * newest check, a check from before the red tick, or a loop with no known tick end all leave
211
- * the verdict standing. Single-homed here so loopPhase's main-red branch and its callers
212
- * cannot drift. */
213
- function mainCheckRecovered(mainCheck, s) {
214
- return mainCheck?.status === "passed"
215
- && s.lastTickEndedAt !== undefined
216
- && mainCheck.at > s.lastTickEndedAt;
236
+ else {
237
+ switch (s.skipReason) {
238
+ case "no-free-entry":
239
+ cell = `idle: no unclaimed ${baseRoleOf(s.role) === "bugfix" ? "bugs" : "plans"}`;
240
+ break;
241
+ case "claim-hold":
242
+ cell = `idle: another ${baseRoleOf(s.role)} instance is working on it`;
243
+ break;
244
+ case "deferred-backlog":
245
+ cell = "deferred (backlog open)";
246
+ break;
247
+ case "deferred-no-work":
248
+ cell = "deferred (no new work)";
249
+ break;
250
+ case "deferred-plans-full":
251
+ cell = "deferred (plans full)";
252
+ break;
253
+ default:
254
+ cell = "due";
255
+ }
256
+ }
257
+ // Two or more consecutive no-change ticks earn the suffix (N=1 is a single quiet tick —
258
+ // ordinary, not yet a pattern).
259
+ const streak = s.noChangeStreak ?? 0;
260
+ if (streak >= 2)
261
+ cell += ` · no change ×${streak}`;
262
+ return cell;
217
263
  }
218
264
  /** One loop row's display derivation — the block both observer surfaces re-ran per loop
219
265
  * (renderStatus's withMetrics map and statusPayload's loops map): a single live tail read
220
266
  * (the running-and-not-parked gate, read at the phase's progressKind), the token metrics
221
267
  * over that tail, and the rendered phase string. The phase's snapshot-level gates are
222
268
  * derived here too — budget paused (only `paused` stops a loop; `fallback` keeps it ticking
223
- * on the free model, which the header badge names), the fleet pause plus the per-role one
224
- * (one `paused` flag to loopPhase covers both), and the role's landing cell (merge queue
269
+ * on the free model, which the header badge names), the team pause plus the per-role one
270
+ * (one `paused` flag to loopPhase covers both), and the role's merge cell (merge queue
225
271
  * 4/5) — so the two dashboards' per-loop cells cannot drift apart. */
226
272
  export function loopRowCells(snap, root, s) {
227
273
  const live = s.running && !s.parkedSince
@@ -230,44 +276,60 @@ export function loopRowCells(snap, root, s) {
230
276
  const m = displayTokenMetrics(root, s, live);
231
277
  const phase = loopPhase(s, snap.running, root,
232
278
  // The per-tier budget pause (part 5c/8): a loop whose model tier resolved to no usable
233
- // free pair reads `budget paused` — per role, not the fleet-wide verdict, so a demoted
234
- // strong pair no longer paints the whole fleet paused while small and default keep
279
+ // free pair reads `budget paused` — per role, not the team-wide verdict, so a demoted
280
+ // strong pair no longer paints the whole team paused while small and default keep
235
281
  // ticking on their own pairs.
236
282
  snap.budgetPausedRoles.includes(s.role), live,
237
- // The per-role pause reads the same `paused` cell as the fleet pause: one flag to
283
+ // The per-role pause reads the same `paused` cell as the team pause: one flag to
238
284
  // loopPhase covers both gates (status-model has no per-role branch of its own).
239
- snap.paused || snap.pausedRoles.includes(s.role), landingForRole(snap.landQueue, s.role),
285
+ snap.paused || snap.pausedLoops.includes(s.role), mergeForRole(snap.mergeQueue, s.role),
240
286
  // The snapshot's newest merge-scope check lets a stale main-red phase retire itself
241
- // between ticks (BUGS.md 2026-09-30) — the banner (fleetAlerts) reads this same phase.
287
+ // between ticks (BUGS.md 2026-09-30) — the banner (teamAlerts) reads this same phase.
242
288
  snap.mainCheck,
243
289
  // The per-role cap's verdict, computed once per poll in the snapshot (status/status-data.ts
244
290
  // roleCapPaused): an idle loop over its own cap reads `cap paused` on every surface.
245
291
  snap.capPaused.includes(s.role),
246
292
  // The per-role quiet window's verdict (status/status-data.ts roleQuietPaused): an idle loop
247
- // inside its own window reads `quiet until <end>` — the fleet badge's wording, scoped.
293
+ // inside its own window reads `quiet until <end>` — the team badge's wording, scoped.
248
294
  snap.roleQuietPaused?.[s.role],
249
295
  // The disk floor's verdict (status/status-data.ts disk): an idle loop while the hold is on
250
- // reads `disk hold` — the fleet-wide gate, ranked with the budget pause.
296
+ // reads `disk hold` — the team-wide gate, ranked with the budget pause.
251
297
  snap.disk?.held === true,
252
298
  // The new-project bootstrap's verdict (status/status-data.ts bootstrapHeld): an idle loop
253
- // it holds reads `held: bootstrap` — the fleet gate's own wording.
299
+ // it holds reads `held: bootstrap` — the team gate's own wording.
254
300
  snap.bootstrapHeld.includes(s.role),
255
301
  // The maintenance allowance's verdict (status/status-data.ts maintenanceQuota): an idle loop
256
- // it holds reads `held: maintenance quota <used>/<allowance>` — the fleet gate's figures.
302
+ // it holds reads `held: maintenance quota <used>/<allowance>` — the team gate's figures.
257
303
  snap.maintenanceQuota.held.includes(s.role)
258
- ? `held: maintenance quota ${snap.maintenanceQuota.used}/${snap.maintenanceQuota.allowance}`
259
- : undefined);
304
+ ? `waiting: maintenance allowance used ${snap.maintenanceQuota.used}/${snap.maintenanceQuota.allowance}`
305
+ : undefined,
306
+ // The permit pool's live state (plans/provider-concurrency.md §8): names the parked
307
+ // waiter's provider key in `awaiting <key> slot` when that key is at its cap.
308
+ snap.permits,
309
+ // A timed per-role pause's deadline — only when the loop itself is individually paused
310
+ // (in `pausedRoles`) and not under a team-wide pause, matching the GUI's phaseDetail
311
+ // guard, so the team pause (whose countdown rides the header badge) never double-appends.
312
+ snap.paused || !snap.pausedLoops.includes(s.role) ? undefined : snap.pausedRolesUntil);
260
313
  return { live, generated: m.generated, peakCtx: m.peakCtx, phase };
261
314
  }
262
- /** The phase prefixes that make a loop a permit holder — working, reviewing, landing. The
315
+ /** The phase prefixes that make a loop a permit holder — working, reviewing, merge. The
263
316
  * browser dashboard (gui-client.ts) interpolates this list into its isActivePhase, exactly as
264
317
  * gui-client-model.ts interpolates LOOP_RANK_RULES, so the two dashboards cannot disagree
265
318
  * about which phases are in flight. */
266
- export const ACTIVE_PHASE_PREFIXES = ["working", "reviewing", "landing"];
319
+ export const ACTIVE_PHASE_PREFIXES = ["working", "reviewing", "merging"];
320
+ /** Whether `key` is the binding limit in the published permit snapshot: its cap is set and
321
+ * its in-use already meets or exceeds it, so the pool is granting nothing more on that key and
322
+ * a waiter carrying it is queued on the key, not on the team total. A key without a cap in
323
+ * the snapshot (uncapped) is never binding. One home for the rule, shared by status-model's
324
+ * `awaiting <key> slot` and the GUI chip's provider detail. */
325
+ function isBindingLimit(permits, key) {
326
+ const entry = permits.byKey[key];
327
+ return entry?.limit !== undefined && entry.inUse >= entry.limit;
328
+ }
267
329
  /** Is this phase one of the PERMIT-HOLDER in-flight states — the rows an operator counts
268
330
  * against maxConcurrent? The three labels come from loopPhase: `working …`, `reviewing …`,
269
- * and the marker-driven `landing …` (merge queue 4/5) — a vetted change waiting for the merge
270
- * slot (`vetted, awaiting merge`) runs nothing, so it is not in flight. The running director's
331
+ * and the marker-driven `merge …` (merge queue 4/5) — a approved change waiting for the merge
332
+ * slot (`approved, awaiting merge`) runs nothing, so it is not in flight. The running director's
271
333
  * `director working …` label is deliberately outside the set: the director never calls
272
334
  * `semaphore.acquire`, so the cap-mirroring rows are exactly the permit holders (BUGS.md
273
335
  * 2026-10-06). Named once so the two rendered tables and their lockstep test share the rule
@@ -281,11 +343,11 @@ export function isActivePhase(phase) {
281
343
  /** A loop's rank in the rendered tables, from its phase label (loopPhase): -1 a RUNNING
282
344
  * DIRECTOR — live work of its own, outside the maxConcurrent cap it bypasses, so it groups
283
345
  * apart and never inflates the permit-holder count (BUGS.md 2026-10-06); 0 live work —
284
- * working, reviewing, landing (isActivePhase); 1 work waiting in the pipeline — a vetted change
346
+ * working, reviewing, merge (isActivePhase); 1 work waiting in the pipeline — a approved change
285
347
  * awaiting the merge slot, or a tick parked awaiting a permit; 2 needs attention — failing, or
286
348
  * blocked by a red main; 3 paused — by the operator, by the budget, or by the loop's own
287
349
  * per-role cap; 4 everything else (idle: sleeping, queued, the director waiting for prompts, a
288
- * stopped fleet). The dashboard groups its loop table by these ranks; the TUI/status table
350
+ * stopped team). The dashboard groups its loop table by these ranks; the TUI/status table
289
351
  * orders by them.
290
352
  *
291
353
  * The rank → phase mapping is LOOP_RANK_RULES, one home for the phase literals so the
@@ -295,10 +357,10 @@ export function isActivePhase(phase) {
295
357
  export const LOOP_RANK_RULES = [
296
358
  { rank: -1, prefixes: ["director working"] },
297
359
  { rank: 0, active: true },
298
- { rank: 1, prefixes: ["vetted", "awaiting slot"] },
299
- { rank: 2, phases: ["failing", "main red"] },
300
- { rank: 3, phases: ["paused", "budget paused", "cap paused", "disk hold", "held: bootstrap"] },
301
- { rank: 3, prefixes: ["held: maintenance quota"] },
360
+ { rank: 1, prefixes: ["approved", "waiting to start", "waiting for "] },
361
+ { rank: 2, phases: ["failing", "main's check is failing"] },
362
+ { rank: 3, phases: ["paused"] },
363
+ { rank: 3, prefixes: ["paused", "waiting:"] },
302
364
  ];
303
365
  /** The rank LOOP_RANK_RULES assigns `phase`: -1 for director working, 4 for idle, otherwise the
304
366
  * first matching rule's rank. */
@@ -324,8 +386,8 @@ export function sortLoopsByState(loops) {
324
386
  const rb = loopRank(b.phase);
325
387
  if (ra !== rb)
326
388
  return ra - rb;
327
- const ta = a.lastTickEndedAt ?? 0;
328
- const tb = b.lastTickEndedAt ?? 0;
389
+ const ta = a.lastRunEndedAt ?? 0;
390
+ const tb = b.lastRunEndedAt ?? 0;
329
391
  if (ta !== tb)
330
392
  return tb - ta;
331
393
  return a.role.localeCompare(b.role);