tumwater 0.2.0 → 0.3.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 (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,35 +1,35 @@
1
1
  import { dailyCost } from "../budget/budget.js";
2
2
  import { clipToWidth, displayWidth } from "../text/text-width.js";
3
3
  import { compactTokens, usd } from "../text/format.js";
4
- import { elapsedSeconds, formatTime, humanSeconds, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
4
+ import { formatTime, humanAgo, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
5
5
  import { projectName } from "../project-name.js";
6
- import { bootstrapBadge, buildBadge, budgetBadge, claudeWindowTone, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, pauseBadge, loopWakeMarker, permitsBadge, quietBadge, runWindowBadge, wakeBadge } from "./badges.js";
7
- import { idleClaimText, isActivePhase, loopRowCells, onErrorStreak, sortLoopsByState } from "./status-model.js";
8
- import { tickProgress } from "./tick-progress-model.js";
6
+ import { bootstrapBadge, buildBadge, budgetBadge, subscriptionWindowTone, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, pauseBadge, roleWakeMarker, permitsBadge, checksBadge, quietBadge, runWindowBadge, wakeBadge, workspacesBadge } from "./badges.js";
7
+ import { idleClaimText, isActivePhase, roleRowCells, onErrorStreak, sortRolesByState } from "./status-model.js";
8
+ import { runProgress } from "./run-progress-model.js";
9
9
  import { eventTypeLabel } from "../events/event-labels.js";
10
10
  import { roleLabel } from "../roles/role-labels.js";
11
11
  import { yieldMultiplierFor, yieldMultiplierSuffix } from "../scheduling/backoff.js";
12
12
  import { phaseTone, resultTone } from "./tone.js";
13
13
  import { teamAlerts } from "./team-alerts.js";
14
14
  import { openQuestions } from "../backlog/backlog.js";
15
- import { refusedEntryTitles } from "../backlog/backlog-eligibility.js";
15
+ import { operatorHeldEntries, refusedEntries, stalledPlanEntries } from "../backlog/backlog-eligibility.js";
16
+ import { planDependencyAlert, skippedMergeFiles, staleMerges, staleOpenBugs, stalePrompts, staleWaitingBugs } from "./status-payload.js";
16
17
  /** The status RENDER layer: time/token cell formatters and the width-aware table shared by
17
18
  * `tumwater status` and the TUI. The labels, badges, and metrics it draws come from the shared
18
19
  * display model (status-model.ts); this module decides column widths and layout. Depends on
19
20
  * status/status-data.ts one way — rendering reads the snapshot; it never collects team state
20
- * itself (live tick detail is display-only). */
21
+ * itself (live run detail is display-only). */
21
22
  function ago(ts) {
22
23
  if (!ts)
23
24
  return "-";
24
- const s = elapsedSeconds(ts);
25
- return `${humanSeconds(s)} ago`;
25
+ return humanAgo(ts, Date.now());
26
26
  }
27
- /** The table's `last tick` cell: the absolute local time of the last tick end alongside its
27
+ /** The table's `last run` cell: the absolute local time of the last run end alongside its
28
28
  * relative age ("14:32:05 · 3m ago"). Zero-padded HH:MM:SS in local time, prefixed `MM-DD `
29
- * once older than a day so multi-day runs stay unambiguous; "-" for loops that never ticked.
29
+ * once older than a day so multi-day runs stay unambiguous; "-" for roles that never ran.
30
30
  * The GUI renders the same cell (absolute stamp plus relative age) from its own JS copy in
31
31
  * gui-client.ts — formatting at each surface, per the fmtTokens precedent. */
32
- export function lastTickCell(ts) {
32
+ export function lastRunCell(ts) {
33
33
  if (!ts)
34
34
  return "-";
35
35
  const d = new Date(ts);
@@ -38,42 +38,42 @@ export function lastTickCell(ts) {
38
38
  s = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${s}`;
39
39
  return `${s} · ${ago(ts)}`;
40
40
  }
41
- /** The table's `last merged` cell: when the loop last merged a change on main, in the SAME
42
- * absolute-time-plus-relative-age shape as the `last tick` cell (lastTickCell), so the two
43
- * columns cannot drift on what "ago" means. A loop that has never merged shows the same dash
44
- * lastTickCell uses for a never-ticked loop. */
41
+ /** The table's `last merged` cell: when the role last merged a change on main, in the SAME
42
+ * absolute-time-plus-relative-age shape as the `last run` cell (lastRunCell), so the two
43
+ * columns cannot drift on what "ago" means. A role that has never merged shows the same dash
44
+ * lastRunCell uses for a role that has never run. */
45
45
  export function lastMergedCell(ts) {
46
- return lastTickCell(ts);
46
+ return lastRunCell(ts);
47
47
  }
48
- /** The table's `next run` cell: when the loop may tick again, or `-` when that is not the
49
- * operator's question — a loop in flight (an active rendered phase: working/reviewing/merge,
48
+ /** The table's `next run` cell: when the role may run again, or `-` when that is not the
49
+ * operator's question — a role in flight (an active rendered phase: working/reviewing/merge,
50
50
  * or `s.running`) has no next run to speak of, and a stopped team renders no schedule at all
51
- * (its `nextRunAt`s are stale leftovers, not plans). A due idle loop reads `now`; a future one
51
+ * (its `nextRunAt`s are stale leftovers, not plans). A due idle role reads `now`; a future one
52
52
  * reads its remaining time through humanSeconds (the same bucketing the sleeping phase label
53
53
  * uses, so the two cannot drift), prefixed `backoff ` while `backoffSeconds > 0` — the wake
54
54
  * row-action clears exactly that, so the prefix is what makes waking meaningful. The GUI
55
55
  * renders the same rules from its own JS copy in gui-client.ts (fmtNextRun). The `phase` and
56
- * `teamRunning` arguments come from the caller because neither fact lives on LoopState: the
56
+ * `teamRunning` arguments come from the caller because neither fact lives on RunnerState: the
57
57
  * active-phase check needs the rendered label (a merge is visible only there), and the
58
- * team's running flag lives on the snapshot, not the loop. */
58
+ * team's running flag lives on the snapshot, not the role. */
59
59
  export function nextRunCell(s, phase, now, teamRunning) {
60
60
  if (!teamRunning || s.running || isActivePhase(phase))
61
61
  return "-";
62
62
  const remain = secondsUntil(s.nextRunAt, now);
63
63
  // The yield multiplier rides the cell as `×N` (yield-scaled clocks, PLANS.md): a scalable
64
- // role whose recent ticks merged nothing keeps a longer effective gap than nextRunAt's
64
+ // role whose recent runs merged nothing keeps a longer effective gap than nextRunAt's
65
65
  // countdown shows, and the suffix is what makes that visible. The multiplier gates the
66
66
  // scheduled gap and the main-moved wake in isEligible, so a `now`-due cell at ×4 is a
67
67
  // role waiting out its stretched gap, not a broken clock.
68
68
  const suffix = yieldMultiplierSuffix(yieldMultiplierFor(s));
69
69
  return nextRunPhrase(remain, s.backoffSeconds > 0, "now", false) + suffix;
70
70
  }
71
- /** The table's state cell: a loop's phase label (loopPhase, computed once per row by
71
+ /** The table's state cell: a role's phase label (rolePhase, computed once per row by
72
72
  * renderStatus so the same label drives the row order), with the current work item prepended
73
- * while a tick is in flight ("implement plan X · working 3m · turn 2"). Prepending — not
73
+ * while a run is in flight ("implement plan X · working 3m · turn 2"). Prepending — not
74
74
  * appending — so the item survives ellipsis clipping on narrow terminals; the live detail
75
- * after it is what gets clipped first. Idle loops are untouched: their log tail describes a
76
- * finished tick and must not leak its work item into the state cell. (The GUI shows the same
75
+ * after it is what gets clipped first. Idle roles are untouched: their log tail describes a
76
+ * finished run and must not leak its work item into the state cell. (The GUI shows the same
77
77
  * item in its own `current` column instead, so its state cell stays clean.) */
78
78
  function stateCell(root, s, phase, live) {
79
79
  // While under review the log tail's "current work" is the reviewer's own output, not the
@@ -88,18 +88,19 @@ function stateCell(root, s, phase, live) {
88
88
  const idle = idleClaimText(s, phase);
89
89
  return idle ? `${idle} · ${phase}` : phase;
90
90
  }
91
- const p = tickProgress(root, s, live);
91
+ const p = runProgress(root, s, live);
92
92
  const work = p?.currentWork;
93
93
  return work ? `${work} · ${phase}` : phase;
94
94
  }
95
95
  /** Columns allowed to shrink when the table is wider than the terminal, widest offender
96
- * first: `last result` (holds the tick summary), then `state` (live working detail), then
97
- * `last tick` — it shrinks last so on a narrow terminal it loses " · 3m ago" before whole
98
- * lines clip; its minimum is a bare HH:MM:SS. `last merged` sits right of `last tick` and is
99
- * not flexible: it is a short sibling time cell whose width follows lastTickCell's, and the
96
+ * first: `last result` (holds the run summary), then `state` (live working detail), then
97
+ * `last run` — it shrinks last so on a narrow terminal it loses " · 3m ago" before whole
98
+ * lines clip; its minimum is a bare HH:MM:SS. `last merged` sits right of `last run` and is
99
+ * not flexible: it is a short sibling time cell whose width follows lastRunCell's, and the
100
100
  * whole-line clip that already protects the header also bounds it. Indices are positional in
101
101
  * the `cols` array below — renumber when columns change. (`today`, like `cost`, is a short
102
- * fixed-width cell: never flexible.) */
102
+ * fixed-width cell: never flexible.)
103
+ * The name column (index 0) gives way after all of these, down to the longest bare role label. */
103
104
  const FLEXIBLE_COLUMNS = [
104
105
  { index: 10, minWidth: 12 },
105
106
  { index: 1, minWidth: 12 },
@@ -173,8 +174,8 @@ function badgeSpans(badge, tone) {
173
174
  /** The daily budget badge's tone: yellow from 85% of the cap, red once it is spent; the
174
175
  * subscription window's own severity (amber near its limit, red when a rejected window holds
175
176
  * the seams) wins when it stands beside the daily spend. */
176
- function budgetTone(budget, claudeWindow) {
177
- const window = claudeWindowTone(claudeWindow);
177
+ function budgetTone(budget, windows) {
178
+ const window = subscriptionWindowTone(windows);
178
179
  if (window !== undefined)
179
180
  return window;
180
181
  if (budget.free || !(budget.capUsd > 0))
@@ -183,14 +184,14 @@ function budgetTone(budget, claudeWindow) {
183
184
  return share >= 1 ? "red" : share >= 0.85 ? "yellow" : undefined;
184
185
  }
185
186
  /** Render the status table shared by `tumwater status` and the TUI as lines of toned spans,
186
- * plus the per-loop facts it computed (see StatusLoopRow). When `maxWidth` is given, wide cells
187
+ * plus the per-role facts it computed (see StatusRoleRow). When `maxWidth` is given, wide cells
187
188
  * are clipped so no line exceeds it (terminal rows never wrap). */
188
- export function renderStatusSpans(root, snap, maxWidth) {
189
+ export function renderStatusSpans(root, snap, maxWidth, scoped = false) {
189
190
  const name = projectName(root);
190
191
  const lines = [];
191
192
  // One clock read per render, shared by the timed-pause badge's countdown and every row's
192
- // next-run cell — a per-site Date.now() could tick over between them and disagree with
193
- // itself.
193
+ // next-run cell — a per-site Date.now() could tick over // retired-name-ok
194
+ // between them and disagree with itself.
194
195
  const now = Date.now();
195
196
  // The questions badge (like the inbox one) appears only when something needs an answer.
196
197
  // The budget badge is standing information for a money-spending system in EVERY cap state
@@ -212,7 +213,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
212
213
  ...badgeSpans(mergeBadge(snap.mergeQueue), "blue"),
213
214
  ...(snap.inbox ? [{ text: ` · inbox: ${snap.inbox}` }] : []),
214
215
  ...(snap.questions ? badgeSpans(` · questions: ${snap.questions}`, "magenta") : []),
215
- ...badgeSpans(budgetBadge(snap.budget, snap.claudeWindow, now, snap.chain), budgetTone(snap.budget, snap.claudeWindow)),
216
+ ...badgeSpans(budgetBadge(snap.budget, snap.subscriptionWindows, snap.subscriptionOnly, now, snap.chain), budgetTone(snap.budget, snap.subscriptionWindows)),
216
217
  // The disk floor (plans/disk-floor.md, part 4/4): `disk 8.2 GB free — holding new work`
217
218
  // while the hold is on, or the bare reading below the reclaim threshold. Empty in every
218
219
  // comfortable or unmeasured state, so an unaffected header stays byte-identical.
@@ -221,6 +222,10 @@ export function renderStatusSpans(root, snap, maxWidth) {
221
222
  // `slots 5/8 · omlx 3/3` only while the per-provider cap map is set (permitsBadge already
222
223
  // omits the providers on a map-less team, so the header stays byte-identical).
223
224
  ...badgeSpans(permitsBadge(snap.permits), "blue"),
225
+ // Checks running/waiting for a check permit; empty while the check pool is idle.
226
+ ...badgeSpans(checksBadge(snap.checkPermits), snap.checkPermits && snap.checkPermits.waiting > 0 ? "yellow" : "blue"),
227
+ // The workspace pool, only while it is full or a run waits for a slot.
228
+ ...badgeSpans(workspacesBadge(snap.slotPool), snap.slotPool && snap.slotPool.waiting > 0 ? "yellow" : "blue"),
224
229
  // New-project bootstrap (plans/work-ratio.md, part 2/2): the progress reading while the
225
230
  // hold is active. Empty in every normal project, so the header stays byte-identical.
226
231
  ...badgeSpans(bootstrapBadge(snap.bootstrap), "blue"),
@@ -238,31 +243,31 @@ export function renderStatusSpans(root, snap, maxWidth) {
238
243
  ...badgeSpans(quietBadge(snap.quietHours, snap.inQuietHours), snap.inQuietHours ? "yellow" : undefined),
239
244
  ]);
240
245
  lines.push([]);
241
- // `today` is the loop's daily budget window (dailyCost): $0.00 while its stamp is stale
242
- // or missing, so loops that never ticked — or last ticked yesterday — read zero without a
246
+ // `today` is the role's daily budget window (dailyCost): $0.00 while its stamp is stale
247
+ // or missing, so roles that never ran — or last ran yesterday — read zero without a
243
248
  // save. It renders whether or not the cap is enabled: spend observability does not depend
244
249
  // on it.
245
250
  // `next run` is a short fixed-width cell like `today`, never flexible: the columns that
246
251
  // give way when the table is wider than the terminal are `last result`, then `state`, then
247
- // `last tick`, in FLEXIBLE_COLUMNS' order.
252
+ // `last run`, in FLEXIBLE_COLUMNS' order.
248
253
  const cols = ["role", "state", "runs", "commits", "output", "peak ctx", "cost", "today", "last run", "last merged", "last result", "next run"];
249
- // One live tail read per running loop per frame, threaded through every cell that shows
254
+ // One live tail read per running role per frame, threaded through every cell that shows
250
255
  // in-flight detail (metrics, state, current work) — each helper used to re-read the log on
251
- // its own, up to three stats + reads per loop per second. The phase is computed once per
252
- // row (in loopRowCells, shared with the GUI payload) and carried on the row: the same
253
- // label drives the state cell and the shared row order (sortLoopsByState).
254
- // Per-role prompts 2/2 — a loop with queued prompts carries a small `p:N` marker on its
256
+ // its own, up to three stats + reads per role per second. The phase is computed once per
257
+ // row (in roleRowCells, shared with the GUI payload) and carried on the row: the same
258
+ // label drives the state cell and the shared row order (sortRolesByState).
259
+ // Per-role prompts 2/2 — a role with queued prompts carries a small `p:N` marker on its
255
260
  // state cell (appended, so the work-item prefix survives clipping ahead of it), telling the
256
- // operator steering one loop that something is waiting for it. A pending scheduled wake
257
- // naming the loop adds a blue ` · wake in …` marker after it.
261
+ // operator steering one role that something is waiting for it. A pending scheduled wake
262
+ // naming the role adds a blue ` · wake in …` marker after it.
258
263
  const roleQueued = (role) => {
259
264
  const n = snap.roleInbox[role] ?? 0;
260
265
  return n > 0 ? ` p:${n}` : "";
261
266
  };
262
- const withMetrics = snap.loops.map((s) => {
263
- // loopRowCells single-homes the per-loop tail read + metrics + phase derivation the GUI
267
+ const withMetrics = snap.roles.map((s) => {
268
+ // roleRowCells single-homes the per-role tail read + metrics + phase derivation the GUI
264
269
  // payload (status-payload.ts) repeats, so the two dashboards cannot drift apart.
265
- const cells = loopRowCells(snap, root, s);
270
+ const cells = roleRowCells(snap, root, s);
266
271
  return {
267
272
  s,
268
273
  m: { generated: cells.generated, peakCtx: cells.peakCtx },
@@ -272,26 +277,32 @@ export function renderStatusSpans(root, snap, maxWidth) {
272
277
  lastRunEndedAt: s.lastRunEndedAt,
273
278
  };
274
279
  });
275
- // User-defined loops (snapshot's `custom` flag) get an asterisk beside their name — the
276
- // dashboards' at-a-glance marker. The name column (index 0) is not in FLEXIBLE_COLUMNS and
277
- // its width derives from row content, so the extra character widens it automatically.
278
- // Rows are ordered by the shared display rule (status-model's loopRank, then last tick most
279
- // recent first) so the TUI/status table groups loops the way the GUI table does.
280
+ // User-defined roles (snapshot's `custom` flag) get an asterisk beside their name — the
281
+ // dashboards' at-a-glance marker. The name column (index 0) is not in FLEXIBLE_COLUMNS;
282
+ // its width derives from row content and it shrinks only after those,
283
+ // so the extra character widens it.
284
+ // Rows are ordered by the shared display rule (status-model's roleRank, then last run most
285
+ // recent first) so the TUI/status table groups roles the way the GUI table does.
280
286
  // Each cell is spans: the state cell takes its phase's tone (the p:N marker stays plain), the
281
287
  // last result tones its outcome word, a backoff countdown reads yellow.
282
288
  const cell = (text, tone) => [{ text, ...(tone ? { tone } : {}) }];
283
- const rows = sortLoopsByState(withMetrics).map(({ s, m, live, phase }) => {
289
+ // A model every role shares prints once in a footnote instead of on each name cell.
290
+ const modelInner = (s) => s.modelTier
291
+ ? `${s.modelTier}${s.model ? ` · ${s.model}` : ""}`
292
+ : s.model
293
+ ? `${s.model}${s.modelFromPi ? ", pi default" : ""}`
294
+ : "";
295
+ const inners = snap.roles.map(modelInner);
296
+ const sharedModel = snap.roles.length >= 2 && inners[0] && inners.every((m) => m === inners[0]) ? inners[0] : undefined;
297
+ const rows = sortRolesByState(withMetrics).map(({ s, m, live, phase }) => {
284
298
  // The name cell appends the selector the role resolves to; with a top-level tier map
285
299
  // declared it additionally names the seam tier (model-tiers.md part 7a,
286
300
  // "Observability"). No model resolved and no tier keeps the name byte-identical to
287
301
  // today's.
288
- const modelSuffix = s.modelTier
289
- ? ` (${s.modelTier}${s.model ? ` · ${s.model}` : ""})`
290
- : s.model
291
- ? ` (${s.model}${s.modelFromPi ? ", pi default" : ""})`
292
- : "";
293
- // Model failure fallback, part 2/2: while the loop runs off-model its name cell says so,
294
- // so the operator can see which loops the fallback pair is carrying without reading logs.
302
+ const inner = modelInner(s);
303
+ const modelSuffix = inner && !sharedModel ? ` (${inner})` : "";
304
+ // Model failure fallback, part 2/2: while the role runs off-model its name cell says so,
305
+ // so the operator can see which roles the fallback pair is carrying without reading logs.
295
306
  const fallbackSuffix = s.fallback ? " (over-cap model)" : "";
296
307
  const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix + fallbackSuffix;
297
308
  const state = stateCell(root, s, phase, live);
@@ -300,12 +311,12 @@ export function renderStatusSpans(root, snap, maxWidth) {
300
311
  // survives clipping), and clipping often leaves only it — the color still tells the state.
301
312
  const stateSpans = [{ text: state, ...(tone ? { tone } : {}) }];
302
313
  const next = nextRunCell(s, phase, now, snap.running);
303
- const wakeMark = loopWakeMarker(snap.scheduledWakes, s.role, s.instanceOf, now);
314
+ const wakeMark = roleWakeMarker(snap.scheduledWakes, s.role, s.instanceOf, now);
304
315
  const resultT = resultTone(s.lastResult);
305
316
  return [
306
317
  cell(name),
307
318
  // Merge queue 4/5 — the merge role's phase label (the marker-driven record, filtered to
308
- // this role by loopPhase's `merge` argument above) rides this same phase string.
319
+ // this role by rolePhase's `merge` argument above) rides this same phase string.
309
320
  [...stateSpans, { text: roleQueued(s.role) }, ...(wakeMark ? [{ text: wakeMark, tone: "blue" }] : [])],
310
321
  cell(String(s.runs)),
311
322
  cell(String(s.commits)),
@@ -313,7 +324,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
313
324
  cell(compactTokens(m.peakCtx)),
314
325
  cell(usd(s.totalCostUsd)),
315
326
  cell(usd(dailyCost(s))),
316
- cell(lastTickCell(s.lastRunEndedAt)),
327
+ cell(lastRunCell(s.lastRunEndedAt)),
317
328
  cell(lastMergedCell(s.lastMergedAt)),
318
329
  s.lastResult
319
330
  ? [{ text: eventTypeLabel(s.lastResult), ...(resultT ? { tone: resultT } : {}) }, { text: s.lastSummary ? ` — ${s.lastSummary}` : "" }]
@@ -328,15 +339,16 @@ export function renderStatusSpans(root, snap, maxWidth) {
328
339
  "",
329
340
  compactTokens(withMetrics.reduce((sum, { m }) => sum + m.generated, 0)),
330
341
  compactTokens(Math.max(0, ...withMetrics.map(({ m }) => m.peakCtx))),
331
- usd(snap.loops.reduce((sum, s) => sum + s.totalCostUsd, 0)),
342
+ usd(snap.roles.reduce((sum, s) => sum + s.totalCostUsd, 0)),
332
343
  // The team's today-spend, from the snapshot's budget block — the same number the header
333
344
  // badge reads, so table and badge cannot drift. While the orchestrator runs that figure is
334
- // the scheduler's live sum over its in-memory states, which can sit ABOVE the per-loop
335
- // `today` cells above (each loop's persisted copy, written only at its saves): the gap is
345
+ // the scheduler's live sum over its in-memory states, which can sit ABOVE the per-role
346
+ // `today` cells above (each role's persisted copy, written only at its saves): the gap is
336
347
  // exactly the in-flight charge the budget gate has already counted, and displaying it is
337
348
  // the point (BUGS.md 2026-09-30). Not running, the snapshot derives the block from these
338
- // same loops, so cells and total agree by construction.
339
- usd(snap.budget.spentUsd),
349
+ // same roles, so cells and total agree by construction.
350
+ // A role-scoped table adds up the rows shown instead: the team figure would not match them.
351
+ usd(scoped ? snap.roles.reduce((sum, s) => sum + dailyCost(s), 0) : snap.budget.spentUsd),
340
352
  "",
341
353
  "",
342
354
  "",
@@ -357,6 +369,11 @@ export function renderStatusSpans(root, snap, maxWidth) {
357
369
  widths[index] = current - reduction;
358
370
  overflow -= reduction;
359
371
  }
372
+ // The name column gives way last, down to the longest bare role label.
373
+ if (overflow > 0) {
374
+ const floor = Math.max(displayWidth(cols[0] ?? ""), ...snap.roles.map((s) => displayWidth(s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role))));
375
+ widths[0] = Math.max(floor, (widths[0] ?? 0) - overflow);
376
+ }
360
377
  }
361
378
  // Each cell clipped and padded to its column (padding stays untoned), the columns joined by
362
379
  // the gap, and the line's trailing padding trimmed — the plain text is exactly the aligned
@@ -390,14 +407,14 @@ export function renderStatusSpans(root, snap, maxWidth) {
390
407
  }
391
408
  return out;
392
409
  };
393
- const loops = withMetrics.map(({ s, phase, live }) => ({
410
+ const roles = withMetrics.map(({ s, phase, live }) => ({
394
411
  role: s.role,
395
412
  phase,
396
413
  errorStreak: onErrorStreak(s),
397
414
  inFlight: isActivePhase(phase),
398
415
  ...(live?.quietMs !== undefined ? { stuckMs: live.quietMs } : {}),
399
416
  ...(s.lastError ? { lastError: s.lastError } : {}),
400
- // The loop's image-input surface (plans/image-input.md "Surfaces", part 5/5): always
417
+ // The role's image-input surface (plans/image-input.md "Surfaces", part 5/5): always
401
418
  // present, so the TUI's images attention line keys on it like the dashboard's.
402
419
  imageInput: s.imageInput,
403
420
  imageReroute: s.imageRerouteLabel,
@@ -409,19 +426,21 @@ export function renderStatusSpans(root, snap, maxWidth) {
409
426
  lines.push(fmt(r));
410
427
  lines.push(separator);
411
428
  lines.push(fmt(totalsRow, "bold"));
412
- // One footnote under the table when any custom loop exists — explains the asterisk without
413
- // taking a column. Absent (byte-identical table) on a team with no user-defined loops.
414
- if (snap.loops.some((l) => l.custom))
429
+ // One footnote under the table when any custom role exists — explains the asterisk without
430
+ // taking a column. Absent (byte-identical table) on a team with no user-defined roles.
431
+ if (sharedModel)
432
+ lines.push([{ text: `model (every role): ${sharedModel}`, tone: "dim" }]);
433
+ if (snap.roles.some((l) => l.custom))
415
434
  lines.push([{ text: "* custom role", tone: "dim" }]);
416
435
  // The header line (and any residual overflow past the columns' minimums) is clipped too,
417
436
  // so no status line ever wraps in a terminal of `maxWidth` columns.
418
- return { lines: maxWidth === undefined ? lines : lines.map((l) => clipSpans(l, maxWidth)), loops };
437
+ return { lines: maxWidth === undefined ? lines : lines.map((l) => clipSpans(l, maxWidth)), roles };
419
438
  }
420
439
  /** Render the status table shared by `tumwater status` and the TUI as plain text (see
421
440
  * renderStatusSpans). When `maxWidth` is given, wide cells are clipped so no line exceeds it
422
441
  * (terminal rows never wrap). */
423
- export function renderStatus(root, snap, maxWidth) {
424
- const { lines, loops } = renderStatusSpans(root, snap, maxWidth);
442
+ export function renderStatus(root, snap, maxWidth, scoped = false) {
443
+ const { lines, roles } = renderStatusSpans(root, snap, maxWidth, scoped);
425
444
  // The team-attention banners (team-alerts.ts) drop under the header's blank line here, on
426
445
  // the CLI-only plain-text surface — the same spelled-out banners the dashboard and TUI draw,
427
446
  // from the one shared source, so the CLI/GUI/TUI cannot drift on wording. One QUESTIONS.md
@@ -431,7 +450,7 @@ export function renderStatus(root, snap, maxWidth) {
431
450
  // Composed here, NOT inside renderStatusSpans: the TUI re-slices that function's shared
432
451
  // `lines` (lines[0], its own attention block, then lines.slice(1)), so a banner embedded
433
452
  // there would render twice in the TUI.
434
- const alerts = teamAlerts(snap, openQuestions(root), loops, Date.now(), refusedEntryTitles(root)).filter((a) => a.key !== "stopped");
453
+ const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root), operatorHeldEntries(root), staleOpenBugs(root), stalePrompts(root), staleMerges(root), planDependencyAlert(root), staleWaitingBugs(root), skippedMergeFiles(root), stalledPlanEntries(root)).filter((a) => a.key !== "stopped");
435
454
  const banner = alertLines(alerts, maxWidth ?? Number.MAX_SAFE_INTEGER, alerts.length).map(plainText);
436
455
  const text = lines.map(plainText);
437
456
  return [...text.slice(0, 2), ...banner, ...text.slice(2)].join("\n");