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,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** The dashboard's view model, browser-side — the pure layer between the status payload and
2
3
  * what the page shows, with no DOM of its own: each loop's phase pill (label, tone, live
3
4
  * detail), its last result in words with the tone of its outcome, the loop sort order and
@@ -17,53 +18,71 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
17
18
  // How a loop's phase label (status-model.ts loopPhase) reads here: a status word and tone for
18
19
  // its pill, whether it is live work, and the detail the label carried after its first word.
19
20
  function phaseInfo(phase) {
20
- const p = String(phase || "");
21
+ // status-model.ts loopPhase appends a " · no change ×N" streak suffix to a resting idle
22
+ // cell's label. Strip it so the exact/startsWith matches and the parsed detail see the
23
+ // plain phase; phaseDetailBase re-appends the streak once itself from the payload's
24
+ // noChangeStreak, so the GUI renders it exactly once.
25
+ const p = String(phase || "").replace(/ · no change ×\d+$/, "");
21
26
  const after = (n) => p.slice(n).replace(/^[\s·,]+/, "");
22
27
  if (p.startsWith("director working")) return { key: "working", label: "Working (director)", tone: "blue", live: true, detail: after(16) };
23
28
  if (p.startsWith("working")) return { key: "working", label: "Working", tone: "blue", live: true, detail: after(7) };
24
29
  if (p.startsWith("reviewing")) return { key: "reviewing", label: "Reviewing", tone: "violet", live: true, detail: after(9) };
25
- if (p.startsWith("landing")) return { key: "landing", label: "Landing", tone: "orange", live: true, detail: after(7) };
26
- if (p.startsWith("vetted")) return { key: "vetted", label: "Approved", tone: "indigo", live: false, detail: "waiting for the merge slot" };
27
- if (p.startsWith("awaiting slot")) return { key: "awaiting", label: "Waiting for a slot", tone: "gray", live: false, detail: after(13) ? "for " + after(13) : "" };
28
- if (p === "failing") return { key: "failing", label: "Failing", tone: "red", live: false, detail: "the same error, tick after tick" };
29
- if (p === "main red") return { key: "mainred", label: "Main red", tone: "red", live: false, detail: "blocked until main's suite passes" };
30
- if (p === "paused") return { key: "paused", label: "Paused", tone: "amber", live: false, detail: "starts no new ticks" };
31
- if (p === "budget paused") return { key: "budget", label: "Budget paused", tone: "amber", live: false, detail: "today's cap is spent" };
32
- if (p === "cap paused") return { key: "cap", label: "Cap paused", tone: "amber", live: false, detail: "its own daily cap is spent" };
33
- if (p === "disk hold") return { key: "disk", label: "Disk hold", tone: "amber", live: false, detail: "free space is below the floor" };
34
- if (p === "held: bootstrap") return { key: "bootstrap", label: "Bootstrap hold", tone: "amber", live: false, detail: "waiting for enough plans to be done" };
35
- if (p.startsWith("held: maintenance quota")) return { key: "quota", label: "Maintenance hold", tone: "amber", live: false, detail: after(24) };
30
+ if (p.startsWith("merging")) return { key: "landing", label: "Merging", tone: "orange", live: true, detail: after(7) }; // vocabulary-ok: internal key the row classes and filters match
31
+ if (p.startsWith("approved")) return { key: "vetted", label: "Approved", tone: "indigo", live: false, detail: "waiting to merge" }; // vocabulary-ok: internal key the row classes and filters match
32
+ if (p.startsWith("waiting to start")) return { key: "awaiting", label: "Waiting to start", tone: "gray", live: false, detail: after(16) ? "for " + after(16) : "" };
33
+ // A waiter parked on a full provider key (provider-concurrency part 5/5): the phase reads
34
+ // waiting for <key> 2m, and this chip names the provider alongside the wait, so the
35
+ // operator sees which cap is full — the same chip and tone as a team-total waiter.
36
+ if (p.startsWith("waiting for ")) {
37
+ const m = after(12).match(/^(\S+)(?:\s+(.*))?$/);
38
+ const prov = m ? m[1] : "";
39
+ const dur = m && m[2] ? m[2].trim() : "";
40
+ return {
41
+ key: "awaiting", label: "Waiting to start", tone: "gray", live: false,
42
+ detail: (prov ? "on " + prov : "") + (dur ? " for " + dur : ""),
43
+ };
44
+ }
45
+ if (p === "failing") return { key: "failing", label: "Failing", tone: "red", live: false, detail: "failed runs in a row" };
46
+ if (p === "main's check is failing") return { key: "mainred", label: "Main's check failing", tone: "red", live: false, detail: "blocked until main's check passes" };
47
+ if (p.startsWith("paused")) return { key: "paused", label: "Paused", tone: "amber", live: false, detail: "starts no new runs" };
48
+ if (p === "waiting: budget spent") return { key: "budget", label: "Waiting", tone: "amber", live: false, detail: "budget" };
49
+ if (p === "waiting: daily cap spent") return { key: "cap", label: "Waiting", tone: "amber", live: false, detail: "daily cap" };
50
+ if (p === "waiting: low disk") return { key: "disk", label: "Waiting", tone: "amber", live: false, detail: "low disk" };
51
+ if (p === "waiting: bootstrap") return { key: "bootstrap", label: "Waiting", tone: "amber", live: false, detail: "bootstrap" };
52
+ if (p.startsWith("waiting: maintenance allowance used")) return { key: "quota", label: "Waiting", tone: "amber", live: false, detail: "maintenance " + after(35) };
36
53
  if (p.startsWith("sleeping")) return { key: "sleeping", label: "Sleeping", tone: "gray", live: false, detail: "" };
37
- if (p === "queued") return { key: "queued", label: "Queued", tone: "gray", live: false, detail: "due — not running: no work to claim, deferred, or starting on the next poll" };
38
- if (p === "waiting for prompts") return { key: "waiting", label: "Waiting", tone: "gray", live: false, detail: "runs when you send it a prompt" };
39
- if (p === "stopped") return { key: "stopped", label: "Stopped", tone: "gray", live: false, detail: "the fleet is not running" };
54
+ if (p.startsWith("idle: ")) return { key: "idle", label: "Idle", tone: "gray", live: false, detail: p.slice(6) };
55
+ if (p.startsWith("deferred (")) return { key: "deferred", label: "Deferred", tone: "gray", live: false, detail: p.slice(10, -1) };
56
+ if (p === "due") return { key: "queued", label: "Due", tone: "gray", live: false, detail: "starts on the next poll" };
57
+ if (p === "ready for messages") return { key: "waiting", label: "Ready", tone: "gray", live: false, detail: "runs when you send it a message" };
58
+ if (p === "stopped") return { key: "stopped", label: "Stopped", tone: "gray", live: false, detail: "the team is not running" };
40
59
  return { key: "other", label: p || "unknown", tone: "gray", live: false, detail: "" };
41
60
  }
