tumwater 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,25 +1,27 @@
1
- /** The dashboard's fleet view, browser-side — the main screen between polls: the sidebar's
2
- * project name and fleet status (loops running, build state, main's check, quiet hours),
1
+ // vocabulary: plain
2
+ /** The dashboard's team view, browser-side — the main screen between polls: the sidebar's
3
+ * project name and team status (loops running, build state, main's check, quiet hours),
3
4
  * the alerts band for whatever needs a human (a failing loop, a red main, a spent budget,
4
5
  * an old build, open questions, a pause), today's progress tiles with their
5
6
  * per-day bar chart, the backlog with its questions/plans/bugs tabs, and the notable activity
6
7
  * feed (the loops table with its per-row actions lives in gui-client-loops.ts; the composer
7
- * rendered onto this view lives in gui-client-composer.ts). renderFleet repaints all of it
8
+ * rendered onto this view lives in gui-client-composer.ts). renderTeam repaints all of it
8
9
  * from each /api/status poll. Spliced into gui-client.ts's script, reaching its helpers (esc,
9
10
  * icon, getJson, postJson, postAction, plural, agree, recall, fmtAgo, fmtTokens, fmtUsd,
10
11
  * paintPanel, showFlash) through that concatenation. */
11
- export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet status, budget and pause controls ----
12
+ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team status, budget and pause controls ----
12
13
  // restart-button:start
13
- // The restart affordance's exact markup — one home shared by the sidebar's stale-build row
14
- // and alertParts()'s build alert, so styling and the global [data-act='restart'] handler
15
- // serve both icons from one place.
14
+ // The alert affordances' exact markup — one home shared by the sidebar's stale-build row and
15
+ // alertParts()'s build alert (a restart) and the held/reclaim disk row (a reclaim), so styling
16
+ // and the global [data-act='restart'] / [data-act='reclaim'] handlers serve both from one place.
17
+ function actionButton(glyph, act, title) {
18
+ return "<button type='button' class='alert-icon' data-act='" + act + "' title='" + title + "'>" + glyph + "</button>";
19
+ }
16
20
  function restartButton(glyph) {
17
- return "<button type='button' class='alert-icon' data-act='restart' title='Restart onto the new build now'>" + glyph + "</button>";
21
+ return actionButton(glyph, "restart", "Restart onto the update now");
18
22
  }
19
- // The disk row's reclaim affordance: its icon IS the button when the row is held or in the
20
- // reclaim band; the global [data-act='reclaim'] handler posts /api/reclaim.
21
23
  function reclaimButton(glyph) {
22
- return "<button type='button' class='alert-icon' data-act='reclaim' title='Reclaim idle build outputs now'>" + glyph + "</button>";
24
+ return actionButton(glyph, "reclaim", "Reclaim idle build outputs now");
23
25
  }
24
26
  // restart-button:end
25
27
  // sidebar:start
@@ -40,7 +42,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
40
42
  else {
41
43
  rows = d.running
42
44
  ? row("green", "<span class='dot live'></span>", "Running", "Orchestrator running as pid " + d.pid)
43
- : row("gray", "<span class='dot'></span>", "Stopped", "Start it with tumwater run");
45
+ : row("gray", "<span class='dot'></span>", "Stopped", "Start it with tumwater start");
44
46
  if (d.build) {
45
47
  const stale = d.build.stale;
46
48
  // A stale build's refresh icon IS the restart button — restartButton(), the same
@@ -48,8 +50,8 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
48
50
  // decoration.
49
51
  const lead = stale ? restartButton(icon("refresh")) : icon("refresh");
50
52
  rows += row(stale ? (d.build.restartBlocked ? "amber" : "blue") : "", lead,
51
- "Build <span class='mono'>" + esc(shortSha(d.build.sha)) + "</span>" + (stale ? " · " + esc(d.build.aheadCommits || 0) + " behind" : ""),
52
- localizeInstants(("Running build" + (d.buildBadge || "").replace(/^, build/, "")).trim()));
53
+ "Version <span class='mono'>" + esc(shortSha(d.build.sha)) + "</span>" + (stale ? " · " + esc(d.build.aheadCommits || 0) + " behind" : ""),
54
+ localizeInstants(("Running version" + (d.buildBadge || "").replace(/^, build/, "")).trim()));
53
55
  }
