tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { callBuffersOutput } from "../pi/command-shape.js";
1
2
  import { readLiveProgress } from "./progress-data.js";
2
3
  import { compactTokens } from "../text/format.js";
3
4
  import { pad2 } from "../text/datetime.js";
@@ -56,13 +57,14 @@ function progressOfRun(s, p) {
56
57
  return { ...p, quietMs: Math.min(p.quietMs, elapsed) };
57
58
  }
58
59
  /** The stall phrases a phase label can carry — inFlightDetail appends "tool call stalled:
59
- * <tool>" and "no agent output for <duration>" to the head — as a regex SOURCE string plus the
60
+ * <tool>", "no model reply for <duration>", and "no agent output for <duration>" to the head — as
61
+ * a regex SOURCE string plus the
60
62
  * compiled matcher, single-homed beside the code that renders those phrases. Every consumer
61
63
  * that must recognize a stall inside a rendered phase label goes through these two, so the
62
64
  * server-side alert (team-alert-status.ts) and the dashboard's browser twin (gui-client-model.ts,
63
65
  * interpolating the source into its String.raw script) cannot drift from the wording the cell
64
66
  * actually renders. */
65
- export const STALL_SOURCE = String.raw `tool call stalled[^·]*|no agent output for [^·]*`;
67
+ export const STALL_SOURCE = String.raw `tool call stalled[^·]*|no model reply for [^·]*|no agent output for [^·]*`;
66
68
  /** STALL_SOURCE compiled — the server-side matcher (team-alert-status.ts's stuck alert). */
67
69
  export const STALL_RE = new RegExp(STALL_SOURCE);
68
70
  /** The shared parts assembly for an in-flight state cell: `head` (the phase label with its
@@ -83,9 +85,23 @@ export function inFlightDetail(head, p) {
83
85
  else if (p.lastTool)
84
86
  parts.push(p.lastTool);
85
87
  // Silence under five minutes is normal (slow local-model prefills, long tool calls);
86
- // only flag a stall once at least five minutes have passed without any pi output.
87
- if (p.quietMs >= 300_000)
88
+ // only flag a stall once at least five minutes have passed without any pi output. While a
89
+ // tool call that buffers its output (a redirect or pipe — the run prompt's prescribed
90
+ // `> check.log` verify shape) is open, that flat threshold would cry wolf inside the call's
91
+ // own generous wall-clock stall window: the silence is the prescribed shape, not a hang,
92
+ // and a real stall past that window has already named itself as `tool call stalled: <tool>`
93
+ // above. Defer to the call so a slow suite stops being read as a hang (BUGS.md 2026-10-28).
94
+ const hasOpenBuffered = p.openToolCalls?.some((call) => callBuffersOutput(p.agent, call.command || call.label)) ?? false;
95
+ // A model request left unanswered past the stall threshold names the cause instead: the
96
+ // provider has not replied (BUGS.md 2026-10-29). pi's retries write to the log, so the bare
97
+ // silence measure restarts with each attempt, while this one spans the whole wait.
98
+ if (p.unansweredReply) {
99
+ const { waitedMs, attempts } = p.unansweredReply;
100
+ parts.push(`no model reply for ${duration(waitedMs)}${attempts > 1 ? ` (${attempts} attempts)` : ""}`);
101
+ }
102
+ else if (p.quietMs >= 5 * 60_000 && !hasOpenBuffered) {
88
103
  parts.push(`no agent output for ${duration(p.quietMs)}`);
104
+ }
89
105
  return parts.join(" · ");
90
106
  }
91
107
  /** Which pi run kind (progress-data.ts's ProgressRunKind) a role's in-flight cells should read:
@@ -84,7 +84,7 @@ export function onErrorStreak(s) {
84
84
  return (errorStreakAtWarn(s) ||
85
85
  (s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT));
86
86
  }
87
- export function rolePhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, merge, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold, permits, pausedUntilMs) {
87
+ export function rolePhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, merge, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold, pausedUntilMs) {
88
88
  if (!orchestratorRunning)
89
89
  return "stopped";
90
90
  // Merge queue 4/5 — the marker-driven merge label: only a role whose change record was
@@ -114,16 +114,6 @@ export function rolePhase(s, orchestratorRunning, root, budgetPaused = false, li
114
114
  // parked waiter visibly does not (BUGS.md 2026-09-24).
115
115
  if (s.parkedSince) {
116
116
  const parkedTime = duration(Date.now() - s.parkedSince);
117
- // The parked waiter's provider key, named only while that key is the binding limit in
118
- // the published permit snapshot (plans/provider-concurrency.md §8): the key is at its
119
- // cap, so THIS waiter is queued on its provider, not on the team total — `awaiting
120
- // omlx slot` tells the operator exactly which cap is full. A role parked on the total
121
- // (no key, or an uncapped one) reads today's `awaiting slot` as before.
122
- if (s.parkedOn !== undefined &&
123
- permits !== undefined &&
124
- isBindingLimit(permits, s.parkedOn)) {
125
- return `waiting for ${s.parkedOn} ${parkedTime}`;
126
- }
127
117
  return `waiting to start ${parkedTime}`;
128
118
  }
129
119
  // The director bypasses maxConcurrent (`usesSlot` is false for it), so its in-flight run
@@ -322,9 +312,6 @@ export function roleRowCells(snap, root, s) {
322
312
  snap.maintenanceQuota.held.includes(s.role)
323
313
  ? `waiting: maintenance allowance used ${snap.maintenanceQuota.used}/${snap.maintenanceQuota.allowance}`
324
314
  : undefined,
325
- // The permit pool's live state (plans/provider-concurrency.md §8): names the parked
326
- // waiter's provider key in `awaiting <key> slot` when that key is at its cap.
327
- snap.permits,
328
315
  // A timed per-role pause's deadline — only when the role itself is individually paused
329
316
  // (in `pausedRoles`) and not under a team-wide pause, matching the GUI's phaseDetail
330
317
  // guard, so the team pause (whose countdown rides the header badge) never double-appends.
@@ -336,15 +323,6 @@ export function roleRowCells(snap, root, s) {
336
323
  * gui-client-model.ts interpolates ROLE_RANK_RULES, so the two dashboards cannot disagree
337
324
  * about which phases are in flight. */