42
61
  // A stalled tool call or a long silence, as the phase label names them (tick-progress-model.ts's
43
62
  // inFlightDetail) — the cue that a working loop may be stuck. The pattern is that module's
44
- // STALL_SOURCE (interpolated at module build), shared with fleet-alerts.ts's server-side
63
+ // STALL_SOURCE (interpolated at module build), shared with team-alerts.ts's server-side
45
64
  // matcher so the two cannot drift from the wording the cell renders.
46
65
  const STALL = new RegExp(${JSON.stringify(STALL_SOURCE)});
47
66
  // A tick result (tick-outcome.ts TickResult) in words, with the tone of its outcome.
48
- const RESULTS = {
49
- changed: ["Landed", "green"],
50
- queued: ["Queued to land", "indigo"],
51
- no_change: ["No change", "gray"],
52
- skipped: ["Skipped", "gray"],
53
- refused: ["Refused", "amber"],
54
- rejected: ["Rejected in review", "amber"],
55
- review_error: ["Review failed", "red"],
56
- merge_conflict: ["Merge conflict", "red"],
57
- merge_blocked: ["Merge blocked", "red"],
58
- error: ["Error", "red"],
59
- aborted: ["Interrupted", "amber"],
60
- quiet_killed: ["Stalled, resumed", "amber"],
61
- user_aborted: ["Aborted by you", "gray"],
62
- main_red: ["Main red", "red"],
67
+ const RESULT_TONES = {
68
+ changed: "green",
69
+ queued: "indigo",
70
+ no_change: "gray",
71
+ skipped: "gray",
72
+ refused: "amber",
73
+ rejected: "amber",
74
+ review_error: "red",
75
+ merge_conflict: "red",
76
+ merge_blocked: "red",
77
+ error: "red",
78
+ aborted: "amber",
79
+ quiet_killed: "amber",
80
+ user_aborted: "gray",
81
+ main_red: "red",
63
82
  };
64
83
  function resultInfo(result) {
65
- const r = RESULTS[result];
66
- return r ? { label: r[0], tone: r[1] } : { label: String(result || "—").replace(/_/g, " "), tone: "gray" };
84
+ const words = result ? eventTypeLabel(result) : "—";
85
+ return { label: words.charAt(0).toUpperCase() + words.slice(1), tone: hasOwn(RESULT_TONES, result) ? RESULT_TONES[result] : "gray" };
67
86
  }
68
87
  // The problem results and the named event sets are tone.ts's lists, interpolated at module
69
88
  // build (like STALL above) so the page cannot drift from the server's eventKind.
@@ -75,7 +94,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
75
94
  // loop-sort:start
76
95
  // Loop order — status-model.ts's loopRank/sortLoopsByState, the TUI's order too: the running
77
96
  // director first (its own live work, outside the maxConcurrent cap it bypasses — BUGS.md
78
- // 2026-10-06), then live work (working, reviewing, landing), then work waiting in the pipe
97
+ // 2026-10-06), then live work (working, reviewing, merge), then work waiting in the pipe
79
98
  // line (approved, awaiting a slot), then loops that need attention (failing, main red), then
80
99
  // paused, then idle; within a rank the most recent tick first, a never-ticked loop last, ties
81
100
  // by name. The phase → rank rules are status-model.ts's LOOP_RANK_RULES (interpolated at
@@ -95,19 +114,19 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
95
114
  const ra = loopRank(a.phase);
96
115
  const rb = loopRank(b.phase);
97
116
  if (ra !== rb) return ra - rb;
98
- const ta = a.lastTickEndedAt ?? 0;
99
- const tb = b.lastTickEndedAt ?? 0;
117
+ const ta = a.lastRunEndedAt ?? 0;
118
+ const tb = b.lastRunEndedAt ?? 0;
100
119
  if (ta !== tb) return tb - ta;
101
120
  return a.role.localeCompare(b.role);
102
121
  });
103
122
  }
104
123
  // loop-sort:end
105
- // The fleet table's section for each rank. The running director has its own section so it
124
+ // The team table's section for each rank. The running director has its own section so it
106
125
  // does not join the In-progress rows an operator scans against maxConcurrent; the permit
107
- // holders (rank 0) and the pipeline waiters (rank 1, labeled "Waiting for a slot") keep
126
+ // holders (rank 0) and the pipeline waiters (rank 1, labeled "Waiting to start") keep
108
127
  // sharing In progress (BUGS.md 2026-10-06).
109
128
  const LOOP_GROUPS = [["Director", -1], ["In progress", 0, 1], ["Needs attention", 2], ["Paused", 3], ["Idle", 4]];