54
56
  if (d.mainCheck) {
55
57
  // Main's newest merge-scope check (status-model's mainCheckBadge): green, red, or — a
@@ -70,28 +72,44 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
70
72
  // The disk floor (plans/disk-floor.md, part 4/4): the sidebar row renders the payload's
71
73
  // preformatted diskBadge (upcased for the row's label style) — the same facts the
72
74
  // TUI/status header carries, so the two surfaces cannot drift — and names the free space,
73
- // the hold and the last reclaim there.
75
+ // and the hold there.
74
76
  if (d.diskBadge) {
75
77
  const held = Boolean(d.disk && d.disk.held);
76
78
  const low = Boolean(d.disk && !held && d.disk.reclaimGB > 0 && d.disk.freeGB < d.disk.reclaimGB);
77
79
  rows += row(held ? "amber" : "blue", held || low ? reclaimButton(icon("info")) : icon("info"),
78
80
  esc(badgeLabel(d.diskBadge)),
79
81
  held
80
- ? "Free space is below the " + esc(String(d.disk.holdGB)) + " GB floor, so the fleet starts no new work until it recovers. Build outputs are reclaimed first."
82
+ ? "Free space is below the " + esc(String(d.disk.holdGB)) + " GB floor, so the team starts no new work until it recovers. Build outputs are reclaimed first."
81
83
  : low
82
84
  ? "Free space is below the " + esc(String(d.disk.reclaimGB)) + " GB reclaim threshold; idle build outputs are being cleaned."
83
- : "The last build-output reclaim freed space on the worktrees volume.");
85
+ : "");
86
+ }
87
+ // The permit pool (plans/provider-concurrency.md §8): the payload's preformatted
88
+ // permitsBadge — running 5 of 8 · omlx 3/3 · claude 2/6 — upcased for the row's label
89
+ // style, the same reading the TUI/status header carries, so the two surfaces cannot
90
+ // drift. Empty on a map-less team, so the sidebar stays unchanged.
91
+ if (d.permitsBadge) {
92
+ const tok = String(d.permitsBadge).replace(/^ · /, "");
93
+ rows += row("blue", icon("gauge"), esc(tok),
94
+ "Roles running at once out of the team limit, then each capped provider's own running/cap; a role waits to start when its provider is at its cap.");
95
+ }
96
+ // The run --for deadline: the payload's preformatted runWindowBadge, like the disk row.
97
+ if (d.runWindowBadge) {
98
+ rows += row("blue", icon("info"), esc(badgeLabel(d.runWindowBadge)),
99
+ d.scheduledStopAt !== undefined && d.scheduledStopAt === d.runStopsAt
100
+ ? "A scheduled stop (tumwater stop --in) — the team finishes current work, then stops."
101
+ : "tumwater was started with start --for; at the deadline it finishes current work, prints a per-role summary, and stops.");
84
102
  }
85
103
  // The configured quiet-hours window ("Quiet hours … part 2/2"): the schedule as
86
104
  // standing information, amber while the local clock is inside it — the same
87
105
  // inside/outside wording the TUI/status header's quietBadge carries, derived from the
88
106
  // payload's raw quietHours/inQuietHours fields (the active case also raises the
89
- // fleet-alerts quiet alert above).
107
+ // team-alerts quiet alert above).
90
108
  if (d.quietHours) {
91
109
  const end = String(d.quietHours).split("-")[1] || "";
92
110
  rows += row(d.inQuietHours ? "amber" : "", icon("pause"),
93
111
  d.inQuietHours ? "Quiet until " + esc(end) : "Quiet " + esc(d.quietHours),
94
- "Quiet hours — role loops start no new ticks during this local-time window; the director keeps running your prompts");
112
+ "Quiet hours — roles start no new runs during this local-time window; the director keeps running your messages");
95
113
  }
96
114
  // New-project bootstrap (plans/work-ratio.md, part 2/2): the payload's preformatted
97
115
  // bootstrapBadge, upcased for the row's label style — the same reading the TUI/status
@@ -99,12 +117,13 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
99
117
  if (d.bootstrapBadge) {
100
118
  rows += row("blue", icon("info"),
101
119
  esc(badgeLabel(d.bootstrapBadge)),
102
- "New-project bootstrap — the harness holds maintenance loops until enough plans are done; plan, feature and director keep ticking");
120
+ "New-project bootstrap — maintenance roles wait until enough plans are done; plan, feature and director keep running");
103
121
  }
104
122
  }
105
123
  paintPanel("statuschips", rows);
106
124
  renderBudgetBadge(d);
107
125
  renderPauseBadge(d);
126
+ renderStopControl(d);
108
127
  renderSoundBadge();
109
128
  }
110
129
  // sidebar:end
@@ -119,7 +138,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
119
138
  // i * 2000 spacing exists for the test path, which passes a stepped clock so each fresh
120
139
  // alert clears the window and sounds its own cue. The keys seen now are kept for the next
121
140
  // poll's diff; lastNeedsYouKeys starts null, so a page opened onto an already-alerting
122
- // fleet cues once.
141
+ // team cues once.
123
142
  let lastNeedsYouKeys = null;
124
143
  function cueNewNeedsYou(alerts, now) {
125
144
  const fresh = newNeedsYouKeys(lastNeedsYouKeys, alerts);
@@ -136,7 +155,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
136
155
  // from under a click in progress.
137
156
  function alertParts(a, restartable) {
138
157
  // A stale-build alert's refresh icon IS the restart button (PLANS.md 2026-09-30): pressing
139
- // it forces the fleet's self-redeploy onto main's head now instead of waiting out the
158
+ // it forces the team's self-redeploy onto main's head now instead of waiting out the
140
159
  // cooldown. Only the build alert carries it, and only while the build is actually stale —
141
160
  // a fresh build's icon stays inert decoration.
142
161
  const glyph = icon(ALERT_ICONS[a.key] || "info");
@@ -194,7 +213,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
194
213
  todayAt = Date.now();
195
214
  } catch { /* keep the last numbers */ }
196
215
  todayLoading = false;
197
- if (lastStatus && activeView === "fleet") renderStats(lastStatus);
216
+ if (lastStatus && activeView === "team") renderStats(lastStatus);
198
217
  }