338
325
  export const ACTIVE_PHASE_PREFIXES = ["working", "reviewing", "merging"];
339
- /** Whether `key` is the binding limit in the published permit snapshot: its cap is set and
340
- * its in-use already meets or exceeds it, so the pool is granting nothing more on that key and
341
- * a waiter carrying it is queued on the key, not on the team total. A key without a cap in
342
- * the snapshot (uncapped) is never binding. One home for the rule, shared by status-model's
343
- * `awaiting <key> slot` and the GUI chip's provider detail. */
344
- function isBindingLimit(permits, key) {
345
- const entry = permits.byKey[key];
346
- return entry?.limit !== undefined && entry.inUse >= entry.limit;
347
- }
348
326
  /** Is this phase one of the PERMIT-HOLDER in-flight states — the rows an operator counts
349
327
  * against maxConcurrent? The three labels come from rolePhase: `working …`, `reviewing …`,
350
328
  * and the marker-driven `merge …` (merge queue 4/5) — an approved change waiting for the merge
@@ -375,7 +353,7 @@ export function isActivePhase(phase) {
375
353
  export const ROLE_RANK_RULES = [
376
354
  { rank: -1, prefixes: ["director working"] },
377
355
  { rank: 0, active: true },
378
- { rank: 1, prefixes: ["approved", "waiting to start", "waiting for "] },
356
+ { rank: 1, prefixes: ["approved", "waiting to start"] },
379
357
  { rank: 2, phases: ["failing", "main's check is failing"] },
380
358
  { rank: 3, phases: ["paused"] },
381
359
  { rank: 3, prefixes: ["paused", "waiting:"] },
@@ -1,9 +1,8 @@
1
- import { answeredQuestionSummaries } from "../backlog/question-answer.js";
1
+ import { answeredQuestionSummaries } from "../backlog/backlog-question-answer.js";
2
2
  import { completionDate, donePlanEntries, DONE_DATE_RE, FIXED_DATE_RE, fixedBugEntries, openBugs, openQuestions, plannedPlans } from "../backlog/backlog.js";
3
- import { backlogHolds, backlogInProgress, backlogMarks, refusedEntries, stalledEntries } from "../backlog/backlog-eligibility.js";
4
- import { finishedTitle, strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
3
+ import { backlogHolds, backlogInProgress, backlogMarks, refusedEntries } from "../backlog/backlog-eligibility.js";
4
+ import { finishedTitle } from "../backlog/backlog-structure.js";
5
5
  import { groupBlockedSeries, partsLabel } from "../backlog/backlog-index-data.js";
6
- import { fixClaimProblems, validationGapProblems } from "../doctor/doctor-backlog-plans.js";
7
6
  import { readEvents } from "../events/event-read.js";
8
7
  import { eventFeedItem, formatEvent } from "../events/event-format.js";
9
8
  import { projectName } from "../project-name.js";
@@ -15,11 +14,9 @@ import { scopeSnapshotToRoles, snapshot } from "../status/status-data.js";
15
14
  import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCountsFragment, ciParityBadge, ciParityTone, permitsBadge, checksBadge, quietBadge, runWindowBadge, workspacesBadge } from "./badges.js";
16
15
  import { isActivePhase, roleRowCells, onErrorStreak } from "./status-model.js";
17
16
  import { yieldMultiplierFor } from "../scheduling/backoff.js";
18
- import { staleClaimEntries } from "../scheduling/claim-entries.js";
19
17
  import { teamAlerts } from "./team-alerts.js";
20
- import { duplicatedBacklogHeadings, liveFinishedEntriesFor } from "../doctor/doctor-backlog-plans.js";
21
18
  import { readPostReviews } from "../review/post-merge-review.js";
22
- import { capacityContention, instancePoolProblems, planDependencyAlert, providerCapProblems, regressedFixesAlert, staleOpenQuestions, staleWorkspacePins } from "./alert-feeds.js";
19
+ import { getOrphanAlert } from "./alert-feeds.js";
23
20
  /** How many recent events the payload carries — enough for the dashboard's activity feed to
24
21
  * still show a screenful of notable events (merges, failures, questions) after it filters
25
22
  * out the routine ones (run starts, wakes, passing checks). */
