tumwater 0.1.3 → 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 (359) hide show
  1. package/README.md +63 -28
  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-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. 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,35 +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.");
84
95
  }
85
96
  // The run --for deadline: the payload's preformatted runWindowBadge, like the disk row.
86
97
  if (d.runWindowBadge) {
87
98
  rows += row("blue", icon("info"), esc(badgeLabel(d.runWindowBadge)),
88
99
  d.scheduledStopAt !== undefined && d.scheduledStopAt === d.runStopsAt
89
- ? "A scheduled stop (tumwater stop --in) — the fleet drains in-flight work and exits then."
90
- : "This run was started with run --for; at the deadline it drains in-flight work, prints a per-loop summary, and exits.");
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.");
91
102
  }
92
103
  // The configured quiet-hours window ("Quiet hours … part 2/2"): the schedule as
93
104
  // standing information, amber while the local clock is inside it — the same
94
105
  // inside/outside wording the TUI/status header's quietBadge carries, derived from the
95
106
  // payload's raw quietHours/inQuietHours fields (the active case also raises the
96
- // fleet-alerts quiet alert above).
107
+ // team-alerts quiet alert above).
97
108
  if (d.quietHours) {
98
109
  const end = String(d.quietHours).split("-")[1] || "";
99
110
  rows += row(d.inQuietHours ? "amber" : "", icon("pause"),
100
111
  d.inQuietHours ? "Quiet until " + esc(end) : "Quiet " + esc(d.quietHours),
101
- "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");
102
113
  }
103
114
  // New-project bootstrap (plans/work-ratio.md, part 2/2): the payload's preformatted
104
115
  // bootstrapBadge, upcased for the row's label style — the same reading the TUI/status
@@ -106,7 +117,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
106
117
  if (d.bootstrapBadge) {
107
118
  rows += row("blue", icon("info"),
108
119
  esc(badgeLabel(d.bootstrapBadge)),
109
- "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");
110
121
  }
111
122
  }
112
123
  paintPanel("statuschips", rows);
@@ -127,7 +138,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
127
138
  // i * 2000 spacing exists for the test path, which passes a stepped clock so each fresh
128
139
  // alert clears the window and sounds its own cue. The keys seen now are kept for the next
129
140
  // poll's diff; lastNeedsYouKeys starts null, so a page opened onto an already-alerting
130
- // fleet cues once.
141
+ // team cues once.
131
142
  let lastNeedsYouKeys = null;
132
143
  function cueNewNeedsYou(alerts, now) {
133
144
  const fresh = newNeedsYouKeys(lastNeedsYouKeys, alerts);
@@ -144,7 +155,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
144
155
  // from under a click in progress.
145
156
  function alertParts(a, restartable) {
146
157
  // A stale-build alert's refresh icon IS the restart button (PLANS.md 2026-09-30): pressing
147
- // 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
148
159
  // cooldown. Only the build alert carries it, and only while the build is actually stale —
149
160
  // a fresh build's icon stays inert decoration.
150
161
  const glyph = icon(ALERT_ICONS[a.key] || "info");
@@ -202,7 +213,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
202
213
  todayAt = Date.now();
203
214
  } catch { /* keep the last numbers */ }
204
215
  todayLoading = false;
205
- if (lastStatus && activeView === "fleet") renderStats(lastStatus);
216
+ if (lastStatus && activeView === "team") renderStats(lastStatus);
206
217
  }
207
218
  // stats:start
208
219
  function statTile(t) {
@@ -211,27 +222,28 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
211
222
  "<span class='stat-sub'>" + t.sub + "</span>" + (t.meter || "") + (t.act ? "</button>" : "</div>");
212
223
  }