199
218
  // stats:start
200
219
  function statTile(t) {
@@ -203,27 +222,28 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
203
222
  "<span class='stat-sub'>" + t.sub + "</span>" + (t.meter || "") + (t.act ? "</button>" : "</div>");
204
223
  }
205
224
  function renderStats(d) {
206
- const loops = d.loops || [];
225
+ const loops = d.roles || [];
207
226
  // The running set (phaseInfo.live), not the permit-holder inFlight: the exempt running
208
227
  // director is genuinely in flight, so the tile must count it even though it holds no
209
228
  // maxConcurrent permit (BUGS.md 2026-10-06).
210
229
  const live = loops.filter((l) => phaseInfo(l.phase).live);
211
230
  const kinds = {};
212
231
  for (const l of live) { const k = phaseInfo(l.phase).key; kinds[k] = (kinds[k] || 0) + 1; }
232
+ const KIND_WORDS = { working: "working", reviewing: "reviewing", landing: "merging" }; // vocabulary-ok: phase key, renamed with the status model // retired-name-ok: phase key, renamed with the status model
213
233
  const liveSub = live.length
214
- ? ["working", "reviewing", "landing"].filter((k) => kinds[k]).map((k) => kinds[k] + " " + k).join(" · ")
215
- : d.running ? "every loop is between ticks" : "the fleet is stopped";
234
+ ? Object.keys(KIND_WORDS).filter((k) => kinds[k]).map((k) => kinds[k] + " " + KIND_WORDS[k]).join(" · ")
235
+ : d.running ? "every role is between runs" : "the team is stopped";
216
236
  const t = today && today.totals;
217
237
  const plans = (d.plans || []).length;
218
238
  const bugs = (d.bugs || []).length;
219
239
  const qs = (d.questions || []).length;
220
240
  paintPanel("stats", [
221
- { label: "In flight", icon: "bolt", value: live.length + " <small>of " + loops.length + " loops</small>", sub: esc(liveSub) },
222
- { label: "Landed today", icon: "merge", act: "view", arg: "history", hint: "See every tick in History",
241
+ { label: "In flight", icon: "bolt", value: live.length + " <small>of " + loops.length + " roles</small>", sub: esc(liveSub) },
242
+ { label: "Merged today", icon: "merge", act: "view", arg: "history", hint: "See every run in History",
223
243
  value: t ? String(t.commits) + " <small>" + agree(t.commits, "commit", "commits") + "</small>" : "—",
224
244
  sub: t ? esc(plural(t.featuresDone, "feature") + " done · " + plural(t.bugsFixed, "bug") + " fixed · " + workSplit(t)) : "counting…" },
225
- { label: "Ticks today", icon: "refresh", act: "view", arg: "usage", hint: "See usage per day and per loop",
226
- value: t ? String(t.ticks) + " <small>" + agree(t.ticks, "tick", "ticks") + "</small>" : "—",
245
+ { label: "Runs today", icon: "refresh", act: "view", arg: "usage", hint: "See usage per day and per role",
246
+ value: t ? String(t.runs) + " <small>" + agree(t.runs, "run", "runs") + "</small>" : "—",
227
247
  sub: t ? esc(fmtTokens(t.tokensOut) + " output tokens" + (t.costUsd > 0 ? " · " + fmtUsd(t.costUsd) : "")) : "counting…" },
228
248
  { label: "Backlog", icon: "inbox", act: "backlog", hint: "Jump to the backlog", value: String(plans + bugs) + " <small>open</small>",
229
249
  sub: esc(plural(plans, "plan") + " · " + plural(bugs, "bug") + (qs ? " · " + plural(qs, "question") : "")) },
@@ -245,6 +265,8 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
245
265
  // guard so a poll does not repaint over the typed title.
246
266
  let backlogNew = null;
247
267
  let backlogNewPainted = false;
268
+ // Names of the blocked series whose parts the Plans tab currently shows.
269
+ let openSeries = new Set();
248
270
  function setBacklogTab(tab) {
249
271
  backlogTab = tab;
250
272
  store("backlog", tab);
@@ -259,6 +281,10 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
259
281
  backlogNewPainted = false;
260
282
  renderBacklog(backlogStatus);
261
283
  }
284
+ function toggleBacklogSeries(name) {
285
+ if (openSeries.has(name)) openSeries.delete(name); else openSeries.add(name);
286
+ renderBacklog(backlogStatus);
287
+ }
262
288
  function endBacklogNew() {
263
289
  backlogNew = null;
264
290
  backlogNewPainted = false;
@@ -276,10 +302,10 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
276
302
  renderBacklog(backlogStatus);
277
303
  }
278
304
  const BACKLOG = [
279
- ["questions", "Questions", "question", "indigo", "No open questions", "Loops post here when they need a decision from you."],
280
- ["plans", "Plans", "plan", "blue", "Nothing planned", "The plan loop files features in PLANS.md."],
305
+ ["questions", "Questions", "question", "indigo", "No open questions", "Roles post here when they need a decision from you."],
306
+ ["plans", "Plans", "plan", "blue", "Nothing planned", "The plan role files features in PLANS.md."],
281
307
  ["bugs", "Bugs", "bug", "red", "No open bugs", "Bugs filed in BUGS.md show up here."],
282
- ["queued", "Queued", "chat", "gray", "No queued prompts", "Prompts you send wait here until their loop takes them."],
308
+ ["queued", "Inbox", "chat", "gray", "Inbox empty", "Messages you send wait here until their role's next run."], // vocabulary-ok: internal tab key
283
309
  ];
284
310
  function renderBacklog(d) {
285
311
  backlogStatus = d;
@@ -295,8 +321,16 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
295
321
  let html;
296
322
  if (!list.length) html = "<div class='empty'><strong>" + esc(emptyTitle) + "</strong>" + esc(emptyText) + "</div>";
297
323
  else if (key === "queued") {
324
+ // Each role's queue head carries no Move to top (a no-op there); every later row of
325
+ // that role does. The rows arrive grouped by role — director first, then each loop in
326
+ // id order — so a Set of roles already painted marks each group's first row.
327
+ const paintedFirst = new Set();
298
328
  html = list.map((q) => {
299
329
  const addr = "data-file='" + esc(q.file) + "' data-role='" + esc(q.role) + "'";
330
+ // Mark this role's group head before the edit-row early return, so an edited head row
331
+ // still leaves the Move to top button on that role's later rows.
332
+ const alreadyFirst = paintedFirst.has(q.role);
333
+ paintedFirst.add(q.role);
300
334
  // The row being edited swaps its preview for a textarea and a Save/Cancel pair; the
301
335
  // address stays the queue-file basename, the same one /api/prompt-cancel uses.
302
336
  if (queuedEdit && q.file === queuedEdit.file && q.role === queuedEdit.role) {
@@ -306,10 +340,14 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
306
340
  "<button type='button' class='btn btn-sm rowaction' data-action='prompteditsave' " + addr + ">Save</button>" +
307
341
  "<button type='button' class='btn btn-sm rowaction' data-action='prompteditcancel' " + addr + ">Cancel</button></div>";
308
342
  }
343
+ const topBtn = alreadyFirst
344
+ ? "<button type='button' class='btn btn-sm rowaction' data-action='prompttop' " + addr + ">Move to top</button>"
345
+ : "";
309
346
  return "<div class='list-item'><span class='li-icon'>" + icon("chat") + "</span><div class='li-main'><span class='li-title'>" + esc(q.preview) +
310
- "</span><span class='li-meta'>" + esc((q.role === "director" ? "for the director" : "for the " + q.role + " loop") +
347
+ "</span><span class='li-meta'>" + esc((q.role === "director" ? "for the director" : "for the " + roleLabel(q.role) + " role") +
311
348
  (q.notBeforeMs && q.notBeforeMs > Date.now() ? " · delivers in " + humanSeconds(Math.round((q.notBeforeMs - Date.now()) / 1000))
312
349
  : (q.queuedAtMs ? " · queued " + fmtAgo(q.queuedAtMs) : ""))) + "</span></div>" +
350
+ topBtn +
313
351
  "<button type='button' class='btn btn-sm rowaction' data-action='promptcancel' " + addr + ">Cancel</button>" +
314
352
  "<button type='button' class='btn btn-sm rowaction' data-action='promptedit' " + addr + ">Edit</button></div>";
315
353
  }).join("");
@@ -319,7 +357,10 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
319
357
  // the same tab position the click sends): empty for eligible rows so their markup is
320
358
  // unchanged, and the Questions tab carries no holds.
321
359
  const holds = key === "plans" ? (d.planHolds || []) : key === "bugs" ? (d.bugHolds || []) : [];
322
- html = list.map((title, i) => {
360
+ // Blocked entries (a hold of any kind) paint after the pickable ones under one divider;
361
+ // each row keeps its original data-index so the drawer and Answer still address it.
362
+ const blocked = key === "plans" ? (d.planBlocked || []) : key === "bugs" ? (d.bugBlocked || []) : [];
363
+ const rows = list.map((title, i) => {
323
364
  const t = splitTitle(title);
324
365
  const mark = holds[i] || "";
325
366
  return "<div class='list-item t-" + tone + (open === key + ":" + i ? " active" : "") + "'><span class='li-icon'>" + icon(ic) + "</span>" +
@@ -327,6 +368,37 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
327
368
  (t.meta ? "<span class='li-meta'>" + esc(t.meta) + "</span>" : "") +
328
369
  (mark ? "<span class='li-meta'>" + esc(mark.trim()) + "</span>" : "") + "</button>" +
329
370
  (key === "questions" ? "<button type='button' class='btn btn-sm backloglink' data-file='questions' data-index='" + i + "'>Answer</button>" : "") + "</div>";
371
+ });
372
+ const ready = rows.filter((r, i) => !blocked[i]);
373
+ const series = key === "plans" ? (d.planSeries || []) : [];
374
+ let heldCount = 0;
375
+ const held = [];
376
+ const seen = new Set();
377
+ rows.forEach((r, i) => {
378
+ if (!blocked[i]) return;
379
+ heldCount++;
380
+ const s = series[i];
381
+ if (!s) { held.push(r); return; }
382
+ if (seen.has(s.name)) return;
383
+ seen.add(s.name);
384
+ held.push("<div class='list-item list-series'><button type='button' class='li-open' data-action='seriestoggle' data-series='" + esc(s.name) + "'><span class='li-title'>" + esc(s.label) + "</span><span class='li-meta'>blocked: each waits on an earlier part</span></button></div>");
385
+ if (openSeries.has(s.name)) rows.forEach((r2, j) => { if (blocked[j] && series[j] && series[j].name === s.name) held.push(r2); });
386
+ });
387
+ html = ready.join("") + (heldCount ? "<div class='list-item list-divider'><span class='li-meta'>Blocked (" + heldCount + ")</span></div>" + held.join("") : "");
388
+ }
389
+ const answered = key === "questions" ? (d.answeredQuestions || []) : [];
390
+ if (answered.length) {
391
+ html += "<div class='list-item list-divider'><span class='li-meta'>Answered recently</span></div>" + answered.map((a, i) =>
392
+ "<div class='list-item t-gray" + (openEntryKey() === "answered:" + i ? " active" : "") + "'><span class='li-icon'>" + icon("check") + "</span><button type='button' class='li-open backloglink' data-file='answered' data-index='" + i + "'><span class='li-title'>" + esc(a.title) + "</span>" +
393
+ (a.decision ? "<span class='li-meta'>" + esc(a.decision) + "</span>" : "") + "</button></div>").join("");
394
+ }
395
+ const finished = key === "plans" ? (d.donePlans || []) : key === "bugs" ? (d.fixedBugs || []) : [];
396
+ if (finished.length) {
397
+ html += "<div class='list-item list-divider'><span class='li-meta'>" + (key === "plans" ? "Done recently" : "Fixed recently") + "</span></div>" + finished.map((f, i) => {
398
+ const doneFile = key === "plans" ? "done" : "fixed";
399
+ return "<div class='list-item t-gray" + (openEntryKey() === doneFile + ":" + i ? " active" : "") + "'><span class='li-icon'>" + icon("check") + "</span>" +
400
+ "<button type='button' class='li-open backloglink' data-file='" + doneFile + "' data-index='" + i + "'><span class='li-title'>" + esc(f.title) + "</span>" +
401
+ (f.completedOn ? "<span class='li-meta'>" + esc((key === "plans" ? "done " : "fixed ") + f.completedOn) + "</span>" : "") + "</button></div>";
330
402
  }).join("");
331
403
  }
332
404
  if (key === "plans" || key === "bugs") {
@@ -350,6 +422,8 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
350
422
  if (b) setBacklogTab(b.dataset.tab);
351
423
  });
352
424
  $("backlog").addEventListener("click", async (ev) => {
425
+ const ser = clickClosest(ev, "button[data-action='seriestoggle']");
426
+ if (ser) { toggleBacklogSeries(ser.dataset.series); return; }
353
427
  const fresh = clickClosest(ev, "button[data-action='backlognew']");
354
428
  if (fresh) { beginBacklogNew(fresh.dataset.kind); return; }
355
429
  if (clickClosest(ev, "button[data-action='backlognewcancel']")) { endBacklogNew(); return; }
@@ -359,16 +433,26 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
359
433
  const title = input ? input.value.trim() : "";
360
434
  if (!title) return;
361
435
  const bodyBox = $("backlognewbody");
362
- await postAction("/api/backlog-file", { kind: freshSave.dataset.kind, title, body: bodyBox ? bodyBox.value : "" }, (d) => "Filed: " + d.title);
436
+ await postAction("/api/backlog-file", { kind: freshSave.dataset.kind, title, body: bodyBox ? bodyBox.value : "" }, (d) => "Filed: " + d.title + uncommittedTail(d));
363
437
  endBacklogNew();
364
438
  return;
365
439
  }
440
+ const top = clickClosest(ev, "button.rowaction[data-action='prompttop']");
441
+ if (top) {
442
+ await postAction("/api/prompt-top", { role: top.dataset.role, file: top.dataset.file }, (d) =>
443
+ d && d.status === "topped"
444
+ ? "Moved to the front: " + (d.preview || "")
445
+ : d && d.status === "already_first"
446
+ ? "Already first"
447
+ : "No longer in the inbox — " + (top.dataset.role ? roleLabel(top.dataset.role) : "the director") + " already took it");
448
+ return;
449
+ }
366
450
  const cancel = clickClosest(ev, "button.rowaction[data-action='promptcancel']");
367
451
  if (cancel) {
368
452
  await postAction("/api/prompt-cancel", { role: cancel.dataset.role, file: cancel.dataset.file }, (d) =>
369
453
  d && d.status === "cancelled"
370
454
  ? "Cancelled: " + (d.preview || "")
371
- : "No longer queued — " + (cancel.dataset.role || "the director") + " already took it");
455
+ : "No longer in the inbox — " + (cancel.dataset.role ? roleLabel(cancel.dataset.role) : "the director") + " already took it");
372
456
  return;
373
457
  }
374
458
  const edit = clickClosest(ev, "button.rowaction[data-action='promptedit']");
@@ -376,7 +460,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
376
460
  try {
377
461
  const d = await getJson("/api/prompt-text?role=" + encodeURIComponent(edit.dataset.role) + "&file=" + encodeURIComponent(edit.dataset.file));
378
462
  if (d && d.status === "gone") {
379
- showFlash("No longer queued — " + (edit.dataset.role || "the director") + " already took it");
463
+ showFlash("No longer in the inbox — " + (edit.dataset.role ? roleLabel(edit.dataset.role) : "the director") + " already took it");
380
464
  refresh();
381
465
  } else {
382
466
  beginQueuedEdit(edit.dataset.role, edit.dataset.file, (d && d.text) || "");
@@ -394,7 +478,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
394
478
  await postAction("/api/prompt-edit", { role: save.dataset.role, file: save.dataset.file, text }, (d) =>
395
479
  d && d.status === "edited"
396
480
  ? "Edited: " + (d.newText || "")
397
- : "No longer queued — " + (save.dataset.role || "the director") + " already took it");
481
+ : "No longer in the inbox — " + (save.dataset.role ? roleLabel(save.dataset.role) : "the director") + " already took it");
398
482
  endQueuedEdit();
399
483
  return;
400
484
  }
@@ -411,12 +495,14 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
411
495
  // The Activity card's own narrowing, beside the Notable/All segmented choice: a Loop picker
412
496
  // and a text box filter the events the card carries, and — once a filter is set — a debounced
413
497
  // /api/logs search reaches past that carried window into the log's recent slice (the same
414
- // loop/match rule 'tumwater logs --role'/'--grep' apply to the CLI feed). Both persist like
415
- // the Notable/All choice, so a reload keeps the operator's scope.
498
+ // loop/match rule 'tumwater logs --role'/'--grep' apply to the CLI feed), while the tally
499
+ // chips select one exact event type the way 'tumwater logs --type <type>' does. All persist
500
+ // like the Notable/All choice, so a reload keeps the operator's scope.
416
501
  let feedFilter = recall("feed") === "all" ? "all" : "notable";
417
502
  let feedRole = recall("feed-role") || "";
503
+ let feedType = recall("feed-type") || "";
418
504
  let feedText = recall("feed-grep") || "";
419
- const KINDS = { landing: ["merge", "green"], problem: ["fail", "red"], attention: ["question", "indigo"], info: ["info", "blue"], routine: ["dot", "gray"] };
505
+ const KINDS = { landing: ["merge", "green"], problem: ["fail", "red"], attention: ["question", "indigo"], info: ["info", "blue"], routine: ["dot", "gray"] }; // retired-name-ok: event kind key, renamed with the status model
420
506
  // Server-backed search state. feedResults is the /api/logs slice once it answers (newest
421
507
  // first, the shape renderFeed consumes), null while the card paints its carried window; the
422
508
  // debounce feedTimer keeps each keystroke from firing a request; feedSeq numbers requests
@@ -429,7 +515,9 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
429
515
  let feedError = "";
430
516
  let feedQueryKey = "";
431
517
  // Only the non-empty filters ride the query: an absent grep/role means unfiltered server-side,
432
- // exactly as handleLogs reads them.
518
+ // exactly as handleLogs reads them. The type is deliberately NOT carried: the server slice must
519
+ // still deliver every type so the tally chips can be built from it — the type is a local
520
+ // narrowing on the rows in hand, like the Loop picker, not a query narrowing.
433
521
  function feedLogsQuery() {
434
522
  const params = [];
435
523
  if (feedText) params.push("grep=" + encodeURIComponent(feedText));
@@ -460,12 +548,36 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
460
548
  }
461
549
  if (lastStatus) renderFeed(lastStatus);
462
550
  }
463
- // The Loop picker's options follow the fleet's role list (custom loops included), keyed so a
464
- // poll with no roster change repaints nothing. A role the fleet dropped falls back to All
551
+ // The Loop picker's options follow the team's role list (custom loops included), keyed so a
552
+ // poll with no roster change repaints nothing. A role the team dropped falls back to All
465
553
  // loops and persists, exactly as syncRolePicker does for the report and History pickers.
466
554
  function syncFeedRoles(d) {
467
555
  if (!d) return;
468
- feedRole = syncRolePicker("feedrole", (d.loops || []).map((l) => l.role), feedRole, "feed-role");
556
+ feedRole = syncRolePicker("feedrole", (d.roles || []).map((l) => l.role), feedRole, "feed-role");
557
+ }
558
+ // An empty feed state: a li.empty with a bold head, a body, and the shared Clear-filter
559
+ // button that ends all three search/error/filter-empty rows — one home for the button's
560
+ // markup so the three cannot drift. The last empty state (nothing notable / no events yet)
561
+ // carries no Clear-filter action, so it stays literal.
562
+ const emptyFeedRow = (head, body) => "<li class='empty'><strong>" + head + "</strong>" + body +
563
+ "<button type='button' class='linkish' data-action='feedclear'>Clear filter</button></li>";
564
+ // A per-type tally for the Activity feed, mirroring the CLI's logs --count display rule
565
+ // (read-only from src/cli/log-commands.ts's sayTally): one N × <type> chip per present event
566
+ // type, count-descending then type-name ascending. A pure function of the rows in hand, so the
567
+ // chips and the <li> rows below cannot drift on which events count; "" for an empty array so
568
+ // empty and empty-state feeds paint no stray chips. Each chip is a button: clicking the
569
+ // currently-selected type clears it, otherwise it selects that type (the active one carries
570
+ // the active class and aria-pressed), the browser twin of 'logs --type <type>'.
571
+ function tallyChips(events, activeType) {
572
+ if (!events.length) return "";
573
+ const counts = new Map();
574
+ for (const e of events) counts.set(e.type, (counts.get(e.type) || 0) + 1);
575
+ const chip = (type, n) => "<button type='button' class='chip mono" + (type === activeType ? " active" : "") +
576
+ "' data-type='" + esc(type) + "' title='" + esc(type) + "'" + (type === activeType ? " aria-pressed='true'" : "") +
577
+ ">" + n + " × " + esc(eventTypeLabel(type)) + "</button>";
578
+ return "<div class='feed-tally chips'>" + [...counts.entries()]
579
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
580
+ .map(([type, n]) => chip(type, n)).join("") + "</div>";
469
581
  }
470
582
  function renderFeed(d) {
471
583
  syncFeedRoles(d);
@@ -480,48 +592,57 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
480
592
  const notable = feedFilter === "all" ? items : items.filter((it) => eventKind(it) !== "routine");
481
593
  const needle = feedText.toLowerCase();
482
594
  const local = notable.filter((it) =>
483
- (!feedRole || it.loop === feedRole) &&
484
- (!needle || (it.loop + " " + it.type + " " + it.message).toLowerCase().includes(needle)));
595
+ (!feedRole || it.source === feedRole) &&
596
+ (!needle || (it.source + " " + it.type + " " + it.message).toLowerCase().includes(needle)));
485
597
  // The server's /api/logs slice replaces the carried window once it answers; the Notable/All
486
598
  // choice still culls it. A failed fetch paints the search-empty state, never a misleading
487
599
  // partial window.
488
600
  const serverRows = feedResults !== null
489
601
  ? (feedFilter === "all" ? feedResults : feedResults.filter((it) => eventKind(it) !== "routine"))
490
602
  : null;
491
- const shown = feedError ? [] : serverRows !== null ? serverRows : local;
603
+ // The type chip narrows whichever slice is in hand (carried window or server slice), so it
604
+ // composes after the Notable/All cull and the role/grep narrowing. The chips are built from
605
+ // the pre-type rows so every present type stays one click away even when the filter empties
606
+ // the feed.
607
+ const preType = feedError ? [] : serverRows !== null ? serverRows : local;
608
+ const shown = feedType ? preType.filter((it) => it.type === feedType) : preType;
492
609
  markActive($("feedfilter"), "filter", feedFilter);
493
- const roles = new Set((d.loops || []).map((l) => l.role));
494
- paintPanel("feed", shown.length
610
+ const roles = new Set((d.roles || []).map((l) => l.role));
611
+ paintPanel("feed", (preType.length ? tallyChips(preType, feedType) : "") + (shown.length
495
612
  ? shown.map((it) => {
496
613
  const kind = eventKind(it);
497
- const tone = it.type === "tick_end" && kind === "problem" ? resultInfo(it.result).tone : KINDS[kind][1];
498
- const who = roles.has(it.loop)
499
- ? "<button type='button' class='linkish feed-loop' data-open='" + esc(it.loop) + "'>" + esc(it.loop) + "</button>"
500
- : "<span class='feed-loop'>" + esc(it.loop) + "</span>";
614
+ const tone = it.type === "run_end" && kind === "problem" ? resultInfo(it.result).tone : KINDS[kind][1];
615
+ const whoLabel = it.source === "harness" ? "tumwater" : roleLabel(it.source); // vocabulary-ok: the harness event source
616
+ const who = roles.has(it.source) // vocabulary-ok: event field name
617
+ ? "<button type='button' class='linkish feed-loop' data-open='" + esc(it.source) + "'>" + esc(whoLabel) + "</button>" // vocabulary-ok: CSS class and event field
618
+ : "<span class='feed-loop'>" + esc(whoLabel) + "</span>"; // vocabulary-ok: CSS class
501
619
  return "<li class='feed-item k-" + kind + "'><span class='feed-icon t-" + tone + "'>" + icon(KINDS[kind][0]) + "</span>" +
502
620
  "<div class='feed-msg clamp2' title='" + esc(it.message) + "'>" + who + esc(it.message) + "</div>" +
503
621
  "<time class='feed-time' title='" + esc(fmtClock(it.ts)) + "'>" + esc(fmtAgo(it.ts)) + "</time></li>";
504
622
  }).join("")
505
623
  : feedResults !== null
506
- ? "<li class='empty'><strong>No events match this filter in the log's recent slice</strong>" +
507
- "Widen the Search or Loop picker to look further back. " +
508
- "<button type='button' class='linkish' data-action='feedclear'>Clear filter</button></li>"
624
+ ? emptyFeedRow("No events match this filter in the log's recent slice", "Widen the Search or Role picker, or clear the type chip, to look further back. ")
509
625
  : feedError
510
- ? "<li class='empty'><strong>Search unavailable</strong>" + esc(feedError) +
511
- " — the running server may predate the /api/logs endpoint. " +
512
- "<button type='button' class='linkish' data-action='feedclear'>Clear filter</button></li>"
513
- : notable.length
514
- ? "<li class='empty'><strong>No recent events match this filter</strong>" +
515
- "Clear the Loop or Search box to see the recent feed again. " +
516
- "<button type='button' class='linkish' data-action='feedclear'>Clear filter</button></li>"
626
+ ? emptyFeedRow("Search unavailable", esc(feedError) + " — the running server may predate the /api/logs endpoint. ")
627
+ : notable.length || preType.length
628
+ ? emptyFeedRow("No recent events match this filter", "Clear the Role, Search, or type chip to see the recent feed again. ")
517
629
  : "<li class='empty'><strong>" + (items.length ? "Nothing notable lately" : "No events yet") + "</strong>" +
518
- (items.length ? "Landings, failures, and questions show up here. All events has every tick start and check." : "Events appear here as the loops work.") + "</li>");
630
+ (items.length ? "Merges, failures, and questions show up here. All events has every run start and check." : "Events appear here as the roles work.") + "</li>"));
631
+ }
632
+ // The single state change both the chip click and Clear filter use: select a type (stored so
633
+ // a reload keeps the scope), or clear it by passing "".
634
+ function setFeedType(type) {
635
+ feedType = type;
636
+ store("feed-type", type);
637
+ if (lastStatus) renderFeed(lastStatus);
519
638
  }
520
639
  function clearFeedFilter() {
521
640
  feedRole = "";
522
641
  feedText = "";
642
+ feedType = "";
523
643
  store("feed-role", "");
524
644
  store("feed-grep", "");
645
+ store("feed-type", "");
525
646
  if ($("feedrole")) $("feedrole").value = "";
526
647
  if ($("feedgrep")) $("feedgrep").value = "";
527
648
  if (lastStatus) renderFeed(lastStatus);
@@ -549,17 +670,20 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
549
670
  if (lastStatus) renderFeed(lastStatus);
550
671
  });
551
672
  // The empty state's #feed is static markup, so its delegated listener registers at load like
552
- // the #feedfilter one above.
673
+ // the #feedfilter one above. A chip click whose type matches the current feedType clears it
674
+ // (toggle off), otherwise it selects that type — the same toggle the Notable/All control uses.
553
675
  $("feed").addEventListener("click", (ev) => {
554
- if (clickClosest(ev, "button[data-action='feedclear']")) clearFeedFilter();
676
+ if (clickClosest(ev, "button[data-action='feedclear']")) { clearFeedFilter(); return; }
677
+ const b = clickClosest(ev, "button[data-type]");
678
+ if (b) setFeedType(b.dataset.type === feedType ? "" : b.dataset.type);
555
679
  });
556
680
  // activity:end
557
681
 
558
- function renderFleet(d) {
559
- const loops = d.loops || [];
560
- const sub = plural(loops.length, "loop") + " · " + (d.running ? loops.filter((l) => phaseInfo(l.phase).live).length + " in flight" : "the fleet is stopped") +
682
+ function renderTeam(d) {
683
+ const loops = d.roles || [];
684
+ const sub = plural(loops.length, "role") + " · " + (d.running ? loops.filter((l) => phaseInfo(l.phase).live).length + " in flight" : "the team is stopped") +
561
685
  (d.paused ? " · paused" : "");
562
- if ($("fleetsub").textContent !== sub) $("fleetsub").textContent = sub;
686
+ if ($("teamsub").textContent !== sub) $("teamsub").textContent = sub;
563
687
  renderStats(d);
564
688
  renderLoops(d);
565
689
  renderBacklog(d);