@@ -61,9 +58,6 @@ function roleRows(snap, root) {
61
58
  // the dashboard can mark a row whose model tumwater.json did not name. Same conditional
62
59
  // spread as `model`, present on exactly the rows that carry the flag.
63
60
  ...(s.modelFromPi ? { modelFromPi: true } : {}),
64
- // Model failure fallback (PLANS.md "Model failure fallback, part 2/2"): present only
65
- // while the role's runs use another model, so the common row shape is unchanged.
66
- ...(s.fallback ? { fallback: s.fallback } : {}),
67
61
  ...(s.slot ? { slot: s.slot, slotPinned: s.slotPinned === true } : {}),
68
62
  // Parallel work instances (plans/parallel-work-instances.md "Observability", part
69
63
  // 6/7): the backlog entry this instance claimed, shown by the dashboards while it is
@@ -173,7 +167,7 @@ function headerFields(root, snap, now) {
173
167
  // The header's daily-cost-budget badge preformatted through badges.ts's budgetBadge —
174
168
  // the same string the TUI/status table renders (n/a for an all-free team; `· no cap`
175
169
  // when disabled). Sent display-ready like buildBadge so the page cannot re-derive it.
176
- budgetBadge: budgetBadge(snap.budget, snap.subscriptionWindows, snap.subscriptionOnly, now, snap.chain),
170
+ budgetBadge: budgetBadge(snap.budget, snap.chain),
177
171
  // The disk floor (plans/disk-floor.md, part 4/4): the raw block is machine-readable for
178
172
  // `status --json`, and the header badge is preformatted through badges.ts's diskBadge —
179
173
  // the same string the TUI/status header renders. Omitted (not null) when no orchestrator
@@ -294,7 +288,7 @@ function inboxFields(snap) {
294
288
  };
295
289
  }
296
290
  /** Budget and pause/scheduling state (status-payload.ts's field-group split): the daily
297
- * budget, its subscription windows, the fallback chain, operator/per-role pauses, the
291
+ * budget, the fallback chain, operator/per-role pauses, the
298
292
  * cap/maintenance holds, scheduled wakes, quiet hours, and the quiet badge. */
299
293
  function budgetAndPauseFields(snap) {
300
294
  return {
@@ -302,19 +296,6 @@ function budgetAndPauseFields(snap) {
302
296
  // reported) — machine-readable for `status --json`; the display-ready header badge is
303
297
  // the preformatted `budgetBadge`.
304
298
  budget: snap.budget,
305
- // The subscription usage-window block (part 6d/7), absent on a pi-only or API-claude
306
- // team — the same raw shape `status --json` carries, which the GUI's spend card reads
307
- // to render the window (the preformatted budgetBadge above already folds it into the
308
- // header for the TUI/status). Raw data, like budget.
309
- ...(snap.claudeWindow ? { claudeWindow: snap.claudeWindow } : {}),
310
- // The per-backend window blocks (part 3a/9): one entry per subscription backend in use,
311
- // machine-readable for `status --json` beside claudeWindow. As of part 3b/9 the preformatted
312
- // budgetBadge above folds the per-backend blocks — the most-constrained first — into the
313
- // header, naming each backend's own window.
314
- ...(snap.subscriptionWindows && snap.subscriptionWindows.length > 0 ? { subscriptionWindows: snap.subscriptionWindows } : {}),
315
- // The team-global free-ness answer (part 3a/9); budgetBadge reads it to decide whether the
316
- // window lines stand alone (subscription-only) beside the daily spend.
317
- subscriptionOnly: snap.subscriptionOnly,
318
299
  // Each tier's fallback step (fallback chain part 7c/7), raw for the GUI side card.
319
300
  ...(snap.chain ? { chain: snap.chain } : {}),
320
301
  // Operator pause (`tumwater pause` marker present): every idle role's phase reads
@@ -419,7 +400,7 @@ function alertFields(root, snap, questions, roles, now) {
419
400
  return {
420
401
  // What needs the operator (team-alerts.ts's teamAlerts): the dashboard's alert banners and
421
402
  // the TUI's attention lines, phrased once.
422
- alerts: teamAlerts(snap, questions, roles, now, refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), capacityContention(root, now), regressedFixesAlert(root, now), staleClaimEntries(root, roles.map((r) => r.role), now), staleOpenQuestions(root, now), fixClaimProblems(root), staleWorkspacePins(root, now), liveFinishedEntriesFor(root), instancePoolProblems(root), providerCapProblems(root)),
403
+ alerts: teamAlerts(snap, questions, roles, now, refusedEntries(root), getOrphanAlert(root)),
423
404
  };
424
405
  }
425
406
  /** The one team-state document both observer surfaces carry: `GET /api/status` (gui/gui-server.ts)
@@ -446,12 +427,12 @@ export function statusPayload(root, now = Date.now(), scope = null, modelsPath =
446
427
  // Why each plan/bug is held (src/backlog/backlog-eligibility.ts): the same hold decision
447
428
  // the prompt's backlog index and `tumwater backlog` render, projected to the mark each
448
429
  // surface appends. Read once here, consumed as the parallel planHolds/bugHolds arrays below.
449
- const marks = backlogMarks(root);
430
+ // Which blocked plans fold into a series row (computed before marks so credit, the same
431
+ // claims walk feeding both, is done once per poll — see backlogMarks' optional working arg).
432
+ const working = backlogInProgress(root);
433
+ const marks = backlogMarks(root, working);
450
434
  const holds = backlogHolds(root);
451
435
  const plans = plannedPlans(root);
452
- // Which blocked plans fold into a series row: the grouping `plan --list` and the prompt's
453
- // backlog index use, as a slot per plan (null for a plan that stays an ordinary row).
454
- const working = backlogInProgress(root);
455
436
  const planSeries = planSeriesSlots(plans, holds, working);
456
437
  const roles = roleRows(snap, root);
457
438
  return {
@@ -1,22 +1,19 @@
1
1
  import { dailyCost } from "../budget/budget.js";
2
2
  import { clipToWidth, displayWidth } from "../text/text-width.js";
3
3
  import { compactTokens, contextShareLabel, usd } from "../text/format.js";
4
- import { formatTime, humanAgo, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
4
+ import { DAY_MS, formatTime, humanAgo, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
5
5
  import { projectName } from "../project-name.js";
6
- import { bootstrapBadge, buildBadge, budgetBadge, subscriptionWindowTone, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, ciParityBadge, ciParityTone, pauseBadge, roleWakeMarker, permitsBadge, checksBadge, quietBadge, runWindowBadge, wakeBadge, workspacesBadge } from "./badges.js";
6
+ import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, ciParityBadge, ciParityTone, pauseBadge, roleWakeMarker, permitsBadge, checksBadge, quietBadge, runWindowBadge, wakeBadge, workspacesBadge } from "./badges.js";
7
7
  import { idleClaimText, isActivePhase, roleRowCells, onErrorStreak, sortRolesByState } from "./status-model.js";
8
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
- import { staleClaimEntries } from "../scheduling/claim-entries.js";
13
12
  import { phaseTone, resultTone } from "./tone.js";
14
13
  import { teamAlerts } from "./team-alerts.js";
15
14
  import { openQuestions } from "../backlog/backlog.js";
16
- import { refusedEntries, stalledEntries } from "../backlog/backlog-eligibility.js";
17
- import { capacityContention, instancePoolProblems, planDependencyAlert, providerCapProblems, regressedFixesAlert, staleOpenQuestions, staleWorkspacePins } from "./alert-feeds.js";
18
- import { strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
19
- import { duplicatedBacklogHeadings, fixClaimProblems, liveFinishedEntriesFor, validationGapProblems } from "../doctor/doctor-backlog-plans.js";
15
+ import { refusedEntries } from "../backlog/backlog-eligibility.js";
16
+ import { getOrphanAlert } from "./alert-feeds.js";
20
17
  /** The status RENDER layer: time/token cell formatters and the width-aware table shared by
21
18
  * `tumwater status` and the TUI. The labels, badges, and metrics it draws come from the shared
22
19
  * display model (status-model.ts); this module decides column widths and layout. Depends on
@@ -37,7 +34,7 @@ export function lastRunCell(ts) {
37
34
  return "-";
38
35
  const d = new Date(ts);
39
36
  let s = formatTime(d);
40
- if (Date.now() - ts > 86_400_000)
37
+ if (Date.now() - ts > DAY_MS)
41
38
  s = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${s}`;
42
39
  return `${s} · ${ago(ts)}`;
43
40
  }
@@ -174,13 +171,8 @@ function badgeSpans(badge, tone) {
174
171
  const sep = /^(?: · |, )/.exec(badge)?.[0] ?? "";
175
172
  return [{ text: sep }, { text: badge.slice(sep.length), ...(tone ? { tone } : {}) }];
176
173
  }
177
- /** The daily budget badge's tone: yellow from 85% of the cap, red once it is spent; the
178
- * subscription window's own severity (amber near its limit, red when a rejected window holds
179
- * the seams) wins when it stands beside the daily spend. */
180
- function budgetTone(budget, windows) {
181
- const window = subscriptionWindowTone(windows);
182
- if (window !== undefined)
183
- return window;
174
+ /** The daily budget badge's tone: yellow from 85% of the cap, red once it is spent. */
175
+ function budgetTone(budget) {
184
176
  if (budget.free || !(budget.capUsd > 0))
185
177
  return undefined;
186
178
  const share = budget.spentUsd / budget.capUsd;
@@ -222,7 +214,7 @@ function statusHeaderLine(root, snap, now) {
222
214
  // empty unless the team marker's deadline stands in the future, so a role-only or
223
215
  // indefinite pause keeps today's header byte-identical.
224
216
  const running = snap.running
225
- ? [{ text: "running", tone: "green" }, { text: ` (pid ${snap.pid}` }, ...badgeSpans(buildBadge(snap.build), snap.build?.stale ? "yellow" : undefined), { text: ")" }]
217
+ ? [{ text: "running", tone: "green" }, { text: ` (pid ${snap.pid}` }, ...badgeSpans(buildBadge(snap.build), snap.build?.stale || snap.build?.waitingForCheck ? "yellow" : undefined), { text: ")" }]
226
218
  : [{ text: "not running — start with `tumwater start`", tone: "dim" }];
227
219
  return [
228
220
  { text: "tumwater", tone: "brand" },
@@ -232,14 +224,14 @@ function statusHeaderLine(root, snap, now) {
232
224
  ...badgeSpans(mergeBadge(snap.mergeQueue), "blue"),
233
225
  ...(snap.inbox ? [{ text: ` · inbox: ${snap.inbox}` }] : []),
234
226
  ...(snap.questions ? badgeSpans(` · questions: ${snap.questions}`, "magenta") : []),
235
- ...badgeSpans(budgetBadge(snap.budget, snap.subscriptionWindows, snap.subscriptionOnly, now, snap.chain), budgetTone(snap.budget, snap.subscriptionWindows)),
227
+ ...badgeSpans(budgetBadge(snap.budget, snap.chain), budgetTone(snap.budget)),
236
228
  // The disk floor (plans/disk-floor.md, part 4/4): `disk 8.2 GB free — holding new work`
237
229
  // while the hold is on, or the bare reading below the reclaim threshold. Empty in every
238
230
  // comfortable or unmeasured state, so an unaffected header stays byte-identical.
239
231
  ...badgeSpans(diskBadge(snap.disk), snap.disk?.held ? "yellow" : "blue"),
240
232
  // The permit pool's live state (plans/provider-concurrency.md §8): the team line reads
241
- // `slots 5/8 · omlx 3/3` only while the per-provider cap map is set (permitsBadge already
242
- // omits the providers on a map-less team, so the header stays byte-identical).
233
+ // `running 5 of 8` — the team total's in-use/cap — for any team with a published permit
234
+ // snapshot; empty until one has been published.
243
235
  ...badgeSpans(permitsBadge(snap.permits), "blue"),
244
236
  // Checks running/waiting for a check permit; empty while the check pool is idle.
245
237
  ...badgeSpans(checksBadge(snap.checkPermits), snap.checkPermits && snap.checkPermits.waiting > 0 ? "yellow" : "blue"),
@@ -306,14 +298,11 @@ function statusRoleRows(root, snap, now, withMetrics, sharedModel) {
306
298
  // today's.
307
299
  const inner = roleModelLabel(s);
308
300
  const modelSuffix = inner && !sharedModel ? ` (${inner})` : "";
309
- // Model failure fallback, part 2/2: while the role runs off-model its name cell says so,
310
- // so the operator can see which roles the fallback pair is carrying without reading logs.
311
- const fallbackSuffix = s.fallback ? " (over-cap model)" : "";
312
301
  // User-defined roles (snapshot's `custom` flag) get an asterisk beside their name — the
313
302
  // dashboards' at-a-glance marker. The name column (index 0) is not in FLEXIBLE_COLUMNS;
314
303
  // its width derives from row content and it shrinks only after those,
315
304
  // so the extra character widens it.
316
- const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix + fallbackSuffix;
305
+ const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix;
317
306
  const state = stateCell(root, s, phase, live);
318
307
  const tone = phaseTone(phase);
319
308
  // The whole state cell carries the phase's tone: a work item leads the cell (so it
@@ -509,7 +498,7 @@ export function renderStatus(root, snap, maxWidth, scoped = false) {
509
498
  // Composed here, NOT inside renderStatusSpans: the TUI re-slices that function's shared
510
499
  // `lines` (lines[0], its own attention block, then lines.slice(1)), so a banner embedded
511
500
  // there would render twice in the TUI.
512
- const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), capacityContention(root), regressedFixesAlert(root, Date.now()), staleClaimEntries(root, roles.map((r) => r.role), Date.now()), staleOpenQuestions(root), fixClaimProblems(root), staleWorkspacePins(root, Date.now()), liveFinishedEntriesFor(root), instancePoolProblems(root), providerCapProblems(root)).filter((a) => a.key !== "stopped");
501
+ const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root), getOrphanAlert(root)).filter((a) => a.key !== "stopped");
513
502
  const banner = alertLines(alerts, maxWidth ?? Number.MAX_SAFE_INTEGER, alerts.length).map(plainText);
514
503
  const text = lines.map(plainText);
515
504
  return [...text.slice(0, 2), ...banner, ...text.slice(2)].join("\n");
@@ -1,14 +1,11 @@
1
- /** The backlog team alerts, in emission order: open questions, refused entries, stalled
2
- * entries, a broken plan-dependency clause, stranded plans, duplicated backlog
3
- * headings, finished history still in the live backlog files, and Fixed records with a bad
4
- * Validation gap trace. One of the per-domain builders split out of src/ui/team-alerts.ts; the
5
- * shared types and wording helpers live in team-alert-shared.ts. */
6
- import { entryListDetail, firstAndMore, openBacklogAction } from "./team-alert-shared.js";
1
+ /** The backlog team alerts, in emission order: open questions, refused entries. The two
2
+ * per-domain builders split out of src/ui/team-alerts.ts; the shared types and wording helpers
3
+ * live in team-alert-shared.ts. The stale questions, stalled entries, plan dependencies,
4
+ * stranded plans, duplicated headings, live finished history, validation gaps and false fixes
5
+ * banners moved to `tumwater doctor` rows (Subtract part 1e/6), each printing its row. */
6
+ import { entryListDetail, openBacklogAction } from "./team-alert-shared.js";
7
7
  import { entryKindLabel } from "../backlog/backlog-eligibility.js";
8
- import { missingSymbolNames } from "../verdict/fix-claim.js";
9
- import { joinWithMore } from "../text/text.js";
10
8
  import { plural } from "../text/phrases.js";
11
- import { liveFinishedRowList } from "../backlog/backlog-archive.js";
12
9
  function pushQuestionsAlert(out, questions) {
13
10
  if (questions.length) {
14
11
  out.push({
@@ -20,29 +17,6 @@ function pushQuestionsAlert(out, questions) {
20
17
  });
21
18
  }
22
19
  }
23
- // A stale question (doctor's STALE_QUESTION_DAYS rule, src/doctor/doctor-backlog-bugs.ts
24
- // checkStaleQuestions) has waited a week without an answer — a role is stuck behind a decision,
25
- // the exact class every other backlog row raises as an amber alert — so it asks (amber), naming
26
- // the oldest question, its age, and the `tumwater questions
27
- // answer` fix. Its callers pass it oldest-first (alert-feeds.ts staleOpenQuestions sorts
28
- // most-days-waited first), so the first element is the most-stale question and is named with its
29
- // position and days; the rest are folded through entryListDetail like the questions and refused
30
- // alerts, so the wording and counts cannot drift. The `questions` action opens the Questions tab,
31
- // so the alert carries no row address.
32
- function pushStaleQuestionsAlert(out, staleQuestions) {
33
- if (staleQuestions.length) {
34
- const { first, more } = firstAndMore(staleQuestions, (q) => q.title);
35
- out.push({
36
- key: "stale-questions", // vocabulary-ok: the stale-questions amber alert's key
37
- tone: "amber",
38
- title: `${plural(staleQuestions.length, "open question waited", "open questions waited")} 7+ days for an answer`,
39
- detail: `#${first.position} "${first.title}" has waited ${first.days}d for an answer` +
40
- more +
41
- ` · answer it with \`tumwater questions answer ${first.position} "<decision>"\`, or close it in QUESTIONS.md`,
42
- actions: [{ label: "Answer", act: "questions" }],
43
- });
44
- }
45
- }
46
20
  // Refused entries hold until a human decides; amber because the operator has to act.
47
21
  function pushRefusedAlert(out, refused) {
48
22
  if (refused.length) {
@@ -56,181 +30,17 @@ function pushRefusedAlert(out, refused) {
56
30
  });
57
31
  }
58
32
  }
59
- // An entry parked by a Needs-review note (the doctor's `stalled entries` row)
60
- // sits until the plan role splits or replans it — amber because the plan role is the only
61
- // one that can clear it, naming the first such entry in file order, its file, its note kind,
62
- // and the fix. Same `entryHold` classification as `stalledEntries` and
63
- // `checkStalledEntries`, so the always-on alert and the doctor cannot disagree on which
64
- // holds are role-resolvable.
65
- function pushStalledEntriesAlert(out, stalledEntries) {
66
- if (stalledEntries.length) {
67
- const first = stalledEntries[0];
68
- out.push({
69
- key: "stalled-entries",
70
- tone: "amber",
71
- title: `${plural(stalledEntries.length, "backlog entry is stalled", "backlog entries are stalled")} in review`,
72
- detail: `${entryKindLabel(first.file)} ${entryListDetail(stalledEntries.map((p) => p.title))} — a Needs review note parks it; the plan role splits or replans the entry, so one that keeps sitting here means the plan role is not running or keeps deferring`,
73
- actions: openBacklogAction(first),
74
- });
75
- }
76
- }
77
- // A broken plan-dependency clause (the doctor's `plan dependencies` rule,
78
- // src/doctor/doctor-backlog-plans.ts planDependencyProblem, via alert-feeds.ts's
79
- // planDependencyAlert) is invisible in the backlog listings — the claim allocator keeps
80
- // acting on the misread clause — so it asks (amber), named by type
81
- // and carrying the doctor's exact message. One alert at most (the problem set is the single
82
- // most serious rule), and the fix is editing PLANS.md's heading, so there are no actions.
83
- function pushPlanDependencyAlert(out, planDependency) {
84
- if (planDependency !== null) {
85
- const titles = {
86
- cycle: "Plan dependency cycle blocks a whole series",
87
- dangling: "A plan requires a part no heading carries",
88
- unparsed: "A plan's requires clause names no readable part",
89
- misworded: "A plan's dependency wording holds nothing",
90
- };
91
- out.push({
92
- key: "plan-dependencies",
93
- tone: "amber",
94
- title: titles[planDependency.kind],
95
- detail: planDependency.message,
96
- // The fix is editing PLANS.md, and the message names the heading and ref.
97
- actions: [],
98
- });
99
- }
100
- }
101
- // A plan heading filed under the wrong ## section (the doctor's `plans stuck in Planned`
102
- // rule, checkStrandedPlans, src/doctor/doctor-backlog-plans.ts, via the shared
103
- // strandedPlanEntriesFor) sits invisible to every reader that scans the right section: a
104
- // `(planned …)` heading under ## Done never enters a Planned reader, and a done-dated heading
105
- // still under ## Planned invites a second implementation. The clean role repairs it only when
106
- // it happens to run and the doctor warns only on request, so it asks (amber) the way the
107
- // plan-dependency alert does, naming the first stranded heading, its file, its section, and
108
- // the section it belongs under; the rest are folded through joinWithMore.
109
- function pushStrandedPlansAlert(out, strandedPlans) {
110
- if (strandedPlans.length) {
111
- const { first, more } = firstAndMore(strandedPlans, (e) => `"${e.title}" in ${e.file ?? "PLANS.md"} ## ${e.section}`, (labels) => joinWithMore(labels, 3));
112
- out.push({
113
- key: "stranded-plans",
114
- tone: "amber",
115
- title: `${strandedPlans.length === 1 ? "A plan sits" : "Plans sit"} under the wrong backlog section`,
116
- detail: `${first.file ?? "PLANS.md"} has a plan stranded under ## ${first.section}: "${first.title}"${more} — move it under ## ${first.section === "Done" ? "Planned" : "Done"}${first.file === undefined || first.file === "PLANS.md" ? "" : " in PLANS.md"} (the clean role repairs these)`,
117
- // The fix is editing a backlog file, and a PLANS-DONE.md entry has no PLANS.md drawer index.
118
- actions: [],
119
- });
120
- }
121
- }
122
- // A duplicated `## ` section heading already on main (the doctor's `backlog headings` row,
123
- // checkBacklogHeadings, src/doctor/doctor-backlog-plans.ts, via the shared
124
- // duplicatedBacklogHeadings) sits until a deliberate removal edit — the merge gate blocks
125
- // only a merge that ADDS another copy, so a duplicate already on main persists and hides
126
- // every later entry under its name from the `## ` section readers. It asks (amber) the way
127
- // the stranded-plan alert does, naming the first file-heading pair and folding the rest
128
- // through joinWithMore; the fix is editing a backlog file, so there are no actions.
129
- function pushBacklogHeadingsAlert(out, duplicatedHeadings) {
130
- if (duplicatedHeadings.length) {
131
- const { first, more } = firstAndMore(duplicatedHeadings, (d) => `"## ${d.heading}" in ${d.file}`, (labels) => joinWithMore(labels, 3));
132
- out.push({
133
- key: "backlog-headings",
134
- tone: "amber",
135
- title: plural(duplicatedHeadings.length, "duplicated backlog heading", "duplicated backlog headings"),
136
- detail: `${first.file} "## ${first.heading}" appears more than once${more} — readers take the first section of a name, so later entries are invisible; remove the duplicate in a deliberate edit`,
137
- actions: [],
138
- });
139
- }
140
- }
141
- // Finished history still in the live backlog files (the doctor's `backlog archive` rule,
142
- // checkLiveFinished, src/doctor/doctor-backlog-plans.ts, via the shared liveFinishedEntriesFor)
143
- // is the one backlog-hygiene rule the doctor warned but no always-on alert surfaced — every
144
- // other rule (stranded plans, duplicated headings, validation gaps, plan dependencies, stale
145
- // bugs and questions) has one, so an operator watching `tumwater status` or the dashboard never
146
- // sees the growing finished sections until they run `tumwater doctor`. It asks (amber) the way
147
- // the duplicated-heading alert does, naming each live file, its finished section, its entry
148
- // count, and its size in KB; the fix is the `tumwater archive` command, so there are no actions.
149
- function pushBacklogArchiveAlert(out, liveFinished) {
150
- if (liveFinished.length) {
151
- out.push({
152
- key: "backlog-archive",
153
- tone: "amber",
154
- title: `${plural(liveFinished.length, "live backlog file still holds", "live backlog files still hold")} finished history`,
155
- detail: liveFinishedRowList(liveFinished) +
156
- " — every run that reads the file pays for them; `tumwater archive` moves them to the archive files",
157
- // The fix is a command, not a drawer row.
158
- actions: [],
159
- });
160
- }
161
- }
162
- // A Fixed record with a bad Validation gap trace (the doctor's `validation gaps` check,
163
- // src/doctor/doctor-backlog-plans.ts checkValidationGaps) is the always-on twin of that
164
- // doctor row: a newest Fixed record that omits its **Validation gap:** line, names a tag
165
- // outside the closed vocabulary, or declares `unclear-invariant` without pinning a
166
- // docs/invariants.md section is invisible or misleading to every closed-vocabulary consumer,
167
- // so it asks (amber) the way the stranded-plan alert does, naming the first offending record
168
- // and the fix. It is fed from the same scan (validationGapProblems over validationGapRecords)
169
- // the doctor's check uses, so the alert and the doctor cannot drift on the problem set, and
170
- // that scan runs no sourceHaystack walk, so it adds only the same two backlog-file reads the
171
- // doctor's check already makes to the poll path. The fix is editing BUGS.md, so it has no
172
- // buttons.
173
- function pushValidationGapsAlert(out, validationGaps) {
174
- if (validationGaps.length) {
175
- const { first, more } = firstAndMore(validationGaps, (v) => `"${v.title}"`, (labels) => joinWithMore(labels, 3));
176
- const reason = first.kind === "missing"
177
- ? "has no **Validation gap:** line"
178
- : first.kind === "unrecognized"
179
- ? `names unrecognized Validation gap tag "${first.tag}"`
180
- : "is an unclear-invariant fix but names no docs/invariants.md section";
181
- out.push({
182
- key: "validation-gaps",
183
- tone: "amber",
184
- title: plural(validationGaps.length, "Fixed record has a validation-gap problem", "Fixed records have validation-gap problems"),
185
- detail: `BUGS.md "${first.title}" ${reason}${more} — every Fixed entry writes exactly one known **Validation gap:** tag (write \`none\` rather than omitting it), and an unclear-invariant fix pins its reconstructed guarantee in docs/invariants.md`,
186
- // The fix is editing BUGS.md.
187
- actions: [],
188
- });
189
- }
190
- }
191
- // A newest Fixed record whose Fix paragraph names only code absent from the tree (the
192
- // doctor's `claimed bugs` check, src/doctor/doctor-backlog-plans.ts checkFixClaims) is the
193
- // always-on twin of that doctor row: a fix claim that names no live symbol is invisible to
194
- // every consumer that reads BUGS.md (BUGS-FIXED.md's long fight over an already-fixed entry),
195
- // so it asks (amber) the way the validation-gaps alert does, naming the first record and the
196
- // missing symbols. It is fed from the same scan (fixClaimProblems over fixClaimRecords) the
197
- // doctor's check uses, so the alert and the doctor cannot drift on the problem set, and that
198
- // scan reads the tree only when a file changed (cachedSourceHaystack), so it adds no re-read
199
- // to the 1 Hz poll path. The fix is editing BUGS.md, so it has no buttons.
200
- function pushFalseFixesAlert(out, fixClaims) {
201
- if (fixClaims.length) {
202
- const { first, more } = firstAndMore(fixClaims, (p) => `"${p.heading}"`, (labels) => joinWithMore(labels, 3));
203
- out.push({
204
- key: "false-fixes",
205
- tone: "amber",
206
- title: plural(fixClaims.length, "Fixed record names code that is not on this tree", "Fixed records name code that is not on this tree"),
207
- detail: `BUGS.md records "${first.heading}" as Fixed, but none of the symbols its Fix paragraph names exist on this tree: ${missingSymbolNames(first.missing)}${more} — land the fix or keep the bug Open / refresh a stale record`,
208
- // The fix is editing BUGS.md.
209
- actions: [],
210
- });
211
- }
212
- }
213
- /** The backlog alerts, in emission order: open questions, refused entries, stalled
214
- * entries, a broken plan-dependency clause, stranded plans, duplicated backlog
215
- * headings, finished history still in the live backlog files, and Fixed records with a
216
- * bad Validation gap trace. Extracted from
217
- * teamAlerts (part 2/5 of the teamAlerts split) so the shared module stays small; the input
218
- * names exactly the fields this run reads. The caller pushes the returned alerts at the
219
- * position this run used to occupy, so the emitted key order is unchanged. Each alert arm is a
220
- * module-private builder named for its alert, in this same emission order, so a backlog alert
221
- * is found by name. */
33
+ /** The backlog alerts, in emission order: open questions, refused entries. The conditions that
34
+ * used to raise the removed backlog banners each print their `tumwater doctor` row instead
35
+ * (Subtract part 1e/6). Extracted from teamAlerts (part 2/5 of the teamAlerts split) so the
36
+ * shared module stays small; the input names exactly the fields this run reads. The caller
37
+ * pushes the returned alerts at the position this run used to occupy, so the emitted key order
38
+ * is unchanged. Each alert arm is a module-private builder named for its alert, in this same
39
+ * emission order, so a backlog alert is found by name. */
222
40
  export function buildBacklogAlerts(input) {
223
41
  const out = [];
224
- const { questions, staleQuestions, refused, stalledEntries, planDependency, strandedPlans, duplicatedHeadings, liveFinished, validationGaps, fixClaims, } = input;
42
+ const { questions, refused } = input;
225
43
  pushQuestionsAlert(out, questions);
226
- pushStaleQuestionsAlert(out, staleQuestions);
227
44
  pushRefusedAlert(out, refused);
228
- pushStalledEntriesAlert(out, stalledEntries);
229
- pushPlanDependencyAlert(out, planDependency);
230
- pushStrandedPlansAlert(out, strandedPlans);
231
- pushBacklogHeadingsAlert(out, duplicatedHeadings);
232
- pushBacklogArchiveAlert(out, liveFinished);
233
- pushValidationGapsAlert(out, validationGaps);
234
- pushFalseFixesAlert(out, fixClaims);
235
45
  return out;
236
46
  }
@@ -15,30 +15,6 @@ export function entryListDetail(titles) {
15
15
  const first = titles[0] === undefined ? "" : stripEntryStamp(titles[0]);
16
16
  return titles.length > 1 ? `${first} — and ${titles.length - 1} more` : first;
17
17
  }
18
- /** Append the folded names of the entries past an alert's first one, after a ` · ` separator,
19
- * when there are any, and the empty string otherwise — the shared idiom every amber
20
- * "name the oldest, fold the rest" alert (stale-questions, stranded-plans,
21
- * backlog-headings, validation-gaps, false-fixes) uses to compose its
22
- * `detail`, so the separator, the empty case, and the default fold (entryListDetail;
23
- * joinWithMore for the alerts that fold their rest labels with a count) cannot
24
- * drift. Callers pre-map each
25
- * rest element to the label the first clause already named. */
26
- function moreDetail(restLabels, fold = entryListDetail) {
27
- return restLabels.length > 0 ? ` · ${fold(restLabels)}` : "";
28
- }
29
- /** Split a bounded entry list into its first (oldest, named) item and the " · rest" suffix
30
- * moreDetail builds for the entries past it — the shared shape every amber "name the oldest,
31
- * fold the rest" alert (stale-questions, stranded-plans,
32
- * backlog-headings, validation-gaps, false-fixes) composes, so
33
- * the split and the separator/empty case have one home beside moreDetail. `label` maps each
34
- * rest element to the string its clause already named; `fold` defaults to entryListDetail
35
- * (moreDetail's own default), with joinWithMore(labels, 3) for the alerts that fold their rest
36
- * labels with a count. */
37
- export function firstAndMore(items, label, fold = entryListDetail) {
38
- const first = items[0];
39
- const rest = items.slice(1);
40
- return { first, more: moreDetail(rest.map(label), fold) };
41
- }
42
18
  /** The dashboard's "Open <role>" buttons for an alert about a set of roles: one button per
43
19
  * role (capped to the first three so the banner does not overflow), each opening that role's
44
20
  * pane. Shared by the failing, stuck, and no-image-target alerts, so the button shape and its
@@ -54,8 +30,8 @@ export function seeFailures() {
54
30
  }
55
31
  /** The dashboard's "Open backlog" button for an alert naming one backlog entry: opens the
56
32
  * entry's backlog-card tab and that row (the dashboard's backlog arm reads `file:index`).
57
- * Shared by the refused and stalled-entries alerts,
58
- * so the button shape and the `file:index` contract cannot drift between them. */
33
+ * Used by the refused alert,
34
+ * so the button shape and the `file:index` contract cannot drift. */
59
35
  export function openBacklogAction(entry) {
60
36
  return [{ label: "Open backlog", act: "backlog", arg: `${entry.file}:${entry.index}` }];
61
37
  }
@@ -77,11 +53,12 @@ export function listRoleIds(ids) {
77
53
  /** The stuck alert's per-role detail: the stall phrase the phase rendered, plus — when that
78
54
  * phrase does not already carry one — the honest silence span from the live tail (stuckMs,
79
55
  * ms since the role's last pi log write, capped to the run by progressOfRun). A
80
- * "no agent output for <d>" phrase already carries its duration, so only a tool-call stall gets
81
- * a " for <duration>" suffix, and only when the caller holds a live tail (stuckMs present). */
56
+ * "no agent output for <d>" or "no model reply for <d>" phrase already carries its duration, so
57
+ * only a tool-call stall gets a " for <duration>" suffix, and only when the caller holds a live
58
+ * tail (stuckMs present). */
82
59
  export function stuckDetail(l) {
83
60
  const cause = (STALL_RE.exec(l.phase)?.[0] ?? "").trim();
84
- if (cause && !cause.startsWith("no agent output for") && l.stuckMs !== undefined) {
61
+ if (cause.startsWith("tool call stalled") && l.stuckMs !== undefined) {
85
62
  return `${roleLabel(l.role)}: ${cause} for ${duration(l.stuckMs)}`;
86
63
  }
87
64
  return `${roleLabel(l.role)}: ${cause}`;