213
224
  function renderStats(d) {
214
- const loops = d.loops || [];
225
+ const loops = d.roles || [];
215
226
  // The running set (phaseInfo.live), not the permit-holder inFlight: the exempt running
216
227
  // director is genuinely in flight, so the tile must count it even though it holds no
217
228
  // maxConcurrent permit (BUGS.md 2026-10-06).
218
229
  const live = loops.filter((l) => phaseInfo(l.phase).live);
219
230
  const kinds = {};
220
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
221
233
  const liveSub = live.length
222
- ? ["working", "reviewing", "landing"].filter((k) => kinds[k]).map((k) => kinds[k] + " " + k).join(" · ")
223
- : 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";
224
236
  const t = today && today.totals;
225
237
  const plans = (d.plans || []).length;
226
238
  const bugs = (d.bugs || []).length;
227
239
  const qs = (d.questions || []).length;
228
240
  paintPanel("stats", [
229
- { label: "In flight", icon: "bolt", value: live.length + " <small>of " + loops.length + " loops</small>", sub: esc(liveSub) },
230
- { 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",
231
243
  value: t ? String(t.commits) + " <small>" + agree(t.commits, "commit", "commits") + "</small>" : "—",
232
244
  sub: t ? esc(plural(t.featuresDone, "feature") + " done · " + plural(t.bugsFixed, "bug") + " fixed · " + workSplit(t)) : "counting…" },
233
- { label: "Ticks today", icon: "refresh", act: "view", arg: "usage", hint: "See usage per day and per loop",
234
- 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>" : "—",
235
247
  sub: t ? esc(fmtTokens(t.tokensOut) + " output tokens" + (t.costUsd > 0 ? " · " + fmtUsd(t.costUsd) : "")) : "counting…" },
236
248
  { label: "Backlog", icon: "inbox", act: "backlog", hint: "Jump to the backlog", value: String(plans + bugs) + " <small>open</small>",
237
249
  sub: esc(plural(plans, "plan") + " · " + plural(bugs, "bug") + (qs ? " · " + plural(qs, "question") : "")) },
@@ -253,6 +265,8 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
253
265
  // guard so a poll does not repaint over the typed title.
254
266
  let backlogNew = null;
255
267
  let backlogNewPainted = false;
268
+ // Names of the blocked series whose parts the Plans tab currently shows.
269
+ let openSeries = new Set();
256
270
  function setBacklogTab(tab) {
257
271
  backlogTab = tab;
258
272
  store("backlog", tab);
@@ -267,6 +281,10 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
267
281
  backlogNewPainted = false;
268
282
  renderBacklog(backlogStatus);
269
283
  }
284
+ function toggleBacklogSeries(name) {
285
+ if (openSeries.has(name)) openSeries.delete(name); else openSeries.add(name);
286
+ renderBacklog(backlogStatus);
287
+ }
270
288
  function endBacklogNew() {
271
289
  backlogNew = null;
272
290
  backlogNewPainted = false;
@@ -284,10 +302,10 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
284
302
  renderBacklog(backlogStatus);
285
303
  }
286
304
  const BACKLOG = [
287
- ["questions", "Questions", "question", "indigo", "No open questions", "Loops post here when they need a decision from you."],
288
- ["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."],
289
307
  ["bugs", "Bugs", "bug", "red", "No open bugs", "Bugs filed in BUGS.md show up here."],
290
- ["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
291
309
  ];
292
310
  function renderBacklog(d) {
293
311
  backlogStatus = d;
@@ -303,8 +321,16 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
303
321
  let html;
304
322
  if (!list.length) html = "<div class='empty'><strong>" + esc(emptyTitle) + "</strong>" + esc(emptyText) + "</div>";
305
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();
306
328
  html = list.map((q) => {
307
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);
308
334
  // The row being edited swaps its preview for a textarea and a Save/Cancel pair; the
309
335
  // address stays the queue-file basename, the same one /api/prompt-cancel uses.
310
336
  if (queuedEdit && q.file === queuedEdit.file && q.role === queuedEdit.role) {
@@ -314,10 +340,14 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
314
340
  "<button type='button' class='btn btn-sm rowaction' data-action='prompteditsave' " + addr + ">Save</button>" +
315
341
  "<button type='button' class='btn btn-sm rowaction' data-action='prompteditcancel' " + addr + ">Cancel</button></div>";
316
342
  }
343
+ const topBtn = alreadyFirst
344
+ ? "<button type='button' class='btn btn-sm rowaction' data-action='prompttop' " + addr + ">Move to top</button>"
345
+ : "";
317
346
  return "<div class='list-item'><span class='li-icon'>" + icon("chat") + "</span><div class='li-main'><span class='li-title'>" + esc(q.preview) +
318
- "</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") +
319
348
  (q.notBeforeMs && q.notBeforeMs > Date.now() ? " · delivers in " + humanSeconds(Math.round((q.notBeforeMs - Date.now()) / 1000))
320
349
  : (q.queuedAtMs ? " · queued " + fmtAgo(q.queuedAtMs) : ""))) + "</span></div>" +
350
+ topBtn +
321
351
  "<button type='button' class='btn btn-sm rowaction' data-action='promptcancel' " + addr + ">Cancel</button>" +
322
352
  "<button type='button' class='btn btn-sm rowaction' data-action='promptedit' " + addr + ">Edit</button></div>";
323
353
  }).join("");
@@ -327,7 +357,10 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
327
357
  // the same tab position the click sends): empty for eligible rows so their markup is
328
358
  // unchanged, and the Questions tab carries no holds.
329
359
  const holds = key === "plans" ? (d.planHolds || []) : key === "bugs" ? (d.bugHolds || []) : [];
330
- 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) => {
331
364
  const t = splitTitle(title);
332
365
  const mark = holds[i] || "";
333
366
  return "<div class='list-item t-" + tone + (open === key + ":" + i ? " active" : "") + "'><span class='li-icon'>" + icon(ic) + "</span>" +
@@ -335,6 +368,37 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
335
368
  (t.meta ? "<span class='li-meta'>" + esc(t.meta) + "</span>" : "") +
336
369
  (mark ? "<span class='li-meta'>" + esc(mark.trim()) + "</span>" : "") + "</button>" +
337
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>";
338
402
  }).join("");