110
- // The payload's alerts (fleet-alerts.ts fleetAlerts — the TUI's attention lines use the same
129
+ // The payload's alerts (team-alerts.ts teamAlerts — the TUI's attention lines use the same
111
130
  // list), plus the one only the page can know: that its server stopped answering.
112
131
  const OFFLINE_ALERT = { key: "offline", tone: "red", title: "Lost contact with the dashboard server",
113
132
  detail: "Retrying every second. Everything below is the last state it reported.", actions: [] };
@@ -125,7 +144,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
125
144
  const needsYouKeys = (alerts) => alerts.filter(NEEDS_YOU).map((a) => a.key);
126
145
  // The needs-you keys of the given alerts that prevKeys (the previous poll's set, or null on
127
146
  // the first poll) lacks — exactly the alerts a new cue should announce. A null prev yields
128
- // every needs-you key, so a page opened onto an already-alerting fleet cues once.
147
+ // every needs-you key, so a page opened onto an already-alerting team cues once.
129
148
  const newNeedsYouKeys = (prevKeys, alerts) => {
130
149
  const prior = new Set(prevKeys || []);
131
150
  return needsYouKeys(alerts).filter((k) => !prior.has(k));
@@ -135,9 +154,10 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
135
154
  const ROUTINE_EVENTS = ${JSON.stringify(ROUTINE_EVENTS)};
136
155
  const PROBLEM_EVENTS = ${JSON.stringify(PROBLEM_EVENTS)};
137
156
  function eventKind(item) {
138
- if (item.type === "merged") return "landing";
157
+ if (item.type === "merged") return "landing"; // vocabulary-ok: internal key the row classes and filters match
139
158
  if (item.type === "question_posted") return "attention";
140
- if (item.type === "tick_end") return PROBLEM_RESULTS.includes(item.result) ? "problem" : "routine";
159
+ if (item.type === "entry_refused") return "attention";
160
+ if (item.type === "run_end") return PROBLEM_RESULTS.includes(item.result) ? "problem" : "routine";
141
161
  if (item.type === "build_check") return ROUTINE_BUILD_RESULTS.includes(item.result) ? "routine" : "problem";
142
162
  if (PROBLEM_EVENTS.includes(item.type)) return "problem";
143
163
  if (ROUTINE_EVENTS.includes(item.type)) return "routine";
@@ -145,7 +165,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
145
165
  }
146
166
  // A backlog title's trailing "(planned 2026-09-29)"-style note, split off as metadata —
147
167
  // the pattern is backlog-structure.ts's ENTRY_STAMP_META_SOURCE (interpolated at module
148
- // load, compiled once here), the same regex fleet-alerts.ts strips through via
168
+ // load, compiled once here), the same regex team-alerts.ts strips through via
149
169
  // stripEntryStamp, so a new stamp verb reaches both dashboards from one edit.
150
170
  const ENTRY_STAMP_RE = new RegExp(${JSON.stringify(ENTRY_STAMP_META_SOURCE)}, "i");
151
171
  function splitTitle(t) {
@@ -1,11 +1,13 @@
1
- import { PAUSE_REASON_MAX } from "../../fleet/fleet-state.js";
2
- /** The dashboard's fleet-wide operator controls, browser-side, all living in the sidebar so
1
+ // vocabulary: plain
2
+ import { PAUSE_REASON_MAX } from "../../team/team-state.js";
3
+ /** The dashboard's team-wide operator controls, browser-side, all living in the sidebar so
3
4
  * they are in reach from every view: the daily spend badge — today's spend against the cap
4
5
  * with a meter, a button that opens a small editor for the cap (POST /api/budget, the same
5
6
  * setter as the TUI's Ctrl+B) — the pause control, a menu of timed pauses with an
6
- * optional reason (POST /api/pause with `forSeconds` and `reason`, the CLI's `pause --for`/`--reason`) that turns into a Resume button with
7
- * the countdown while the fleet is paused, and the sound toggle that mutes the alert cue (a
8
- * per-browser preference, no server call). Neither keeps optimistic state: after a successful
7
+ * optional reason (POST /api/pause with `forSeconds` and `reason`, the CLI's
8
+ * `pause --for`/`--reason`) that turns into a Resume button with the countdown while the
9
+ * team is paused, and the sound toggle that mutes the alert cue (a per-browser preference,
10
+ * no server call). Neither keeps optimistic state: after a successful
9
11
  * POST the next status poll re-renders them from the server's answer, and a failed one toasts
10
12
  * the error and leaves the editor open. Spliced into gui-client.ts's script, where it reaches
11
13
  * the other sections' shared plumbing through that concatenation: the DOM and HTML helpers
@@ -55,9 +57,17 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
55
57
  const pct = (b.spentUsd / b.capUsd) * 100;
56
58
  return pct >= 100 ? " t-red" : pct >= 85 ? " t-amber" : "";
57
59
  }
60
+ // The bar under a spend/window readout: a span class='meter{tone}' wrapping a fill whose
61
+ // width is the spent share. The one home of the markup both the claude-window line
62
+ // (windowLinesHtml) and the budget card (budgetCardHtml) render, so the class/nesting
63
+ // cannot drift between them; each call site shapes its own percent (an integer round for
64
+ // the window, one decimal for the card).
65
+ function meterBar(tone, pct) {
66
+ return "<span class='meter" + tone + "'><span style='width:" + pct + "%'></span></span>";
67
+ }
58
68
  // The subscription usage window (plans/agent-backends.md §8, PLANS.md part 6d/7): the
59
69
  // sidebar's spend card renders it beside the daily spend, or in its place for a
60
- // subscription-only fleet. The label/status/clock rules mirror badges.ts's
70
+ // subscription-only team. The label/status/clock rules mirror badges.ts's
61
71
  // claudeWindowBadge — the preformatted header fragment the TUI uses cannot be reused here
62
72
  // because the card lists EVERY window, not just the most constrained one.
63
73
  function windowLabel(type) {
@@ -76,13 +86,33 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
76
86
  const d = new Date(ms);
77
87
  return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
78
88
  }
79
- function windowLine(w, cw) {
89
+ function windowResets(ms) {
90
+ const minutes = Math.floor((ms - Date.now()) / 60000);
91
+ if (minutes < 1) return "resets now";
92
+ if (minutes < 60) return "resets in " + minutes + "m";
93
+ const hours = Math.floor(minutes / 60);
94
+ if (hours < 24) return "resets in " + hours + "h " + (minutes % 60) + "m";
95
+ return "resets in " + Math.floor(hours / 24) + "d " + (hours % 24) + "h";
96
+ }
97
+ function chainMoved(chain) {
98
+ return Object.values(chain || {}).some((t) => t.reason === "window" || t.reason === "reserve");
99
+ }
100
+ // One line per tier off its primary, the same text as badges.ts's chainFragments.
101
+ function chainLinesHtml(chain) {
102
+ return ["small", "default", "strong"].filter((tier) => chain && chain[tier]).map((tier) => {
103
+ const t = chain[tier];
104
+ const money = t.spentUsd !== undefined && t.capUsd !== undefined ? " " + fmtUsd(t.spentUsd) + "/" + fmtCap(t.capUsd) : "";
105
+ const until = t.returnsAt !== undefined ? " until " + windowClock(t.returnsAt) : "";
106
+ return "<span class='side-card-value window-line'>" + esc(tier + " → " + t.pair + money + until) + "</span>";
107
+ }).join("");
108
+ }
109
+ function windowLine(w, cw, chain) {
80
110
  const parts = ["Claude " + windowLabel(w.type)];
81
111
  const word = windowWord(w.status);
82
112
  parts.push(typeof w.utilization === "number" ? Math.round(w.utilization * 100) + "%" : word === "ok" ? "ok · usage not reported" : word);
83
113
  if (w.isUsingOverage) parts.push("overage");
84
- if (w.status === "rejected" && cw.heldUntil != null) parts.push("held until " + windowClock(cw.heldUntil));
85
- else if (w.status !== "reset" && typeof w.resetsAt === "number") parts.push("resets " + windowClock(w.resetsAt));
114
+ if (w.status === "rejected" && cw.heldUntil != null) parts.push((chainMoved(chain) ? "on fallback until " : "held until ") + windowClock(cw.heldUntil));
115
+ else if (w.status !== "reset" && typeof w.resetsAt === "number") parts.push(windowResets(w.resetsAt));
86
116
  return parts.join(" · ");
87
117
  }
88
118
  function windowLineTone(w) {
@@ -90,28 +120,35 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
90
120
  if (w.status === "allowed_warning") return " t-amber";
91
121
  return "";
92
122
  }
93
- function windowLinesHtml(cw) {
94
- if (!cw) return "";
95
- return cw.windows.map((w) => {
96
- const line = "<span class='side-card-value window-line" + windowLineTone(w) + "'>" + esc(windowLine(w, cw)) + "</span>";
123
+ function windowLinesHtml(cw, chain) {
124
+ if (!cw) return chainLinesHtml(chain);
125
+ // Shortest window first (5h above 7d), whatever order the server ranks severity in; an
126
+ // unlabelled type sorts last, then by type name. A copy, so cw.windows keeps its order.
127
+ const hours = (w) => {
128
+ const m = /^(\d+)([hdw])$/.exec(windowLabel(w.type));
129
+ return m ? Number(m[1]) * (m[2] === "h" ? 1 : m[2] === "d" ? 24 : 168) : Infinity;
130
+ };
131
+ const shortestFirst = cw.windows.slice().sort((a, b) => (hours(a) === hours(b) ? (a.type < b.type ? -1 : a.type > b.type ? 1 : 0) : hours(a) < hours(b) ? -1 : 1));
132
+ return shortestFirst.map((w) => {
133
+ const line = "<span class='side-card-value window-line" + windowLineTone(w) + "'>" + esc(windowLine(w, cw, chain)) + "</span>";
97
134
  if (typeof w.utilization !== "number") return line;
98
135
  const pct = Math.min(100, Math.round(w.utilization * 100));
99
- return line + "<span class='meter" + windowLineTone(w) + "'><span style='width:" + pct + "%'></span></span>";
100
- }).join("");
136
+ return line + meterBar(windowLineTone(w), pct);
137
+ }).join("") + chainLinesHtml(chain);
101
138
  }
102
- // The sidebar's spend card for a payload's budget block: a plain card for an all-free fleet
139
+ // The sidebar's spend card for a payload's budget block: a plain card for an all-free team
103
140
  // (no spend a cap could bind, so no editor), otherwise the button that opens the cap editor,
104
141
  // with a meter while a cap is set.
105
- function budgetCardHtml(b, cw) {
106
- // A subscription-only fleet has no priced spend for a cap to bind, so the window card
142
+ function budgetCardHtml(b, cw, chain) {
143
+ // A subscription-only team has no priced spend for a cap to bind, so the window card
107
144
  // stands in place of the spend card and carries no editor.
108
145
  if (cw && cw.only) {
109
- return "<div class='side-card' title='Claude.ai subscription usage windows — a rejected window holds the claude seams until it resets'>" +
110
- "<span class='side-card-label'>" + icon("clock") + "Claude window</span>" + windowLinesHtml(cw) + "</div>";
146
+ return "<div class='side-card' title='Claude.ai subscription usage windows — a rejected window pauses claude model jobs until it resets'>" +
147
+ "<span class='side-card-label'>" + icon("clock") + "Claude window</span>" + windowLinesHtml(cw, chain) + "</div>";
111
148
  }
112
149
  if (b.free) {
113
- return "<div class='side-card' title='Every model the fleet can use is cost n/a, so there is nothing to cap'>" +
114
- "<span class='side-card-label'>" + icon("dollar") + "Spend</span><span class='side-card-value'>Free models</span>" + windowLinesHtml(cw) + "</div>";
150
+ return "<div class='side-card' title='Every model the team can use is cost n/a, so there is nothing to cap'>" +
151
+ "<span class='side-card-label'>" + icon("dollar") + "Spend</span><span class='side-card-value'>Free models</span>" + windowLinesHtml(cw, chain) + "</div>";
115
152
  }
116
153
  const capped = b.capUsd > 0;
117
154
  const pct = capped ? Math.min(100, (b.spentUsd / b.capUsd) * 100) : 0;
@@ -120,12 +157,12 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
120
157
  esc(capped ? "Today's spend against the daily cap — click to change the cap" : "No daily cap — click to set one") + "'>" +
121
158
  "<span class='side-card-label'>" + icon("dollar") + "Spent today<span class='edit'>" + (capped ? "Edit cap" : "Set a cap") + "</span></span>" +
122
159
  "<span class='side-card-value'>" + esc(fmtUsd(b.spentUsd)) + " <small>" + esc(capped ? "of " + fmtCap(b.capUsd) : "no daily cap") + "</small></span>" +
123
- (capped ? "<span class='meter" + tone + "'><span style='width:" + pct.toFixed(1) + "%'></span></span>" : "") + windowLinesHtml(cw) + "</button>";
160
+ (capped ? meterBar(tone, pct.toFixed(1)) : "") + windowLinesHtml(cw, chain) + "</button>";
124
161
  }
125
162
  function renderBudgetBadge(d) {
126
163
  if (budgetEditing) return; // the open editor owns the slot until save or cancel
127
164
  const b = d && d.budget;
128
- paintPanel("budgetwrap", b ? budgetCardHtml(b, d && d.claudeWindow) : "");
165
+ paintPanel("budgetwrap", b ? budgetCardHtml(b, d && d.claudeWindow, d && d.chain) : "");
129
166
  }
130
167
  function openBudgetEditor() {
131
168
  const b = lastStatus && lastStatus.budget;
@@ -137,7 +174,7 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
137
174
  html: "<label for='budgetinput'>Daily spend cap</label>" +
138
175
  "<div class='input-prefix'><span>$</span><input type='number' min='0' step='0.01' id='budgetinput' value='" +
139
176
  esc(b.capUsd > 0 ? String(b.capUsd) : "") + "' placeholder='no cap'></div>" +
140
- "<p class='hint'>" + esc("Spent today: " + fmtUsd(b.spentUsd) + ". At the cap the loops " +
177
+ "<p class='hint'>" + esc("Spent today: " + fmtUsd(b.spentUsd) + ". At the cap the roles " +
141
178
  (b.fallback ? "switch to the free fallback model" : "pause") + " until midnight. Leave it empty for no cap.") + "</p>" +
142
179
  "<div class='popover-actions'><button type='button' class='btn btn-sm' id='budgetcancel'>Cancel</button>" +
143
180
  "<button type='button' class='btn btn-sm btn-primary' id='budgetset'>Save</button></div>",
@@ -211,20 +248,20 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
211
248
  let pauseMenuOpen = false;
212
249
  // The timed pauses the menu offers; 0 is a standing pause that lasts until Resume.
213
250
  const PAUSE_CHOICES = [[1800, "For 30 minutes"], [3600, "For 1 hour"], [7200, "For 2 hours"], [28800, "For 8 hours"], [0, "Until I resume it"]];
214
- // How long a timed fleet pause has left (humanSeconds), or "" for a standing one.
251
+ // How long a timed team pause has left (humanSeconds), or "" for a standing one.
215
252
  function pauseLeft(d) {
216
253
  return d.pausedUntil ? humanSeconds(Math.max(0, Math.round((d.pausedUntil - Date.now()) / 1000))) : "";
217
254
  }
218
- // The sidebar's pause control: Resume (with a timed pause's countdown) while the fleet is
255
+ // The sidebar's pause control: Resume (with a timed pause's countdown) while the team is
219
256
  // paused, otherwise the button that opens the menu of timed pauses.
220
257
  function pauseControlHtml(d) {
221
258
  if (d.paused) {
222
259
  const left = pauseLeft(d);
223
- return "<button type='button' class='btn btn-warn' id='pausebadge' title='" + esc("The fleet is paused — click to resume it" + (d.pauseReason ? " (" + d.pauseReason + ")" : "")) + "'>" +
224
- icon("play") + "<span>Resume the fleet</span><span class='btn-note' id='pausenote'>" + (left ? esc(left) + " left" : "") + "</span></button>";
260
+ return "<button type='button' class='btn btn-warn' id='pausebadge' title='" + esc("The team is paused — click to resume it" + (d.pauseReason ? " (" + d.pauseReason + ")" : "")) + "'>" +
261
+ icon("play") + "<span>Resume the team</span><span class='btn-note' id='pausenote'>" + (left ? esc(left) + " left" : "") + "</span></button>";
225
262
  }
226
- return "<button type='button' class='btn' id='pausebadge' aria-haspopup='menu' title='Pause the fleet — loops start no new ticks'>" +
227
- icon("pause") + "<span>Pause the fleet</span><span class='btn-note'>" + icon("chev") + "</span></button>";
263
+ return "<button type='button' class='btn' id='pausebadge' aria-haspopup='menu' title='Pause the team — roles start no new runs'>" +
264
+ icon("pause") + "<span>Pause the team</span><span class='btn-note'>" + icon("chev") + "</span></button>";
228
265
  }
229
266
  function renderPauseBadge(d) {
230
267
  if (pauseMenuOpen) return;
@@ -241,11 +278,11 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
241
278
  const pop = openPopover("pausewrap", {
242
279
  menu: true,
243
280
  role: "menu",
244
- html: "<div class='menu-title'>Pause the fleet</div>" +
281
+ html: "<div class='menu-title'>Pause the team</div>" +
245
282
  "<input type='text' id='pausereason' class='menu-input' maxlength=${PAUSE_REASON_MAX} placeholder='Reason (optional)' aria-label='Pause reason' style='margin:2px 10px 4px;width:calc(100% - 20px)'>" +
246
283
  PAUSE_CHOICES.map((c) => "<button type='button' class='menu-item' role='menuitem' data-pause='" + c[0] + "'>" +
247
284
  icon(c[0] ? "clock" : "pause") + esc(c[1]) + "</button>").join("") +
248
- "<p class='hint' style='margin:4px 10px 4px'>In-flight ticks finish, and the director keeps running your prompts.</p>",
285
+ "<p class='hint' style='margin:4px 10px 4px'>Current runs finish, and the director keeps running your messages.</p>",
249
286
  onClosed: () => {
250
287
  pauseMenuOpen = false;
251
288
  renderPauseBadge(lastStatus);
@@ -255,20 +292,20 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
255
292
  const first = pop.querySelector("button");
256
293
  if (first) first.focus({ preventScroll: true });
257
294
  }
258
- async function setFleetPause(paused, forSeconds, reason) {
295
+ async function setTeamPause(paused, forSeconds, reason) {
259
296
  const body = { paused: paused };
260
297
  if (paused && forSeconds > 0) body.forSeconds = forSeconds;
261
298
  const why = paused && reason ? String(reason).trim() : "";
262
299
  if (why) body.reason = why;
263
300
  // A failure changes nothing; the control still shows the real state.
264
301
  await postAction("/api/pause", body, paused
265
- ? "Fleet paused" + (forSeconds > 0 ? " for " + humanSeconds(forSeconds) : "") + " — in-flight ticks finish"
266
- : "Fleet resumed");
302
+ ? "Team paused" + (forSeconds > 0 ? " for " + humanSeconds(forSeconds) : "") + " — current runs finish"
303
+ : "Team resumed");
267
304
  }
268
305
  onClick((t, ev) => {
269
306
  if (t.closest("#pausebadge")) {
270
307
  ev.preventDefault();
271
- if (lastStatus && lastStatus.paused) setFleetPause(false);
308
+ if (lastStatus && lastStatus.paused) setTeamPause(false);
272
309
  else if (pauseMenuOpen) closeMenus();
273
310
  else openPauseMenu();
274
311
  return;
@@ -276,14 +313,67 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
276
313
  const choice = t.closest("[data-pause]");
277
314
  if (choice) {
278
315
  const input = $("pausereason");
279
- setFleetPause(true, Number(choice.dataset.pause), input ? input.value : "");
316
+ setTeamPause(true, Number(choice.dataset.pause), input ? input.value : "");
280
317
  }
281
318
  });
282
319
  // pause-control:end
283
320
 
321
+ // stop-control:start
322
+ let stopMenuOpen = false;
323
+ // The delays the menu offers for a scheduled graceful stop.
324
+ const STOP_CHOICES = [[3600, "In 1 hour"], [7200, "In 2 hours"], [14400, "In 4 hours"], [28800, "In 8 hours"]];
325
+ // The sidebar's scheduled-stop control: nothing while the team is stopped; Cancel while a
326
+ // stop is scheduled; otherwise the button that opens the menu of delays. No immediate stop:
327
+ // under run --gui it would take the page down mid-click.
328
+ function stopControlHtml(d) {
329
+ if (!d || !d.running) return "";
330
+ if (d.scheduledStopAt) {
331
+ return "<button type='button' class='btn btn-warn' id='stopbadge' title='A stop is scheduled — click to cancel it'>" +
332
+ icon("stop") + "<span>Cancel scheduled stop</span></button>";
333
+ }
334
+ return "<button type='button' class='btn' id='stopbadge' aria-haspopup='menu' title='Stop the team later — current runs finish, then it stops'>" +
335
+ icon("stop") + "<span>Stop the team later</span><span class='btn-note'>" + icon("chev") + "</span></button>";
336
+ }
337
+ function renderStopControl(d) {
338
+ if (stopMenuOpen) return;
339
+ paintPanel("stopwrap", stopControlHtml(d));
340
+ }
341
+ function openStopMenu() {
342
+ const pop = openPopover("stopwrap", {
343
+ menu: true,
344
+ role: "menu",
345
+ html: "<div class='menu-title'>Stop the team later</div>" +
346
+ STOP_CHOICES.map((c) => "<button type='button' class='menu-item' role='menuitem' data-stopin='" + c[0] + "'>" +
347
+ icon("clock") + esc(c[1]) + "</button>").join("") +
348
+ "<p class='hint' style='margin:4px 10px 4px'>Current runs finish, then the team stops.</p>",
349
+ onClosed: () => {
350
+ stopMenuOpen = false;
351
+ renderStopControl(lastStatus);
352
+ },
353
+ });
354
+ stopMenuOpen = true;
355
+ const first = pop.querySelector("button");
356
+ if (first) first.focus({ preventScroll: true });
357
+ }
358
+ function postStopSchedule(body) {
359
+ return postAction("/api/stop-schedule", body, (d) => d && typeof d.message === "string" ? d.message : "Stop schedule updated");
360
+ }
361
+ onClick((t, ev) => {
362
+ if (t.closest("#stopbadge")) {
363
+ ev.preventDefault();
364
+ if (lastStatus && lastStatus.scheduledStopAt) postStopSchedule({ cancel: true });
365
+ else if (stopMenuOpen) closeMenus();
366
+ else openStopMenu();
367
+ return;
368
+ }
369
+ const choice = t.closest("[data-stopin]");
370
+ if (choice) postStopSchedule({ inSeconds: Number(choice.dataset.stopin) });
371
+ });
372
+ // stop-control:end
373
+
284
374
  // sound-control:start
285
375
  // The sidebar's sound control, beside the theme toggle: a speaker icon that flips between
286
- // the unmuted and muted glyph. A per-browser preference (setSoundMuted stores it), not fleet
376
+ // the unmuted and muted glyph. A per-browser preference (setSoundMuted stores it), not team
287
377
  // state — no server call; the choice takes effect on the next cue.
288
378
  function soundControlHtml() {
289
379
  return "<button type='button' class='icon-btn' id='soundtoggle' title='" +
@@ -1,11 +1,12 @@
1
- /** The dashboard's Pending view, browser-side: every loop's unlanded change as /api/diff
1
+ // vocabulary: plain
2
+ /** The dashboard's Pending view, browser-side: every loop's unmerged change as /api/diff
2
3
  * serves it (the same document `tumwater diff` prints from the shared change collectors). The
3
4
  * roster shows each role's branch, state, commits ahead of main with their subjects, and its
4
5
  * uncommitted-file count; a row opens that loop's drawer, whose Pending change section fetches
5
6
  * the full patch (`/api/diff?role=<id>`). It refetches when the view opens and when its
6
7
  * Refresh button is pressed — never on a timer of its own. Spliced into gui-client.ts's
7
8
  * script, reaching its helpers (esc, getJson, icon, errorPanel) through that concatenation. */
8
- export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
9
+ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
9
10
  // pending-table:start
10
11
  // One role's state cell: the collector's three states, with "ready" split into pending vs
11
12
  // idle work. Never colors a degraded state as an active one.
@@ -13,10 +14,10 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
13
14
  if (r.state === "absent") return "<span class='muted'>no worktree</span>";
14
15
  if (r.state === "no-base") return "<span class='muted'>no base</span>";
15
16
  if (r.state !== "ready") return "<span class='muted'>unknown</span>";
16
- if (r.ahead === 0 && (!r.dirtyFiles || r.dirtyFiles.length === 0)) return "<span class='muted'>no unlanded work</span>";
17
+ if (r.ahead === 0 && (!r.dirtyFiles || r.dirtyFiles.length === 0)) return "<span class='muted'>no unmerged work</span>";
17
18
  return "<span class='res t-indigo'>pending</span>";
18
19
  }
19
- // One role's ahead cell: the commit count plus each unlanded commit's sha and subject, or a
20
+ // One role's ahead cell: the commit count plus each unmerged commit's sha and subject, or a
20
21
  // dash for a degraded/empty role.
21
22
  function pendingAheadHtml(r) {
22
23
  if (r.state !== "ready" || !r.ahead) return "<span class='muted'>—</span>";
@@ -30,69 +31,72 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
30
31
  }
31
32
  // The Pending roster: one clickable row per known role (the collector's config order), each
32
33
  // opening that loop's drawer. Pure — the view paints what this returns.
33
- function pendingTableHtml(fleet) {
34
- if (!fleet || !Array.isArray(fleet.roles) || fleet.roles.length === 0)
35
- return "<div class='empty'><strong>No loops yet</strong>Loops appear here once the fleet is configured.</div>";
36
- if (fleet.roles.every((r) => r.state === "no-base"))
37
- return "<div class='empty'><strong>Main branch " + esc(fleet.mainBranch) + " does not exist</strong>There is no base to compare unlanded work against.</div>";
38
- const rows = fleet.roles.map((r) =>
34
+ function pendingTableHtml(team) {
35
+ if (!team || !Array.isArray(team.roles) || team.roles.length === 0)
36
+ return "<div class='empty'><strong>No roles yet</strong>Roles appear here once the team is configured.</div>";
37
+ if (team.roles.every((r) => r.state === "no-base"))
38
+ return "<div class='empty'><strong>Main branch " + esc(team.mainBranch) + " does not exist</strong>There is no base to compare unmerged work against.</div>";
39
+ const rows = team.roles.map((r) =>
39
40
  "<tr class='clickable' data-open='" + esc(r.role) + "'>" +
40
- "<td class='mono'>" + esc(r.role) + "</td>" +
41
+ "<td class='mono'>" + esc(roleLabel(r.role)) + "</td>" +
41
42
  "<td class='mono muted'>" + esc(r.branch) + "</td>" +
42
43
  "<td>" + pendingStateHtml(r) + "</td>" +
43
44
  "<td>" + pendingAheadHtml(r) + "</td>" +
44
45
  "<td class='num'>" + pendingDirtyHtml(r) + "</td></tr>",
45
46
  ).join("");
46
- return "<div class='table-wrap'><table class='table pending'><thead><tr><th class='c-loop'>Loop</th><th>Branch</th>" +
47
- "<th>State</th><th>Ahead of " + esc(fleet.mainBranch) + "</th><th class='c-dirty num'>Uncommitted</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
47
+ return "<div class='table-wrap'><table class='table pending'><thead><tr><th class='c-loop'>Role</th><th>Branch</th>" + // vocabulary-ok: CSS class the stylesheet matches
48
+ "<th>State</th><th>Ahead of " + esc(team.mainBranch) + "</th><th class='c-dirty num'>Uncommitted</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
48
49
  }
49
- // The Land queue card's body: every queued landing in execution order (oldest first), one
50
- // line each mirroring tumwater queue's renderLandingQueue — 1-based position, loop, tick,
50
+ // The Merge queue card's body: every queued merge in execution order (oldest first), one
51
+ // line each mirroring tumwater queue's renderMergeQueue — 1-based position, loop, tick,
51
52
  // short sha, age, summary — so the page and the CLI cannot drift. Painted from the status
52
53
  // payload the page already polls; depth counts the unreadable queue files the status reader
53
54
  // skips, so a positive depth with no readable list states the count instead of inventing rows.
54
55
  // Pure: the age comes from the passed now, and a malformed entry renders a stand-in rather
55
56
  // than undefined/NaN. Null/absent/zero-depth reads as empty.
56
- function pendingQueueHtml(landQueue, now) {
57
- const depth = landQueue && Number.isFinite(landQueue.depth) ? landQueue.depth : 0;
58
- const entries = landQueue && Array.isArray(landQueue.entries) ? landQueue.entries : [];
57
+ function pendingQueueHtml(mergeQueue, now, labels) {
58
+ const depth = mergeQueue && Number.isFinite(mergeQueue.depth) ? mergeQueue.depth : 0;
59
+ const entries = mergeQueue && Array.isArray(mergeQueue.entries) ? mergeQueue.entries : [];
59
60
  let body;
60
61
  if (depth === 0) {
61
- body = "<div class='empty'>Land queue empty</div>";
62
+ body = "<div class='empty'>Merge queue empty</div>";
62
63
  } else if (entries.length === 0) {
63
- body = "<div class='empty'>land queue: " + esc(depth) + " waiting</div>";
64
+ body = "<div class='empty'>merge queue: " + esc(depth) + " waiting</div>";
64
65
  } else {
65
66
  body = "<div style='padding:10px 16px 12px'>" + entries.map((e, i) => {
66
67
  const sec = Number.isFinite(e.enqueuedAt) ? Math.max(0, Math.round((now - e.enqueuedAt) / 1000)) : 0;
67
- const tick = Number.isFinite(e.tick) ? e.tick : "?";
68
- return "<div class='mono clamp1' style='padding:3px 0'>" + (i + 1) + ". " + esc(e.role || "?") +
69
- " tick #" + esc(tick) + " · " + esc(shortSha(e.sha)) + " · " + esc(humanSeconds(sec)) +
70
- " ago · " + esc(e.summary || "—") + "</div>";
68
+ const run = Number.isFinite(e.run) ? e.run : "?";
69
+ const note = Number.isFinite(e.revisionRound) ? " · revision " + e.revisionRound + (Number.isFinite(e.priorObjections) && e.priorObjections > 0 ? " · " + plural(e.priorObjections, "prior objection") : "") : "";
70
+ return "<div class='mono clamp1' style='padding:3px 0'>" + (i + 1) + ". " + esc(e.role ? roleLabel(e.role) : "?") +
71
+ " run #" + esc(run) + " · " + esc(shortSha(e.sha)) + " · " + esc(humanSeconds(sec)) +
72
+ " ago" + (Array.isArray(labels) && labels[i] && typeof labels[i] === "string" ? " · [" + esc(labels[i]) + "]" : "") +
73
+ note +
74
+ " · " + esc(e.summary || "—") + "</div>";
71
75
  }).join("") + "</div>";
72
76
  }
73
- return "<div class='card-head'><h2>Land queue</h2></div>" + body;
77
+ return "<div class='card-head'><h2>Merge queue</h2></div>" + body;
74
78
  }
75
79
  // pending-table:end
76
- // Paint the Land queue card from the polled status payload, at paint-time age; no fetch of
80
+ // Paint the Merge queue card from the polled status payload, at paint-time age; no fetch of
77
81
  // its own (the entries ride /api/status, unlike the roster's /api/diff).
78
82
  function renderPendingQueue(status) {
79
83
  const card = $("pendingqueue");
80
84
  if (!card) return;
81
- card.innerHTML = pendingQueueHtml(status && status.landQueue, Date.now());
85
+ card.innerHTML = pendingQueueHtml(status && status.mergeQueue, Date.now(), status && status.mergeQueueLabels);
82
86
  }
83
87
  function buildPendingView() {
84
- if (!buildOnce("pending", "<div class='view-head'><div><h1>Pending</h1><p>Every loop's unlanded change — its branch, commits ahead of main, and uncommitted files. Open a row for the full diff.</p></div>" +
88
+ if (!buildOnce("pending", "<div class='view-head'><div><h1>Pending</h1><p>Every role's unmerged change — its branch, commits ahead of main, and uncommitted files. Open a row for the full diff.</p></div>" +
85
89
  "<div class='toolbar'><button type='button' class='btn btn-sm' id='pendingrefresh'>" + icon("refresh") + "Refresh</button></div></div>" +
86
90
  "<div class='card' id='pendingqueue'><div class='empty'>Loading…</div></div>" +
87
91
  "<div class='card' id='pendingbody'><div class='empty'>Loading…</div></div>")) return;
88
92
  $("pendingrefresh").addEventListener("click", () => fetchPending());
89
93
  }
90
- // Load the fleet-wide change roster. Called when the view opens and by the Refresh button;
91
- // no timer of its own, and the roster only changes when a loop lands or edits its worktree.
94
+ // Load the team-wide change roster. Called when the view opens and by the Refresh button;
95
+ // no timer of its own, and the roster only changes when a loop merges or edits its worktree.
92
96
  async function fetchPending() {
93
97
  buildPendingView();
94
98
  try {
95
- pendingFleet = await getJson("/api/diff");
99
+ pendingTeam = await getJson("/api/diff");
96
100
  } catch (err) {
97
101
  const body = $("pendingbody");
98
102
  if (body) body.innerHTML = errorPanel("Pending unavailable", err);
@@ -104,6 +108,6 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
104
108
  function renderPending() {
105
109
  const body = $("pendingbody");
106
110
  if (!body) return;
107
- body.innerHTML = pendingTableHtml(pendingFleet);
111
+ body.innerHTML = pendingTableHtml(pendingTeam);
108
112
  }
109
113
  `;