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,7 +1,8 @@
1
+ import { imageReroute } from "../agent/image-input.js";
1
2
  import { DIRECTOR_ROLE } from "../roles/roles.js";
2
3
  import { isDirty, deleteRef } from "../git/git.js";
3
4
  import { applyConfigRequest } from "../config/config-write.js";
4
- import { diagnoseNoChange } from "../verdict/no-change.js";
5
+ import { diagnoseNoChange, extractNoChangeAnswer } from "../verdict/no-change.js";
5
6
  import { handleRefusal, refusalContradiction } from "../verdict/refusal.js";
6
7
  import { extractSummary } from "../git/commit-message.js";
7
8
  import { recordFlow } from "./qa-coverage.js";
@@ -41,7 +42,7 @@ export async function resolveTickVerdict(ctx) {
41
42
  if (request.error)
42
43
  ctx.warn(`config request rejected: ${request.error}`);
43
44
  if (request.ignored.length)
44
- ctx.warn(`config request ignored key(s): ${request.ignored.join(", ")} — only customLoops is accepted`);
45
+ ctx.warn(`config request ignored key(s): ${request.ignored.join(", ")} — only customRoles is accepted`);
45
46
  }
46
47
  }
47
48
  if (pi.quietKilled) {
@@ -68,13 +69,13 @@ export async function resolveTickVerdict(ctx) {
68
69
  }
69
70
  if (pi.timedOut) {
70
71
  s.lastError = pi.errorMessage ?? "timed out";
71
- // The request never ran to completion and no work landed: put it back so the next
72
+ // The request never ran to completion and no work merged: put it back so the next
72
73
  // tick retries it. (A killed run's half-done edits are discarded by the reset.)
73
74
  ctx.pending.requeueUnfulfilled(ctx.userPrompt);
74
75
  return { result: "error" };
75
76
  }
76
77
  // A subscription claude run the usage window rejected is a hold, not a run failure: the
77
- // fleet hold keeps every subscription claude seam from starting until `resetsAt`
78
+ // team hold keeps every subscription claude seam from starting until `resetsAt`
78
79
  // (plans/agent-backends.md §8, part 6c), and this run's session and worktree edits must
79
80
  // survive the wait — the next eligible tick resumes them (part 6e) instead of committing
80
81
  // partial work or resetting the worktree to main. The rejected-window reading is the
@@ -105,6 +106,62 @@ export async function resolveTickVerdict(ctx) {
105
106
  ctx.pending.requeueForResume(s, ctx.userPrompt);
106
107
  return { result: "aborted", resumeCause: "budget-capped" };
107
108
  }
109
+ // A run the runner killed the moment its agent read an image its model cannot see
110
+ // (plans/image-input.md "Failing the tick"): pi strips such an image from the request, so
111
+ // the agent was about to answer about a picture it never saw. The run was deliberately
112
+ // stopped before it could work on, so its half-done edits are discarded exactly as a
113
+ // timed-out run's are — nothing to resume, and the prompt re-queues to run fresh on a
114
+ // model that CAN see (part 4 adds the reroute arm here). The error backoff and streak apply
115
+ // as for any error.
116
+ if (pi.imageBlind) {
117
+ // One hop only (plans/image-input.md "Rerunning on a model that can read images"): a
118
+ // tick that ALREADY started on the image-capable target and still read blind is not
119
+ // rerouted a second time — it fails, so a misreadable image cannot bounce the loop
120
+ // between tiers forever. Set at tick start by tickPhase when it routes a tick onto the
121
+ // target (blind-read handoff or attachment preflight).
122
+ if (s.imageRerouteActive) {
123
+ s.lastError = pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`;
124
+ // No work was done (the run was killed at the first read): nothing to resume, and the
125
+ // prompt re-queues to run fresh later like any other error. The error backoff/streak
126
+ // apply as usual.
127
+ ctx.pending.requeueUnfulfilled(ctx.userPrompt);
128
+ return { result: "error" };
129
+ }
130
+ // Otherwise, when the role's tier has an image-capable reroute target, the failed work
131
+ // is not discarded: the next tick starts directly on that target over the kept worktree.
132
+ // The one-shot field carries the target seam plus the `from`/`path` facts the handoff
133
+ // note needs; the tick ends `aborted` with the `image-reroute` resume cause, and
134
+ // applyTickOutcome schedules it promptly (nextRunAt = now) while planTickStart hands the
135
+ // kept worktree off to a FRESH session on the target.
136
+ const regime = ctx.rerouteRegime ?? "primary";
137
+ const target = ctx.config !== undefined ? imageReroute(ctx.config, ctx.role, regime) : null;
138
+ if (target) {
139
+ const from = ctx.runModel ?? target.config.model ?? "the model";
140
+ s.imageReroute = {
141
+ tier: target.tier,
142
+ provider: target.config.provider,
143
+ model: target.config.model,
144
+ thinking: target.config.thinking,
145
+ from,
146
+ path: pi.imageBlind.path,
147
+ };
148
+ s.lastError = `read ${pi.imageBlind.path}, but ${from} cannot read images — rerouting to ${target.tier}`;
149
+ ctx.pending.requeueForResume(s, ctx.userPrompt);
150
+ logEventBestEffort(ctx.root, {
151
+ source: ctx.role,
152
+ type: "image_reroute",
153
+ run: s.runs,
154
+ from,
155
+ to: target.config.model,
156
+ tier: target.tier,
157
+ preflight: false,
158
+ });
159
+ return { result: "aborted", resumeCause: "image-reroute" };
160
+ }
161
+ s.lastError = pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`;
162
+ ctx.pending.requeueUnfulfilled(ctx.userPrompt);
163
+ return { result: "error" };
164
+ }
108
165
  // A refusal is a decision, not a failure: even when pi's exit was abnormal, the sentinel
109
166
  // and any note it left are the run's verdict — classify what it left behind
110
167
  // (src/verdict/refusal.ts).
@@ -120,6 +177,7 @@ export async function resolveTickVerdict(ctx) {
120
177
  else {
121
178
  return handleRefusal({
122
179
  role: ctx.role,
180
+ root: ctx.root,
123
181
  mainBranch: ctx.mainBranch,
124
182
  turns: ctx.turns,
125
183
  merge: (w, sum) => ctx.merge(w, sum),
@@ -136,7 +194,7 @@ export async function resolveTickVerdict(ctx) {
136
194
  // would ship unfinished work under a diff-derived subject and drop the resumable session
137
195
  // (BUGS.md 2026-10-06). Keep the worktree and session for the next resume.
138
196
  if (!pi.ok && (!changed || !pi.producedAssistantContent)) {
139
- s.lastError = pi.errorMessage ?? "pi failed";
197
+ s.lastError = pi.errorMessage ?? "the agent failed";
140
198
  // A dirty worktree on a run that added nothing of its own is a prior tick's work: arm the
141
199
  // resume so the next tick continues that session instead of resetting the edits. A role
142
200
  // loop re-queues through requeueForResume, so the resume reclaims exactly this prompt
@@ -148,7 +206,7 @@ export async function resolveTickVerdict(ctx) {
148
206
  ctx.pending.requeueForResume(s, ctx.userPrompt);
149
207
  }
150
208
  else {
151
- // No work landed, so the request was not fulfilled: re-queue it. A no_change outcome
209
+ // No work merged, so the request was not fulfilled: re-queue it. A no_change outcome
152
210
  // IS fulfillment (a question-type prompt answered without file changes) — never
153
211
  // re-queue that, or such prompts would loop forever.
154
212
  ctx.pending.requeueUnfulfilled(ctx.userPrompt);
@@ -170,9 +228,9 @@ export async function resolveTickVerdict(ctx) {
170
228
  await resetWorktreeToMain(ctx.wt, ctx.mainBranch);
171
229
  await deleteRef(ctx.root, rejectedRefName(ctx.role));
172
230
  // The drop's durable effects — the worktree reset, the rejected ref's deletion, and the
173
- // cleared state — have all landed; the event is observability, so an unwritable feed must
231
+ // cleared state — have all taken effect; the event is observability, so an unwritable feed must
174
232
  // not end the tick as an error and mask a change that really was dropped.
175
- logEventBestEffort(ctx.root, { loop: ctx.role, type: "revision", action: "dropped", round: ctx.revisionRound, sha });
233
+ logEventBestEffort(ctx.root, { source: ctx.role, type: "revision", action: "dropped", round: ctx.revisionRound, sha });
176
234
  return { result: "no_change" };
177
235
  }
178
236
  if (!changed) {
@@ -181,7 +239,7 @@ export async function resolveTickVerdict(ctx) {
181
239
  // diagnosable on its own.
182
240
  const diagnosis = diagnoseNoChange(pi);
183
241
  if (!pi.nothingToDo) {
184
- ctx.warn(`pi finished without changes and without declaring nothing-to-do` +
242
+ ctx.warn(`the agent finished without changes and without declaring nothing-to-do` +
185
243
  (diagnosis.notes.length ? ` (${diagnosis.notes.join(", ")})` : ""));
186
244
  }
187
245
  // A cut-off run did real work and was NOT fulfilled: a director prompt goes back
@@ -194,7 +252,15 @@ export async function resolveTickVerdict(ctx) {
194
252
  // rotation past a check that did not complete. Only a run that was not cut off records.
195
253
  if (ctx.flow && !diagnosis.cutOff)
196
254
  recordFlow(ctx.root, ctx.flow.flow, ctx.flow.result, ctx.flow.result === "bug" ? extractSummary(pi.finalText) ?? undefined : undefined);
197
- return { result: "no_change", cutOff: diagnosis.cutOff || undefined };
255
+ // A prompt-driven (director) tick that answers a question in prose ends `no_change` with
256
+ // no SUMMARY block, so its reply would be lost — the dashboard reads a bare "No change"
257
+ // and the feed carries nothing (BUGS.md 2026-10-14). Keep the prose above the sentinel as
258
+ // the tick's summary so `lastSummary` and the run_end event show the answer where the
259
+ // operator asked the question. A role loop's bare nothing-to-do stays summary-less.
260
+ const summary = ctx.userPrompt !== null && !diagnosis.cutOff
261
+ ? (extractNoChangeAnswer(pi.finalText) ?? undefined)
262
+ : undefined;
263
+ return { result: "no_change", cutOff: diagnosis.cutOff || undefined, summary };
198
264
  }
199
265
  return null;
200
266
  }
@@ -1,19 +1,21 @@
1
- import { claudeWindowLabel } from "../agent/claude/claude-account.js";
1
+ import { claudeUtilizationPct, claudeWindowLabel } from "../agent/claude/claude-account.js";
2
+ import { MODEL_TIERS } from "../config/config-schema.js";
2
3
  import { budgetGate, budgetReached, fallbackTierEntries } from "../budget/budget.js";
4
+ import { fallbackDisplayPair } from "../config/config-views.js";
3
5
  import { quietWindowEnd } from "../scheduling/quiet-hours.js";
4
- import { humanSeconds, pad2, secondsSince, secondsUntil } from "../text/datetime.js";
5
- import { pauseReasonSuffix } from "../text/phrases.js";
6
+ import { clockHm, humanUntil } from "../text/datetime.js";
7
+ import { pauseReasonSuffix, plural } from "../text/phrases.js";
6
8
  import { formatGB, shortSha, usd, usdCap } from "../text/format.js";
7
9
  /** The status header's BADGE fragments, phrased once for both observer surfaces: the terminal
8
10
  * table (status-render.ts's renderStatus header) and the JSON/GUI payload
9
11
  * (status-payload.ts's preformatted badge fields). Split from status-model.ts, which keeps the
10
- * per-loop display model (phase, rows, token metrics) — the badges are header-scale fleet
11
- * facts (build, main check, budget, land queue, timed pause) rather than per-loop state, and
12
+ * per-loop display model (phase, rows, token metrics) — the badges are header-scale team
13
+ * facts (build, main check, budget, merge queue, timed pause) rather than per-loop state, and
12
14
  * their one-home rule ("the page cannot re-derive and drift from these strings") is easier to
13
15
  * see when the module is exactly the badges. The duration phrasing they share (humanSeconds)
14
16
  * lives in datetime.ts — core modules need it too, so it sits below the ui layer. */
15
17
  /** The header's build fragment: which commit the running harness was compiled from and, when
16
- * main's build inputs have moved past it, how far — the fleet is then executing code main no
18
+ * main's build inputs have moved past it, how far — the team is then executing code main no
17
19
  * longer describes (auto-restart lands the new build; off, restart `tumwater run` by hand).
18
20
  * A stale build also says what auto-restart is doing about it: `restart pending` resolves
19
21
  * itself, `restart BLOCKED` never will until main moves, and telling them apart at a glance is
@@ -22,11 +24,11 @@ import { formatGB, shortSha, usd, usdCap } from "../text/format.js";
22
24
  export function buildBadge(build) {
23
25
  if (!build)
24
26
  return "";
25
- const stale = build.stale ? ` — STALE: main +${build.aheadCommits ?? 0} commit(s) since` : "";
27
+ const stale = build.stale ? ` — update available (${plural(build.aheadCommits ?? 0, "commit")} behind)` : "";
26
28
  const restart = build.restartBlocked
27
- ? `; restart BLOCKED: ${build.restartBlocked}`
29
+ ? `; restart blocked: ${build.restartBlocked}`
28
30
  : build.restartPending
29
- ? "; restart pending"
31
+ ? " — restarts when idle"
30
32
  : "";
31
33
  return `, build ${shortSha(build.sha)}${stale}${restart}`;
32
34
  }
@@ -52,7 +54,7 @@ export function mainCountsFragment(counts) {
52
54
  * its raw status word with `fallbackTone` for color — callers pick the tone their palette
53
55
  * uses (the TUI's status header renders yellow, the GUI sidebar amber). The verdict word
54
56
  * never reads green for an unverified tree. Readers: mainCheckBadge (below) and the header
55
- * badge tone in status-render.ts. The GUI sidebar's status row (gui-client-fleet.ts) ships
57
+ * badge tone in status-render.ts. The GUI sidebar's status row (gui-client-team.ts) ships
56
58
  * its own copy of the mapping — it is browser-side JS spliced into gui-client.ts and cannot
57
59
  * import this module; its three-line ternary block is deliberately left as is. */
58
60
  export function mainCheckVerdict(status, fallbackTone = status) {
@@ -67,7 +69,7 @@ export function mainCheckVerdict(status, fallbackTone = status) {
67
69
  /** Main's newest merge-scope check as a header badge (PLANS.md "Retire the README freshness
68
70
  * stamp"): `· main <sha>: green · N/N (N skipped)` — the live replacement for the committed
69
71
  * README stamp the readme role used to maintain. Empty when the snapshot carries no check
70
- * (none has run yet), so a quiet header stays byte-identical to a pre-check fleet. The
72
+ * (none has run yet), so a quiet header stays byte-identical to a pre-check team. The
71
73
  * verdict maps passed→green, failed→red; anything else (a skipped scope check) renders its
72
74
  * raw word — an unverified tree must not read green. The counts fragment (mainCountsFragment)
73
75
  * omits parentheticals for zero counts, matching the stamp wording the README carried. */
@@ -80,11 +82,11 @@ export function mainCheckBadge(mainCheck) {
80
82
  const sha = mainCheck.sha ? `${shortSha(mainCheck.sha)}: ` : "";
81
83
  return ` · main ${sha}${verdict}${counts}`;
82
84
  }
83
- /** The fleet's current budget-gate state, derived from a snapshot's budget block — the header
84
- * badge's default-tier story (part 5c/8): the badge names the fleet's bulk fallback, so it
85
+ /** The team's current budget-gate state, derived from a snapshot's budget block — the header
86
+ * badge's default-tier story (part 5c/8): the badge names the team's bulk fallback, so it
85
87
  * reads the `default` tier's verdict alone (the per-tier pause lives on the loop rows, via
86
88
  * status-data.ts's budgetPausedRoles). */
87
- function fleetBudgetGate(budget) {
89
+ function teamBudgetGate(budget) {
88
90
  return budgetGate(budgetReached(budget), { default: budget.fallback !== null, strong: budget.fallback !== null }, false);
89
91
  }
90
92
  /** The new-project bootstrap's progress fragment (plans/work-ratio.md, part 2/2):
@@ -103,32 +105,63 @@ function claudeWindowStatusWord(status) {
103
105
  return "limited";
104
106
  return status === "reset" ? "reset" : "ok";
105
107
  }
106
- /** The local wall-clock `HH:MM` of a window instant, the same hours/minutes-only shape the
107
- * burn-rate forecast uses. */
108
- function clockHm(ms) {
109
- const d = new Date(ms);
110
- return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
108
+ /** How far off a window's reset is, two units wide: `4h 12m`, `5d 3h`, `37m`. A clock time
109
+ * alone cannot tell a 7d window resetting Monday from one resetting tonight. */
110
+ function untilReset(resetsAt, now) {
111
+ const minutes = Math.floor((resetsAt - now) / 60_000);
112
+ if (minutes < 60)
113
+ return `${minutes}m`;
114
+ const hours = Math.floor(minutes / 60);
115
+ if (hours < 24)
116
+ return `${hours}h ${minutes % 60}m`;
117
+ return `${Math.floor(hours / 24)}d ${hours % 24}h`;
111
118
  }
112
119
  /** The subscription usage-window fragment (plans/agent-backends.md §8, PLANS.md part 6d/7):
113
- * `· Claude 5h · 64% · resets 14:30`, with the status word when no `utilization` was reported
114
- * and an `overage` marker while the account spends into overage. A rejected window reads
115
- * `claude seams held until 14:30` in place of `resets` while a future deadline holds the
120
+ * `· Claude 5h · 64% · resets in 4h 12m` (`resets now` once the deadline has passed), with
121
+ * the status word when no `utilization` was reported and an `overage` marker while the
122
+ * account spends into overage. A rejected window reads
123
+ * `claude model jobs wait until 14:30` in place of `resets` while a future deadline holds the
116
124
  * seams; a reset reading names neither. The one home of the string, so the TUI header and the
117
125
  * payload's preformatted badge cannot drift. */
118
- function claudeWindowBadge(claudeWindow) {
126
+ function claudeWindowBadge(claudeWindow, now, moved = false) {
119
127
  const w = claudeWindow.primary;
120
128
  const parts = [`Claude ${claudeWindowLabel(w.type)}`];
121
- parts.push(typeof w.utilization === "number" ? `${Math.round(w.utilization * 100)}%` : claudeWindowStatusWord(w.status));
129
+ parts.push(typeof w.utilization === "number" ? `${claudeUtilizationPct(w.utilization)}%` : claudeWindowStatusWord(w.status));
122
130
  if (w.isUsingOverage === true)
123
131
  parts.push("overage");
124
132
  if (w.status === "rejected" && claudeWindow.heldUntil !== null) {
125
- parts.push(`claude seams held until ${clockHm(claudeWindow.heldUntil)}`);
133
+ parts.push(`${moved ? "on fallback" : "claude model jobs wait"} until ${clockHm(claudeWindow.heldUntil)}`);
126
134
  }
127
135
  else if (w.status !== "reset" && typeof w.resetsAt === "number") {
128
- parts.push(`resets ${clockHm(w.resetsAt)}`);
136
+ parts.push(w.resetsAt - now < 60_000 ? "resets now" : `resets in ${untilReset(w.resetsAt, now)}`);
129
137
  }
130
138
  return ` · ${parts.join(" · ")}`;
131
139
  }
140
+ /** Whether any tier of a chain block runs on a fallback because the Claude window (or its
141
+ * reserve) is used up. */
142
+ function chainMovedByWindow(chain) {
143
+ return Object.values(chain ?? {}).some((t) => t.reason === "window" || t.reason === "reserve");
144
+ }
145
+ /** The tiers' fallback steps as badge text, ` · `-joined without a leading separator:
146
+ * `default → DeepSeek-V4-Flash $3.10/$5 until 17:40`, one fragment per tier in MODEL_TIERS
147
+ * order. Empty when the block is absent or empty. */
148
+ export function chainFragments(chain) {
149
+ const out = [];
150
+ for (const tier of MODEL_TIERS) {
151
+ const t = chain?.[tier];
152
+ if (!t)
153
+ continue;
154
+ const money = t.spentUsd !== undefined && t.capUsd !== undefined ? ` ${usd(t.spentUsd)}/${usdCap(t.capUsd)}` : "";
155
+ const until = t.returnsAt !== undefined ? ` until ${clockHm(t.returnsAt)}` : "";
156
+ out.push(`${tier} → ${t.pair}${money}${until}`);
157
+ }
158
+ return out.join(" · ");
159
+ }
160
+ /** The chain block as a header fragment: ` · ` plus chainFragments, empty without a chain. */
161
+ export function chainBadge(chain) {
162
+ const text = chainFragments(chain);
163
+ return text ? ` · ${text}` : "";
164
+ }
132
165
  /** The window fragment's tone rank, for the header badge and the GUI meter: a rejected window
133
166
  * is red, an allowed_warning amber, anything else no tone. */
134
167
  export function claudeWindowTone(claudeWindow) {
@@ -141,8 +174,8 @@ export function claudeWindowTone(claudeWindow) {
141
174
  return undefined;
142
175
  }
143
176
  /** The header's daily-cost-budget fragment, standing in every cap state (the badge is also
144
- * the affordance for editing the cap, so a disabled fleet needs it too): `· budget: n/a`
145
- * for a fleet whose models are all free (spend can never accumulate against a cap that
177
+ * the affordance for editing the cap, so a disabled team needs it too): `· budget: n/a`
178
+ * for a team whose models are all free (spend can never accumulate against a cap that
146
179
  * cannot be reached — and no priced model means no daily spend to count, hence no "today"
147
180
  * — checked first, in every cap state), `· budget: $X.XX/$Y today` while enabled with
148
181
  * priced models, and `· budget: $X today · no cap` when disabled. One
@@ -160,16 +193,16 @@ export function claudeWindowTone(claudeWindow) {
160
193
  * needs no forecast slot: a disabled cap never carries a capHitAt (projectCapHit returns null
161
194
  * for capUsd <= 0), and the fallback fragment only stands once the cap IS reached — the one
162
195
  * state where the forecast is itself null — so the fragments can never compete for the line. */
163
- export function budgetBadge(budget, claudeWindow) {
164
- const window = claudeWindow ? claudeWindowBadge(claudeWindow) : "";
196
+ export function budgetBadge(budget, claudeWindow, now = Date.now(), chain) {
197
+ const window = (claudeWindow ? claudeWindowBadge(claudeWindow, now, chainMovedByWindow(chain)) : "") + chainBadge(chain);
165
198
  // Subscription-only: the window stands IN PLACE of the daily spend — there is no priced
166
199
  // spend for a dollar figure to report. Mixed and none fall through to the daily spend.
167
200
  if (claudeWindow?.only)
168
201
  return window;
169
202
  if (budget.free)
170
203
  return ` · budget: n/a${window}`;
171
- // Only while the fallback is actually carrying the fleet (plans/fallback-model.md): the cap
172
- // is spent, so the dollar figure alone would read like a stopped fleet. Naming the model
204
+ // Only while the fallback is actually carrying the team (plans/fallback-model.md): the cap
205
+ // is spent, so the dollar figure alone would read like a stopped team. Naming the model
173
206
  // answers the operator's next question — what is it running on now? Its cost is n/a by
174
207
  // construction (the gate engages nothing else), so no second figure is shown. When the
175
208
  // snapshot carries the per-tier map (tiers, part 7b/8 — non-null exactly when the tiers
@@ -179,12 +212,12 @@ export function budgetBadge(budget, claudeWindow) {
179
212
  // identical to before either way.
180
213
  const tierEntries = fallbackTierEntries(budget.tiers);
181
214
  // The backend-default label when the engaged pair names neither half: `<agent> default`
182
- // (plans/agent-backends.md part 4f/7), `pi default` for a pi fleet as before.
183
- const agentDefault = `${budget.agent && budget.agent !== "pi" ? budget.agent : "pi"} default`;
184
- const fallback = fleetBudgetGate(budget) === "fallback"
215
+ // (plans/agent-backends.md part 4f/7), `pi default` for a pi team as before.
216
+ const agentDefault = `${budget.backend && budget.backend !== "pi" ? budget.backend : "pi"} default`;
217
+ const fallback = teamBudgetGate(budget) === "fallback"
185
218
  ? tierEntries.length >= 2
186
- ? ` · fallback: ${tierEntries.join(", ")} (cost n/a)`
187
- : ` · fallback: ${budget.fallback?.model ?? budget.fallback?.provider ?? agentDefault} (cost n/a)`
219
+ ? ` · over-cap model: ${tierEntries.join(", ")} (cost n/a)`
220
+ : ` · over-cap model: ${fallbackDisplayPair(budget.fallback, agentDefault)} (cost n/a)`
188
221
  : "";
189
222
  const forecast = budget.capHitAt !== null
190
223
  ? ` · ~cap at ${clockHm(budget.capHitAt)}`
@@ -193,13 +226,20 @@ export function budgetBadge(budget, claudeWindow) {
193
226
  return ` · budget: ${usd(budget.spentUsd)}/${usdCap(budget.capUsd)} today${forecast}${fallback}${window}`;
194
227
  return ` · budget: ${usd(budget.spentUsd)} today · no cap${window}`;
195
228
  }
196
- /** The header's land-queue fragment (plans/merge-queue.md 4/5): `· land queue: N` while any
197
- * landing is queued or in flight, empty when the queue is idle — so an idle fleet keeps
229
+ /** The header's merge queue fragment (plans/merge-queue.md 4/5): `· merge queue: N` while any
230
+ * merge is queued or in flight, empty when the queue is idle — so an idle team keeps
198
231
  * every existing header byte intact. One home for the rule, like buildBadge and budgetBadge:
199
232
  * renderStatus renders it in the TUI/status header and status-payload.ts ships its output
200
233
  * preformatted, so the GUI page cannot drift from this string. */
201
- export function landingBadge(landQueue) {
202
- return landQueue.depth > 0 ? ` · land queue: ${landQueue.depth}` : "";
234
+ export function mergeBadge(mergeQueue) {
235
+ return mergeQueue.depth > 0 ? ` · merge queue: ${mergeQueue.depth}` : "";
236
+ }
237
+ /** The header's `run --for` window fragment: ` · stops in <duration>` while the deadline is
238
+ * ahead, ` · stopping` once it has passed (the drain), empty for an unwindowed team. */
239
+ export function runWindowBadge(stopsAt, now) {
240
+ if (stopsAt === undefined)
241
+ return "";
242
+ return stopsAt > now ? ` · stops in ${humanUntil(stopsAt, now)}` : " · stopping";
203
243
  }
204
244
  /** The header's scheduled-wake fragment: `· wake scheduled in <duration> (<roles>)` while a
205
245
  * `wake --in` marker stands, empty otherwise. It exists because a scheduled wake is otherwise
@@ -207,7 +247,7 @@ export function landingBadge(landQueue) {
207
247
  export function wakeBadge(wake, now) {
208
248
  if (!wake || wake.atMs <= now)
209
249
  return "";
210
- return ` · wake scheduled in ${humanSeconds(secondsUntil(wake.atMs, now))} (${wake.roles.join(", ")})`;
250
+ return ` · wake scheduled in ${humanUntil(wake.atMs, now)} (${wake.roles.join(", ")})`;
211
251
  }
212
252
  /** One loop's own pending-wake marker for its state cell: ` · wake in <duration>` for the
213
253
  * earliest future wake naming `role` (or `instanceOf`, the base role an extra instance runs),
@@ -221,13 +261,13 @@ export function loopWakeMarker(wakes, role, instanceOf, now) {
221
261
  if (at === undefined || w.atMs < at)
222
262
  at = w.atMs;
223
263
  }
224
- return at === undefined ? "" : ` · wake in ${humanSeconds(secondsUntil(at, now))}`;
264
+ return at === undefined ? "" : ` · wake in ${humanUntil(at, now)}`;
225
265
  }
226
- /** The header's fleet timed-pause fragment: `· paused — auto-resumes in <duration>` while a
266
+ /** The header's team timed-pause fragment: `· paused — auto-resumes in <duration>` while a
227
267
  * timed pause stands, empty otherwise — so an operator can tell at a glance whether a paused
228
- * fleet will come back on its own or needs a manual `resume` (the Timed pause plan left the
229
- * display out on purpose). The snapshot's `pausedUntil` is the FLEET marker's deadline (ms
230
- * epoch, plans/daily-cost-budget.md item 5), so the badge stands exactly when the fleet
268
+ * team will come back on its own or needs a manual `resume` (the Timed pause plan left the
269
+ * display out on purpose). The snapshot's `pausedUntil` is the TEAM marker's deadline (ms
270
+ * epoch, plans/daily-cost-budget.md item 5), so the badge stands exactly when the team
231
271
  * itself is timed-paused: a role-only timed pause leaves the header unchanged, and an absent
232
272
  * or already-expired deadline renders nothing — the read side treats an expired marker as
233
273
  * unpaused, so the badge never claims a countdown that is over. The duration goes through
@@ -249,10 +289,10 @@ export function pauseBadge(pausedUntil, now, reason) {
249
289
  return "";
250
290
  return left ? ` · paused — auto-resumes in ${left}${why}` : ` · paused${why}`;
251
291
  }
252
- /** The countdown a timed fleet pause has left, as humanSeconds — `null` when there is none
292
+ /** The countdown a timed team pause has left, as humanSeconds — `null` when there is none
253
293
  * (no deadline, or an already-expired one: the read side treats an expired marker as
254
- * unpaused). The one home of the guard + ms→seconds rounding behind the fleet's pause
255
- * countdowns, shared by pauseBadge's header fragment and fleet-alerts.ts's paused alert
294
+ * unpaused). The one home of the guard + ms→seconds rounding behind the team's pause
295
+ * countdowns, shared by pauseBadge's header fragment and team-alerts.ts's paused alert
256
296
  * title, so the two surfaces cannot disagree about how much time remains. The guard is this
257
297
  * function's own; the ms→seconds rounding rides datetime.ts's secondsUntil. The GUI's pause
258
298
  * control recomputes the ticking number client-side from the payload's raw `pausedUntil`
@@ -260,19 +300,19 @@ export function pauseBadge(pausedUntil, now, reason) {
260
300
  export function pauseCountdown(pausedUntil, now) {
261
301
  if (pausedUntil === undefined || pausedUntil <= now)
262
302
  return null;
263
- return humanSeconds(secondsUntil(pausedUntil, now));
303
+ return humanUntil(pausedUntil, now);
264
304
  }
265
305
  /** The header's quiet-hours fragment (plans: "Quiet hours … part 2/2, observability"): the
266
306
  * schedule as standing information in every configured state — `· quiet until 07:00` while
267
307
  * the local clock sits inside the window (the active reading answers "why are the loops
268
308
  * idle?" with when they start again) and `· quiet 23:00-07:00` otherwise — and empty when
269
- * the snapshot carries no window, so a fleet without the schedule keeps its header
309
+ * the snapshot carries no window, so a team without the schedule keeps its header
270
310
  * byte-identical. The window end comes from quiet-hours.ts's quietWindowEnd — read back out of
271
311
  * the window string, not re-parsed into minutes — so the badge renders the operator's own
272
312
  * spelling.
273
313
  * renderStatus renders it in the TUI/status header after the pause badge; the GUI page
274
314
  * renders its sidebar chip from the payload's raw fields and its active indicator from
275
- * fleet-alerts' quiet alert, so both dashboards say the same thing. */
315
+ * team-alerts' quiet alert, so both dashboards say the same thing. */
276
316
  export function quietBadge(quietHours, inQuietHours) {
277
317
  if (!quietHours)
278
318
  return "";
@@ -281,25 +321,36 @@ export function quietBadge(quietHours, inQuietHours) {
281
321
  }
282
322
  /** The header's disk-floor fragment (plans/disk-floor.md, part 4/4): shown while the disk hold
283
323
  * is on (`— holding new work`) or free space sits below the reclaim threshold, so an operator
284
- * sees why the fleet stopped without reading events. The most recent reclaim's freed size and
285
- * age ride the same fragment whenever one has run — standing information, so an operator
286
- * watching a volume recover can see what the clean bought even after space rises above the
287
- * threshold. Empty when the snapshot carries no disk block (an older orchestrator, or an
288
- * unmeasurable volume) and space is comfortable with no reclaim on record — a quiet header
289
- * reads exactly as before. One home for the rule, like budgetBadge: renderStatus renders it in
290
- * the TUI/status header and status-payload.ts ships it preformatted. `now` pins the reclaim
291
- * age (the same elapsed-seconds rule status-render.ts's own `ago` uses). */
292
- export function diskBadge(disk, now = Date.now()) {
324
+ * sees why the team stopped without reading events. A finished reclaim is not standing
325
+ * information here; it surfaces as a `disk_reclaim` item in the Activity log's Notable feed.
326
+ * Empty when the snapshot carries no disk block (an older orchestrator, or an unmeasurable
327
+ * volume) and space is comfortable. One home for the rule, like budgetBadge: renderStatus
328
+ * renders it in the TUI/status header and status-payload.ts ships it preformatted. */
329
+ export function diskBadge(disk) {
293
330
  if (!disk)
294
331
  return "";
295
332
  const free = `${formatGB(disk.freeGB)} free`;
296
- const reading = disk.held
333
+ return disk.held
297
334
  ? ` · disk ${free} — holding new work`
298
335
  : disk.reclaimGB > 0 && disk.freeGB < disk.reclaimGB
299
336
  ? ` · disk ${free}`
300
337
  : "";
301
- const reclaim = disk.lastReclaim
302
- ? ` · last reclaim freed ${formatGB(disk.lastReclaim.freedGB)} ${humanSeconds(secondsSince(disk.lastReclaim.at, now))} ago`
303
- : "";
304
- return reading + reclaim;
338
+ }
339
+ /** The permit pool's header fragment (plans/provider-concurrency.md §8): `slots 5/8 · omlx
340
+ * 3/3 · claude 2/6` — the team total's in-use/cap followed by each capped provider key's own
341
+ * in-use/cap. The WHOLE fragment appears only while at least one key carries a cap (the
342
+ * `maxConcurrentPerProvider` map is set), so a team without it renders the header exactly
343
+ * as before; uncapped keys that happen to be in the snapshot are dropped rather than shown
344
+ * with a bare in-use that would read like a cap. */
345
+ export function permitsBadge(permits) {
346
+ if (!permits)
347
+ return "";
348
+ const providers = Object.entries(permits.byKey)
349
+ .filter(([, e]) => e.limit !== undefined)
350
+ .sort(([a], [b]) => a.localeCompare(b))
351
+ .map(([k, e]) => `${k} ${e.inUse}/${e.limit}`)
352
+ .join(" · ");
353
+ if (providers.length === 0)
354
+ return "";
355
+ return ` · running ${permits.inUse} of ${permits.limit} · ${providers}`;
305
356
  }