339
403
  }
340
404
  if (key === "plans" || key === "bugs") {
@@ -358,6 +422,8 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
358
422
  if (b) setBacklogTab(b.dataset.tab);
359
423
  });
360
424
  $("backlog").addEventListener("click", async (ev) => {
425
+ const ser = clickClosest(ev, "button[data-action='seriestoggle']");
426
+ if (ser) { toggleBacklogSeries(ser.dataset.series); return; }
361
427
  const fresh = clickClosest(ev, "button[data-action='backlognew']");
362
428
  if (fresh) { beginBacklogNew(fresh.dataset.kind); return; }
363
429
  if (clickClosest(ev, "button[data-action='backlognewcancel']")) { endBacklogNew(); return; }
@@ -367,16 +433,26 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
367
433
  const title = input ? input.value.trim() : "";
368
434
  if (!title) return;
369
435
  const bodyBox = $("backlognewbody");
370
- await postAction("/api/backlog-file", { kind: freshSave.dataset.kind, title, body: bodyBox ? bodyBox.value : "" }, (d) => "Filed: " + d.title + (d.committed === false ? ", but NOT committed to main (uncommitted in the checkout)" : ""));
436
+ await postAction("/api/backlog-file", { kind: freshSave.dataset.kind, title, body: bodyBox ? bodyBox.value : "" }, (d) => "Filed: " + d.title + uncommittedTail(d));
371
437
  endBacklogNew();
372
438
  return;
373
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
+ }
374
450
  const cancel = clickClosest(ev, "button.rowaction[data-action='promptcancel']");
375
451
  if (cancel) {
376
452
  await postAction("/api/prompt-cancel", { role: cancel.dataset.role, file: cancel.dataset.file }, (d) =>
377
453
  d && d.status === "cancelled"
378
454
  ? "Cancelled: " + (d.preview || "")
379
- : "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");
380
456
  return;
381
457
  }
382
458
  const edit = clickClosest(ev, "button.rowaction[data-action='promptedit']");
@@ -384,7 +460,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
384
460
  try {
385
461
  const d = await getJson("/api/prompt-text?role=" + encodeURIComponent(edit.dataset.role) + "&file=" + encodeURIComponent(edit.dataset.file));
386
462
  if (d && d.status === "gone") {
387
- 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");
388
464
  refresh();
389
465
  } else {
390
466
  beginQueuedEdit(edit.dataset.role, edit.dataset.file, (d && d.text) || "");
@@ -402,7 +478,7 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
402
478
  await postAction("/api/prompt-edit", { role: save.dataset.role, file: save.dataset.file, text }, (d) =>
403
479
  d && d.status === "edited"
404
480
  ? "Edited: " + (d.newText || "")
405
- : "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");
406
482
  endQueuedEdit();
407
483
  return;
408
484
  }
@@ -419,12 +495,14 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
419
495
  // The Activity card's own narrowing, beside the Notable/All segmented choice: a Loop picker
420
496
  // and a text box filter the events the card carries, and — once a filter is set — a debounced
421
497
  // /api/logs search reaches past that carried window into the log's recent slice (the same
422
- // loop/match rule 'tumwater logs --role'/'--grep' apply to the CLI feed). Both persist like
423
- // 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.
424
501
  let feedFilter = recall("feed") === "all" ? "all" : "notable";
425
502
  let feedRole = recall("feed-role") || "";
503
+ let feedType = recall("feed-type") || "";
426
504
  let feedText = recall("feed-grep") || "";
427
- 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
428
506
  // Server-backed search state. feedResults is the /api/logs slice once it answers (newest
429
507
  // first, the shape renderFeed consumes), null while the card paints its carried window; the
430
508
  // debounce feedTimer keeps each keystroke from firing a request; feedSeq numbers requests
@@ -437,7 +515,9 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
437
515
  let feedError = "";
438
516
  let feedQueryKey = "";
439
517
  // Only the non-empty filters ride the query: an absent grep/role means unfiltered server-side,
440
- // 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.
441
521
  function feedLogsQuery() {
442
522
  const params = [];
443
523
  if (feedText) params.push("grep=" + encodeURIComponent(feedText));
@@ -468,12 +548,36 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
468
548
  }
469
549
  if (lastStatus) renderFeed(lastStatus);
470
550
  }
471
- // The Loop picker's options follow the fleet's role list (custom loops included), keyed so a
472
- // 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
473
553
  // loops and persists, exactly as syncRolePicker does for the report and History pickers.
474
554
  function syncFeedRoles(d) {
475
555
  if (!d) return;
476
- 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>";
477
581
  }
478
582
  function renderFeed(d) {
479
583
  syncFeedRoles(d);
@@ -488,48 +592,57 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
488
592
  const notable = feedFilter === "all" ? items : items.filter((it) => eventKind(it) !== "routine");
489
593
  const needle = feedText.toLowerCase();
490
594
  const local = notable.filter((it) =>
491
- (!feedRole || it.loop === feedRole) &&
492
- (!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)));
493
597
  // The server's /api/logs slice replaces the carried window once it answers; the Notable/All
494
598
  // choice still culls it. A failed fetch paints the search-empty state, never a misleading
495
599
  // partial window.
496
600
  const serverRows = feedResults !== null
497
601
  ? (feedFilter === "all" ? feedResults : feedResults.filter((it) => eventKind(it) !== "routine"))
498
602
  : null;
499
- 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;
500
609
  markActive($("feedfilter"), "filter", feedFilter);
501
- const roles = new Set((d.loops || []).map((l) => l.role));
502
- 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
503
612
  ? shown.map((it) => {
504
613
  const kind = eventKind(it);
505
- const tone = it.type === "tick_end" && kind === "problem" ? resultInfo(it.result).tone : KINDS[kind][1];
506
- const who = roles.has(it.loop)
507
- ? "<button type='button' class='linkish feed-loop' data-open='" + esc(it.loop) + "'>" + esc(it.loop) + "</button>"
508
- : "<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
509
619
  return "<li class='feed-item k-" + kind + "'><span class='feed-icon t-" + tone + "'>" + icon(KINDS[kind][0]) + "</span>" +
510
620
  "<div class='feed-msg clamp2' title='" + esc(it.message) + "'>" + who + esc(it.message) + "</div>" +
511
621
  "<time class='feed-time' title='" + esc(fmtClock(it.ts)) + "'>" + esc(fmtAgo(it.ts)) + "</time></li>";
512
622
  }).join("")
513
623
  : feedResults !== null
514
- ? "<li class='empty'><strong>No events match this filter in the log's recent slice</strong>" +
515
- "Widen the Search or Loop picker to look further back. " +
516
- "<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. ")
517
625
  : feedError
518
- ? "<li class='empty'><strong>Search unavailable</strong>" + esc(feedError) +
519
- " — the running server may predate the /api/logs endpoint. " +
520
- "<button type='button' class='linkish' data-action='feedclear'>Clear filter</button></li>"
521
- : notable.length
522
- ? "<li class='empty'><strong>No recent events match this filter</strong>" +
523
- "Clear the Loop or Search box to see the recent feed again. " +
524
- "<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. ")
525
629
  : "<li class='empty'><strong>" + (items.length ? "Nothing notable lately" : "No events yet") + "</strong>" +
526
- (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);
527
638
  }
528
639
  function clearFeedFilter() {
529
640
  feedRole = "";
530
641
  feedText = "";
642
+ feedType = "";
531
643
  store("feed-role", "");
532
644
  store("feed-grep", "");
645
+ store("feed-type", "");
533
646
  if ($("feedrole")) $("feedrole").value = "";
534
647
  if ($("feedgrep")) $("feedgrep").value = "";
535
648
  if (lastStatus) renderFeed(lastStatus);
@@ -557,17 +670,20 @@ export const GUI_CLIENT_FLEET_JS = String.raw ` // ---- sidebar: project, fleet
557
670
  if (lastStatus) renderFeed(lastStatus);
558
671
  });
559
672
  // The empty state's #feed is static markup, so its delegated listener registers at load like
560
- // 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.
561
675
  $("feed").addEventListener("click", (ev) => {
562
- 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);
563
679
  });
564
680
  // activity:end
565
681
 
566
- function renderFleet(d) {
567
- const loops = d.loops || [];
568
- 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") +
569
685
  (d.paused ? " · paused" : "");
570
- if ($("fleetsub").textContent !== sub) $("fleetsub").textContent = sub;
686
+ if ($("teamsub").textContent !== sub) $("teamsub").textContent = sub;
571
687
  renderStats(d);
572
688
  renderLoops(d);
573
689
  renderBacklog(d);