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 History view, browser-side: every completed tick, newest first, as
2
3
  * /api/history serves them (the rows `tumwater history` prints). A toolbar narrows it to one
3
4
  * loop (server-side, so a quiet loop still gets its full count), searches the rendered row text
@@ -16,16 +17,20 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
16
17
  let histSinceKey = recall("hist-since") || "";
17
18
  let histNote = "";
18
19
  let histFilter = "all";
20
+ let histMode = recall("hist-mode") === "summary" ? "summary" : "table";
19
21
  let histRows = null;
22
+ let histSummary = null;
20
23
  let histFetchedAt = 0;
21
- const HIST_FILTERS = [["all", "All"], ["changes", "Changes"], ["none", "No change"], ["problems", "Problems"]];
24
+ const HIST_FILTERS = [["all", "All"], ["changes", "Changes"], ["none", "No change"], ["refused", "Refused"], ["problems", "Problems"]];
25
+ const HIST_MODES = [["table", "Table"], ["summary", "Summary"]];
22
26
  function buildHistoryView() {
23
- if (!buildOnce("history", "<div class='view-head'><div><h1>History</h1><p>Every completed tick, newest first.</p></div>" +
27
+ if (!buildOnce("history", "<div class='view-head'><div><h1>History</h1><p>Every completed run, newest first.</p></div>" +
24
28
  "<div class='toolbar'>" +
25
- "<label><span class='sr-only'>Loop</span><select class='field' id='histrole'><option value=''>All loops</option></select></label>" +
29
+ "<label><span class='sr-only'>Role</span><select class='field' id='histrole'><option value=''>All roles</option></select></label>" +
26
30
  "<div class='seg' id='histfilter'>" + HIST_FILTERS.map((f) => "<button type='button' data-hist='" + f[0] + "'>" + f[1] + "</button>").join("") + "</div>" +
27
- "<label><span class='sr-only'>Search</span><input class='field' id='histgrep' type='search' placeholder='Search ticks…' autocomplete='off'></label>" +
28
- "<label><span class='sr-only'>Ticks to show</span><select class='field' id='histcount'>" +
31
+ "<div class='seg' id='histsum'>" + HIST_MODES.map((m) => "<button type='button' data-histsum='" + m[0] + "'>" + m[1] + "</button>").join("") + "</div>" +
32
+ "<label><span class='sr-only'>Search</span><input class='field' id='histgrep' type='search' placeholder='Search runs…' autocomplete='off'></label>" +
33
+ "<label><span class='sr-only'>Runs to show</span><select class='field' id='histcount'>" +
29
34
  HIST_COUNTS.map((n) => "<option value='" + n + "'>Last " + n + "</option>").join("") +
30
35
  histWindowOptions() + "</select></label>" +
31
36
  "<button type='button' class='btn btn-sm' id='histrefresh'>" + icon("refresh") + "Refresh</button>" +
@@ -60,6 +65,13 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
60
65
  histFilter = b.dataset.hist;
61
66
  renderHistory();
62
67
  });
68
+ $("histsum").addEventListener("click", (ev) => {
69
+ const b = ev.target.closest("button[data-histsum]");
70
+ if (!b) return;
71
+ histMode = b.dataset.histsum;
72
+ store("hist-mode", histMode);
73
+ renderHistory();
74
+ });
63
75
  $("histrefresh").addEventListener("click", () => fetchHistory());
64
76
  // The drill-down's details toggle: the button alone expands or collapses (the row's own
65
77
  // click still opens the drawer — gui-client-boot's data-open handler skips clicks on a
@@ -73,7 +85,7 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
73
85
  // history-detail:start
74
86
  // Tick drill-down state: one open card at a time, keyed "role#tick", with each key's cache
75
87
  // entry — { state: "loading" } while its fetch is in flight, { state: "error", message }
76
- // when one failed, { state: "ok", text } once /api/tick answered. Cached until the view
88
+ // when one failed, { state: "ok", text } once /api/run answered. Cached until the view
77
89
  // refetches (fetchHistory clears the map), so collapsing and re-expanding reuses the card.
78
90
  const histDetails = new Map();
79
91
  let histDetailOpen = "";
@@ -88,13 +100,14 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
88
100
  // the tumwater tick rendering).
89
101
  function histDetailCardHtml(entry) {
90
102
  const state = entry && entry.state;
91
- if (!state || state === "loading") return "<div class='empty'>Loading tick detail…</div>";
92
- if (state === "error") return "<div class='empty'><strong>" + esc(entry.message || "Tick detail unavailable") + "</strong></div>";
103
+ if (!state || state === "loading") return "<div class='empty'>Loading run detail…</div>";
104
+ if (state === "error") return "<div class='empty'><strong>" + esc(entry.message || "Run detail unavailable") + "</strong></div>";
93
105
  const lines = String(entry.text || "").split("\n");
94
106
  const head = lines.shift() || "";
95
107
  const trail = lines.join("\n");
96
108
  return "<div class='hist-detail'><div class='hist-detail-head mono'>" + esc(head) + "</div>" +
97
- (trail ? "<pre class='mono'>" + esc(trail) + "</pre>" : "<div class='empty'>No events in this tick's block.</div>") +
109
+ (trail ? "<pre class='mono'>" + esc(trail) + "</pre>" : "<div class='empty'>No events in this run's block.</div>") +
110
+ (entry.stat ? "<pre class='mono hist-detail-stat'>" + esc(entry.stat) + "</pre>" : "") +
98
111
  "</div>";
99
112
  }
100
113
  // Expand or collapse one row's card. Opening fetches the trail on first expand only — an
@@ -116,27 +129,27 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
116
129
  // (and a re-expand during flight sees it and lets the first fetch finish).
117
130
  histDetails.set(key, { state: "loading" });
118
131
  try {
119
- const d = await getJson("/api/tick?role=" + encodeURIComponent(role) + "&tick=" + encodeURIComponent(String(tick)));
132
+ const d = await getJson("/api/run?role=" + encodeURIComponent(role) + "&run=" + encodeURIComponent(String(tick)));
120
133
  // Name the endpoint, the tick it answered for, and the likely cause: a 200 without
121
134
  // text is a server too old (or too new) for this dashboard's client, not a lost tick —
122
135
  // a 404 already answers that shape with the CLI's own not-found wording.
123
136
  if (!d || typeof d.text !== "string")
124
137
  throw new Error(
125
- "/api/tick answered " + role + " tick " + tick + " without a text field — the running server's build may not match this dashboard",
138
+ "/api/run answered " + role + " run " + tick + " without a text field — the running tumwater may not match this dashboard",
126
139
  );
127
- histDetails.set(key, { state: "ok", text: d.text });
140
+ histDetails.set(key, { state: "ok", text: d.text, stat: typeof d.stat === "string" ? d.stat : "" });
128
141
  } catch (err) {
129
- histDetails.set(key, { state: "error", message: err && err.message ? err.message : "Tick detail unavailable" });
142
+ histDetails.set(key, { state: "error", message: err && err.message ? err.message : "Run detail unavailable" });
130
143
  }
131
144
  renderHistory();
132
145
  }
133
146
  // history-detail:end
134
- // The loop picker's options follow the fleet's role list (custom loops included), keyed so a
135
- // poll with no roster change repaints nothing. A role the fleet dropped falls back to All
147
+ // The loop picker's options follow the team's role list (custom loops included), keyed so a
148
+ // poll with no roster change repaints nothing. A role the team dropped falls back to All
136
149
  // loops and persists, exactly as syncRolePicker does for the report and Activity pickers.
137
150
  function syncHistoryRoles(d) {
138
151
  if (!d) return;
139
- histRole = syncRolePicker("histrole", (d.loops || []).map((l) => l.role), histRole, "hist-role");
152
+ histRole = syncRolePicker("histrole", (d.roles || []).map((l) => l.role), histRole, "hist-role");
140
153
  }
141
154
  async function fetchHistory(throttled) {
142
155
  if (throttled && Date.now() - histFetchedAt < 4000) return;
@@ -146,6 +159,10 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
146
159
  try {
147
160
  const d = await getJson(historyQuery(histCount, histRole, histSinceKey, histGrep));
148
161
  histRows = d && Array.isArray(d.rows) ? d.rows : [];
162
+ // The summary rides the same fetch (the endpoint folds the exact rows it served into
163
+ // its additive summary), so toggling to Summary never refetches; a server too old to
164
+ // include it leaves histSummary null and the toggle falls back to the table.
165
+ histSummary = d && d.summary && Array.isArray(d.summary.roles) ? d.summary : null;
149
166
  // The trailing-window shape carries the server's coverage; the count shape has no note.
150
167
  histNote = d && typeof d.note === "string" ? d.note : "";
151
168
  // A refetch invalidates the drill-down cache: cached trails could disagree with the new
@@ -166,10 +183,12 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
166
183
  // names the window in the empty state.
167
184
  const HIST_WINDOWS = { "1h": [3_600_000, "the last hour"], "6h": [21_600_000, "the last 6 hours"], "1d": [86_400_000, "the last day"], "7d": [604_800_000, "the last 7 days"] };
168
185
  // The /api/history URL for the current toolbar: the trailing-window shape when a window key
169
- // is set, the count shape otherwise, role and grep appended only when chosen. Pure.
186
+ // is set, the count shape otherwise, role and grep appended only when chosen. summary=1 rides
187
+ // every ask — the endpoint's additive summary folds the exact served rows, so the Summary
188
+ // mode renders from already-fetched data and the toggle swaps only the body.
170
189
  function historyQuery(count, role, sinceKey, grep) {
171
190
  const w = HIST_WINDOWS[sinceKey];
172
- return (w ? "/api/history?sinceMs=" + w[0] : "/api/history?n=" + count) + (role ? "&role=" + encodeURIComponent(role) : "") + (grep ? "&grep=" + encodeURIComponent(grep) : "");
191
+ return (w ? "/api/history?sinceMs=" + w[0] : "/api/history?n=" + count) + (role ? "&role=" + encodeURIComponent(role) : "") + (grep ? "&grep=" + encodeURIComponent(grep) : "") + "&summary=1";
173
192
  }
174
193
  // The window options, the phrase capitalized for the select ("The last day").
175
194
  function histWindowOptions() {
@@ -186,58 +205,97 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
186
205
  function historyClass(result) {
187
206
  if (result === "changed" || result === "queued") return "changes";
188
207
  if (result === "no_change" || result === "skipped" || result === "user_aborted") return "none";
208
+ if (result === "refused") return "refused";
189
209
  return "problems";
190
210
  }
191
211
  // The History table for rows (/api/history's, newest first) under a grep (already applied
192
212
  // server-side) and an outcome filter; known is
193
- // the set of the fleet's loop ids, whose rows open that loop's drawer. Pure — the view's
213
+ // the set of the team's loop ids, whose rows open that loop's drawer. Pure — the view's
194
214
  // render paints what this returns.
195
215
  function historyTableHtml(rows, filter, known, grep, sinceKey) {
196
216
  const shown = (rows || []).filter((r) => filter === "all" || historyClass(r.result) === filter);
197
217
  if (!shown.length) {
198
218
  const hasRows = Boolean(rows && rows.length);
199
219
  if (hasRows)
200
- return "<div class='empty'><strong>No ticks match this filter</strong>Try another outcome or loop.</div>";
220
+ return "<div class='empty'><strong>No runs match this filter</strong>Try another outcome or role.</div>";
201
221
  if (grep)
202
- return "<div class='empty'><strong>No ticks match this search</strong>Try another search.</div>";
222
+ return "<div class='empty'><strong>No runs match this search</strong>Try another search.</div>";
203
223
  const w = HIST_WINDOWS[sinceKey];
204
224
  return w
205
- ? "<div class='empty'><strong>No ticks in " + w[1] + "</strong>The window may be quiet, or its older events may have rotated out.</div>"
206
- : "<div class='empty'><strong>No ticks yet</strong>Completed ticks show up here as the loops work.</div>";
225
+ ? "<div class='empty'><strong>No runs in " + w[1] + "</strong>The window may have had nothing to do, or its older events may have rotated out.</div>"
226
+ : "<div class='empty'><strong>No runs yet</strong>Completed runs show up here as the roles work.</div>";
207
227
  }
208
- return "<div class='table-wrap'><table class='table history'><thead><tr><th class='c-x'></th><th>When</th><th>Loop</th><th class='c-tick num'>Tick</th>" +
209
- "<th>Result</th><th class='c-detail'>Detail</th><th class='num c-dur'>Took</th><th class='num c-usage'>Usage</th></tr></thead><tbody>" +
228
+ return "<div class='table-wrap'><table class='table history'><thead><tr><th class='c-x'></th><th>When</th><th>Role</th><th class='c-tick num'>Run</th>" + // vocabulary-ok: the c-tick class stays
229
+ "<th>Result</th><th class='c-model'>Model</th><th class='c-detail'>Detail</th><th class='num c-dur'>Took</th><th class='num c-usage'>Usage</th></tr></thead><tbody>" +
210
230
  shown.map((r) => {
211
231
  const res = resultInfo(r.result);
212
232
  // The drill-down toggle rides the drawer's button style and lives in its own leading
213
- // cell; only a fleet loop's row carries one, the same rows that open the drawer (the
214
- // /api/tick endpoint validates the role, so an id the config no longer knows would
233
+ // cell; only a team loop's row carries one, the same rows that open the drawer (the
234
+ // /api/run endpoint validates the role, so an id the config no longer knows would
215
235
  // only ever answer 400).
216
- const key = histDetailKey(r.loop, r.tick);
236
+ const key = histDetailKey(r.role, r.run);
217
237
  const open = histDetailOpen === key;
218
- const toggle = known.has(r.loop)
219
- ? "<button type='button' class='btn btn-sm hist-x' data-tickdetail='1' data-role='" + esc(r.loop) +
220
- "' data-tick='" + esc(r.tick) + "' aria-expanded='" + open + "' title='" + (open ? "Hide tick detail" : "Show tick detail") + "'>" + icon("chev") + "</button>"
238
+ const toggle = known.has(r.role)
239
+ ? "<button type='button' class='btn btn-sm hist-x' data-tickdetail='1' data-role='" + esc(r.role) +
240
+ "' data-tick='" + esc(r.run) + "' aria-expanded='" + open + "' title='" + (open ? "Hide run detail" : "Show run detail") + "'>" + icon("chev") + "</button>" // vocabulary-ok: the data-tick name stays
221
241
  : "";
222
- const row = "<tr" + (known.has(r.loop) ? " class='clickable' data-open='" + esc(r.loop) + "'" : "") + ">" +
242
+ const row = "<tr" + (known.has(r.role) ? " class='clickable' data-open='" + esc(r.role) + "'" : "") + ">" +
223
243
  "<td class='c-x'>" + toggle + "</td>" +
224
244
  "<td title='" + esc(r.time) + "'><span class='clamp1'>" + esc(fmtAgo(r.ts)) + "</span></td>" +
225
- "<td class='mono'>" + esc(r.loop) + "</td>" +
226
- "<td class='c-tick num muted'>#" + esc(r.tick) + "</td>" +
245
+ "<td class='mono'>" + esc(roleLabel(r.role)) + "</td>" +
246
+ "<td class='c-tick num muted'>#" + esc(r.run) + "</td>" + // vocabulary-ok: the c-tick class stays
227
247
  "<td><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span></td>" +
248
+ "<td class='c-model'><span class='clamp1' title='" + esc(r.model || "") + "'>" + (r.model ? esc(r.model) : "<span class='muted'>—</span>") + "</span></td>" +
228
249
  "<td class='c-detail'><span class='clamp2' title='" + esc(r.detail) + "'>" + (r.detail ? esc(r.detail) : "<span class='muted'>—</span>") + "</span></td>" +
229
- "<td class='num c-dur'>" + (r.durationMs === null || r.durationMs === undefined ? "—" : esc(fmtSpan(Number(r.durationMs)))) + "</td>" +
250
+ "<td class='num c-dur'>" + durCell(r.durationMs) + "</td>" +
230
251
  "<td class='num c-usage muted'>" + esc(r.usage || "—") + "</td></tr>";
231
252
  // The expanded card rides directly beneath its row, full width.
232
- return open ? row + "<tr class='detail-row'><td colspan='8'>" + histDetailCardHtml(histDetails.get(key)) + "</td></tr>" : row;
253
+ return open ? row + "<tr class='detail-row'><td colspan='9'>" + histDetailCardHtml(histDetails.get(key)) + "</td></tr>" : row;
233
254
  }).join("") + "</tbody></table></div>";
234
255
  }
235
256
  // history-table:end
257
+ // history-summary:start
258
+ // The Summary table: one row per loop plus the total, in the same column set the CLI's
259
+ // history --summary prints (ticks, changes, none, refused, problems, tokens, cost, time,
260
+ // last change), so the dashboard and the CLI cannot drift column for column. Every number
261
+ // comes from the
262
+ // server's summary (built on the shared summarizeTickRows) — nothing here re-derives a
263
+ // tally — and the cost/token cells reuse the page's report formatters. The time cell is the
264
+ // sum of the loop's run spans via fmtSpan, a dash when none of its rows carried a span. The
265
+ // last-change cell is the newest changes-class tick's age via fmtAgo, a dash when the loop had
266
+ // none. Pure — the view's render paints what this returns.
267
+ function histSummaryHtml(summary) {
268
+ const all = summary.roles.concat(summary.total ? [summary.total] : []);
269
+ const cell = (r, key, fmt) => "<td class='num'>" + esc(fmt ? fmt(r[key]) : r[key]) + "</td>";
270
+ const rowsHtml = all.map((r) =>
271
+ "<tr" + (r.role === "total" ? " class='hist-total'" : "") + ">" +
272
+ "<td class='mono'>" + esc(r.role === "total" ? r.role : roleLabel(r.role)) + "</td>" +
273
+ cell(r, "runs") + cell(r, "changes") + cell(r, "none") + cell(r, "refused") + cell(r, "problems") +
274
+ cell(r, "tokens", (n) => fmtTokens(Number(n))) + cell(r, "costUsd", (n) => fmtUsd(Number(n))) +
275
+ "<td class='num'>" + durCell(r.durationMs) + "</td>" +
276
+ "<td>" + esc(r.lastChangeTs === null || r.lastChangeTs === undefined ? "—" : fmtAgo(Number(r.lastChangeTs))) + "</td></tr>").join("");
277
+ return "<div class='table-wrap'><table class='table history'><thead><tr><th>Role</th><th class='num'>Runs</th><th class='num'>Merged</th><th class='num'>None</th><th class='num'>Refused</th><th class='num'>Problems</th><th class='num'>Tokens</th><th class='num'>Cost</th><th class='num'>Time</th><th>Last change</th></tr></thead><tbody>" +
278
+ rowsHtml + "</tbody></table></div>";
279
+ }
280
+ // history-summary:end
236
281
  function renderHistory() {
237
282
  const body = $("histbody");
238
283
  if (!body) return;
239
284
  markActive($("histfilter"), "hist", histFilter);
240
- body.innerHTML = historyNoteHtml(histNote) +
241
- historyTableHtml(histRows, histFilter, new Set(((lastStatus && lastStatus.loops) || []).map((l) => l.role)), histGrep, histSinceKey);
285
+ const sum = $("histsum");
286
+ if (sum) sum.style.display = "";
287
+ markActive(sum, "histsum", histMode);
288
+ // The outcome filter is meaningless in Summary mode (the summary already folds every class),
289
+ // so it is hidden there; only the body changes, keeping the same fetch path.
290
+ const filterEl = $("histfilter");
291
+ if (filterEl) filterEl.style.display = histMode === "summary" ? "none" : "";
292
+ // Summary mode shows the per-loop table when the fetch carried a summary over actual rows;
293
+ // otherwise (no rows, or a too-old server that answered without a summary) it falls back to
294
+ // the same empty state the table shows.
295
+ const showSummary = histMode === "summary" && histSummary && histRows && histRows.length > 0;
296
+ const bodyHtml = showSummary
297
+ ? histSummaryHtml(histSummary)
298
+ : historyTableHtml(histRows, histFilter, new Set(((lastStatus && lastStatus.roles) || []).map((l) => l.role)), histGrep, histSinceKey);
299
+ body.innerHTML = historyNoteHtml(histNote) + bodyHtml;
242
300
  }
243
301
  `;
@@ -0,0 +1,24 @@
1
+ // vocabulary: plain
2
+ /** The dashboard's browser-side twins of roleLabel and eventTypeLabel. The script is
3
+ * String.raw text and cannot import server modules, so the data is interpolated at module build
4
+ * and the few lines of logic are hand-kept copies, pinned against their TypeScript originals by
5
+ * test/gui-client-labels.test.ts. Spliced into gui-client.ts's script right after the format
6
+ * helpers, so every later client module can call the two functions. */
7
+ import { EVENT_TYPE_LABELS, RUN_RESULT_LABELS } from "../../events/event-labels.js";
8
+ import { INSTANCE_ID_SOURCE } from "../../roles/loop-ids.js";
9
+ export const GUI_CLIENT_LABELS_JS = String.raw ` // labels:start
10
+ const INSTANCE_ID = new RegExp(${JSON.stringify(INSTANCE_ID_SOURCE)});
11
+ const EVENT_TYPE_LABELS = ${JSON.stringify(EVENT_TYPE_LABELS)};
12
+ const RUN_RESULT_LABELS = ${JSON.stringify(RUN_RESULT_LABELS)};
13
+ const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
14
+ function roleLabel(id) {
15
+ const m = INSTANCE_ID.exec(id);
16
+ return m ? m[1] + " #" + Number(m[2]) : id;
17
+ }
18
+ function eventTypeLabel(code) {
19
+ if (hasOwn(EVENT_TYPE_LABELS, code)) return EVENT_TYPE_LABELS[code];
20
+ if (hasOwn(RUN_RESULT_LABELS, code)) return RUN_RESULT_LABELS[code];
21
+ return String(code).replace(/_/g, " ");
22
+ }
23
+ // labels:end
24
+ `;
@@ -1,15 +1,40 @@
1
- /** The dashboard's loops table, browser-side — the fleet's rows grouped by what the loops are
2
- * doing (working, landing, needs-attention, paused, idle), each with its status pill, current
1
+ // vocabulary: plain
2
+ /** The dashboard's loops table, browser-side — the team's rows grouped by what the loops are
3
+ * doing (working, merge, needs-attention, paused, idle), each with its status pill, current
3
4
  * work, spend, and per-row operator controls (prompt, wake, pause/resume, and the two-click
4
5
  * abort), patched cell by cell so a ticking row keeps focus and hover. renderLoops repaints it
5
- * from each /api/status poll; renderFleet (gui-client-fleet.ts) calls it. Spliced into
6
+ * from each /api/status poll; renderTeam (gui-client-team.ts) calls it. Spliced into
6
7
  * gui-client.ts's script, reaching its helpers (esc, icon, phaseInfo, sortLoops, postAction,
7
8
  * setTarget, focusComposer, rerender, …) through that concatenation. */
8
9
  export const GUI_CLIENT_LOOPS_JS = String.raw `
9
10
  // ---- loops ----
10
11
  // row-actions:start
11
- let confirmAbort = null; // { role, until } while an abort waits for its confirming click
12
- const abortConfirming = (role) => Boolean(confirmAbort && confirmAbort.role === role && confirmAbort.until > Date.now());
12
+ // A two-click confirm, the confirmSlot shape the drawer's note-clear/entry-cancel also use:
13
+ // the first click stores a { id, title?, until } record and repaints; armed() fires a second
14
+ // click inside the 5 s window; a 5100 ms auto-disarm drops an unconfirmed record past its
15
+ // expiry. Defined here (the earliest consumer splice) so the loops' abort in this file, an
16
+ // earlier splice, and the drawer's notebook-clear/entry-cancel in a later splice all share
17
+ // one arm, guard, and disarm instead of three copies.
18
+ function confirmSlot(repaint) {
19
+ let confirm = null; // { id, title?, until }
20
+ return {
21
+ armed: (id, title) => Boolean(confirm && confirm.id === id && (title === undefined || confirm.title === title) && confirm.until > Date.now()),
22
+ disarm: () => { confirm = null; },
23
+ arm(id, title) {
24
+ confirm = { id, title, until: Date.now() + 5000 };
25
+ repaint();
26
+ setTimeout(() => { if (confirm && confirm.id === id && confirm.until <= Date.now()) { confirm = null; repaint(); } }, 5100);
27
+ },
28
+ };
29
+ }
30
+ // The abort slot: keyed by role, so arming is per loop — a second click on the same loop's
31
+ // abort fires, while a first click on another loop's abort re-arms instead of posting.
32
+ const confirmAbortSlot = confirmSlot(rerender);
33
+ const abortConfirming = (role) => confirmAbortSlot.armed(role);
34
+ // The drawer's Disable slot: a second click on the same role's Disable fires, a first click
35
+ // on another role's re-arms — the same per-role keying as the abort slot it sits beside.
36
+ const confirmDisableSlot = confirmSlot(rerender);
37
+ const disableConfirming = (role) => confirmDisableSlot.armed(role);
13
38
  let roleChooser = null; // { role, action, until }: the one open duration chooser ("pause-for" or "wake-in")
14
39
  const roleChooserOpen = (role, action) => Boolean(roleChooser && roleChooser.role === role && roleChooser.action === action && roleChooser.until > Date.now());
15
40
  const rolePauseMenuOpen = (role) => roleChooserOpen(role, "pause-for");
@@ -26,9 +51,11 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
26
51
  return;
27
52
  }
28
53
  if (action === "abort" && !abortConfirming(role)) {
29
- confirmAbort = { role, until: Date.now() + 5000 };
30
- rerender();
31
- setTimeout(() => { if (confirmAbort && confirmAbort.role === role && confirmAbort.until <= Date.now()) { confirmAbort = null; rerender(); } }, 5100);
54
+ confirmAbortSlot.arm(role);
55
+ return;
56
+ }
57
+ if (action === "disable" && !disableConfirming(role)) {
58
+ confirmDisableSlot.arm(role);
32
59
  return;
33
60
  }
34
61
  if ((action === "pause-for" || action === "wake-in") && forSeconds === undefined) {
@@ -36,20 +63,27 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
36
63
  rerender();
37
64
  return;
38
65
  }
39
- confirmAbort = null;
66
+ confirmAbortSlot.disarm();
67
+ confirmDisableSlot.disarm();
40
68
  roleChooser = null;
41
69
  const pausing = action === "pause" || action === "pause-for";
42
70
  const timed = action === "pause-for" && forSeconds > 0;
43
- const path = action === "abort" ? "/api/abort" : action === "reset-counters" ? "/api/reset-counters" : action === "wake-cancel" ? "/api/wake-cancel" : pausing || action === "resume" ? "/api/pause-role" : "/api/wake";
71
+ const path = action === "abort" ? "/api/abort" : action === "disable" ? "/api/role-disable" : action === "reset-counters" ? "/api/reset-counters" : action === "wake-cancel" ? "/api/wake-cancel" : pausing || action === "resume" ? "/api/pause-role" : "/api/wake";
44
72
  const wakeIn = action === "wake-in";
45
73
  const body = pausing || action === "resume" ? { role, paused: pausing, ...(timed ? { forSeconds } : {}) } : wakeIn ? { role, inSeconds: forSeconds } : { role };
46
74
  await postAction(path, body, (d) => {
75
+ if (action === "disable") {
76
+ // The role drops out of d.roles on the next poll, so without closing it the open
77
+ // drawer would fall to its "no longer part of the team" head.
78
+ closeDrawer();
79
+ return d && typeof d.message === "string" ? d.message : role + ": done";
80
+ }
47
81
  let msg = d && typeof d.message === "string" ? d.message : action === "wake-cancel" ? role + ": scheduled wake cancelled" : role + ": done";
48
82
  if (wakeIn && !(d && typeof d.message === "string")) msg = role + " wakes in " + humanSeconds(forSeconds);
49
83
  else if (timed) msg = role + " paused for " + humanSeconds(forSeconds);
50
84
  else if (pausing || action === "resume") {
51
85
  msg = role + (d && d.changed
52
- ? (pausing ? " paused — it starts no new ticks" : " resumed")
86
+ ? (pausing ? " paused — it starts no new runs" : " resumed")
53
87
  : (pausing ? " was already paused" : " was not paused"));
54
88
  }
55
89
  return msg;
@@ -72,77 +106,85 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
72
106
  const detail = phaseDetailBase(l, info, d);
73
107
  const w = scheduledWakeFor(l, d);
74
108
  if (!w) return detail;
75
- return (detail ? detail + " · " : "") + "operator wake in " + humanSeconds(Math.max(0, Math.round((w.atMs - Date.now()) / 1000)));
109
+ return (detail ? detail + " · " : "") + "your wake in " + humanSeconds(Math.max(0, Math.round((w.atMs - Date.now()) / 1000)));
76
110
  }
77
111
  function phaseDetailBase(l, info, d) {
78
112
  const raw = fmtNextRun(l, d.running);
79
113
  const slowed = / ×(\d+)$/.exec(raw);
80
114
  const next = slowed ? raw.slice(0, slowed.index) : raw;
81
- const backoff = next.startsWith("backoff ") ? "backing off — retries in " + next.slice(8) : "";
82
- // A yield-scaled clock: the loop's recent ticks landed nothing, so it ticks less often.
83
- const note = slowed ? " · slowed ×" + slowed[1] + ", nothing landed lately" : "";
84
- if (info.key === "sleeping") return (backoff || (next === "-" ? "" : next === "now" ? "wakes now" : "wakes in " + next)) + note;
85
- if (info.key === "queued") return (backoff || info.detail) + note;
86
- if (info.key === "paused" && d.pausedRolesUntil && !d.paused && (d.pausedRoles || []).includes(l.role)) return info.detail + " · resumes in " + humanSeconds(Math.max(0, Math.round((d.pausedRolesUntil - Date.now()) / 1000)));
87
- return info.detail;
115
+ const backoff = next.startsWith("sleeping ") ? "sleeping — next check in " + next.slice(9) : "";
116
+ // A yield-scaled clock: the loop's recent ticks merged nothing, so it ticks less often.
117
+ const note = slowed ? " · slowed ×" + slowed[1] + ", nothing merged lately" : "";
118
+ // The consecutive no-change streak (status-payload noChangeStreak, mirrored from
119
+ // status-model loopPhase's · no change ×N suffix): how many completed ticks in a row
120
+ // returned nothing to do. Shown only on a resting idle cell (sleeping/queued/idle/
121
+ // deferred) — never on live work, a pause, a failure, or main-red, which the status
122
+ // label likewise leaves bare. Observational only: a legitimately idle maintenance loop
123
+ // can hold a long streak.
124
+ const noChange = ["sleeping", "queued", "idle", "deferred"].includes(info.key) && (l.noChangeStreak || 0) >= 2
125
+ ? " · no change ×" + l.noChangeStreak : "";
126
+ if (info.key === "sleeping") return (backoff || (next === "-" ? "" : next === "now" ? "wakes now" : "wakes in " + next)) + note + noChange;
127
+ if (info.key === "queued") return (backoff || info.detail) + note + noChange;
128
+ if (info.key === "paused" && d.pausedRolesUntil && !d.paused && (d.pausedLoops || []).includes(l.role)) return info.detail + " · resumes in " + humanSeconds(Math.max(0, Math.round((d.pausedRolesUntil - Date.now()) / 1000)));
129
+ return info.detail + noChange;
88
130
  }
89
131
  function loopCells(l, d) {
90
132
  const info = phaseInfo(l.phase);
91
- const paused = (d.pausedRoles || []).includes(l.role);
133
+ const paused = (d.pausedLoops || []).includes(l.role);
92
134
  const queued = (d.roleInbox || {})[l.role] || 0;
93
135
  const detail = phaseDetail(l, info, d);
94
136
  const stalled = info.live && STALL.test(l.phase);
95
- const name = "<div class='loop-name'><button type='button' class='linkish role' data-open='" + esc(l.role) + "' title='Open " + esc(l.role) + "'>" + esc(l.role) + "</button>" +
96
- (l.custom ? "<span class='tag' title='A user-defined loop from tumwater.json'>custom</span>" : "") +
97
- (l.modelTier ? "<span class='tag' title='The tier of the top-level model map this loop resolves at'>" + esc(l.modelTier) + "</span>" : "") +
137
+ const name = "<div class='loop-name'><button type='button' class='linkish role' data-open='" + esc(l.role) + "' title='Open " + esc(roleLabel(l.role)) + "'>" + esc(roleLabel(l.role)) + "</button>" + // vocabulary-ok: CSS class the stylesheet and tests match
138
+ (l.custom ? "<span class='tag' title='A custom role from tumwater.json'>custom</span>" : "") +
139
+ (l.modelTier ? "<span class='tag' title='The model tier this role resolves at'>" + esc(l.modelTier) + "</span>" : "") +
98
140
  (l.fallback ? "<span class='tag t-amber' title='Running on the model fallback while its primary keeps failing provider-class errors'>fallback</span>" : "") +
99
- (queued ? "<span class='tag t-indigo' title='" + esc(plural(queued, "prompt") + " queued for this loop") + "'>" + icon("chat") + queued + "</span>" : "") +
100
- "</div><div class='sub'>" + esc(plural(l.commits, "commit") + " · " + plural(l.ticks, "tick")) + (l.model ? " · " + esc(l.model) : "") + "</div>";
141
+ (queued ? "<span class='tag t-indigo' title='" + esc(plural(queued, "message") + " in this role's inbox") + "'>" + icon("chat") + queued + "</span>" : "") +
142
+ "</div><div class='sub'>" + esc(plural(l.commits, "commit") + " · " + plural(l.runs, "run") + " · merged " + (l.lastMergedAt ? fmtAgo(l.lastMergedAt) : "never")) + (l.model ? " · " + esc(l.model) : "") +
143
+ (l.modelFromPi ? " <span class='tag' title='pi&#39;s own default model, from its settings.json: tumwater.json names no model'>(pi default)</span>" : "") + "</div>";
101
144
  const status = pill(info) + (detail ? "<div class='sub" + (stalled ? " t-red" : "") + "' title='" + esc(l.phase) + "'>" + esc(detail) + "</div>" : "");
102
145
  let activity;
103
- if (info.live || info.key === "vetted") {
104
- // Parallel work instances, part 6/7: while idle or landing, show the entry the instance
105
- // claimed before the landing summary placeholder.
106
- const text = l.currentWork || l.claim || landingSummary(d, l.role);
107
- const pending = info.key === "working" ? "Starting up…" : info.key === "reviewing" ? "Reviewing the change…" : "Landing the change…";
146
+ if (info.live || info.key === "vetted") { // vocabulary-ok: internal phase key
147
+ // Parallel work instances, part 6/7: while idle or merge, show the entry the instance
148
+ // claimed before the merge summary placeholder.
149
+ const text = l.currentWork || l.claim || mergeSummary(d, l.role);
150
+ const pending = info.key === "working" ? "Starting up…" : info.key === "reviewing" ? "Reviewing the change…" : "Merging the change…";
108
151
  activity = text ? "<div class='now clamp2' title='" + esc(text) + "'>" + esc(text) + "</div>" : "<div class='muted'>" + pending + "</div>";
109
- } else if (l.claim || (l.instanceOf && info.key === "queued")) {
110
- // An idle instance holding a claim names its entry; an idle extra with none (skipped for
111
- // want of a free entry) says why. The queued key is the tell that the skip, not a
112
- // sleep or a gate, left it idle.
113
- const text = l.claim || ("idle — no unclaimed " + (l.instanceOf === "bugfix" ? "bugs" : "plans"));
152
+ } else if (l.claim) {
153
+ // An idle instance holding a claim names its entry; the pill detail carries why an
154
+ // idle loop with none is idle.
155
+ const text = l.claim;
114
156
  activity = "<div class='now clamp2' title='" + esc(text) + "'>" + esc(text) + "</div>";
115
157
  } else if (l.lastResult) {
116
158
  const r = resultInfo(l.lastResult);
117
159
  const why = resultWhy(l);
118
160
  activity = "<div class='clamp2' title='" + esc(why || r.label) + "'><span class='res t-" + r.tone + "'>" + esc(r.label) + "</span>" +
119
- (why ? " <span class='now'>" + esc(why) + "</span>" : "") + "</div><div class='sub'>" + esc(fmtAgo(l.lastTickEndedAt)) + "</div>";
120
- } else activity = "<div class='muted'>No ticks yet</div>";
161
+ (why ? " <span class='now'>" + esc(why) + "</span>" : "") + "</div><div class='sub'>" + esc(fmtAgo(l.lastRunEndedAt)) + "</div>";
162
+ } else activity = "<div class='muted'>No runs yet</div>";
121
163
  const spent = "<div>" + esc(fmtUsd(l.todayUsd)) + "</div><div class='sub'>" + esc(fmtUsd(l.costUsd)) + " total</div>";
122
164
  const b = (action, ic, title, cls) => "<button type='button' class='icon-btn rowaction" + (cls ? " " + cls : "") + "' data-action='" + action +
123
165
  "' data-role='" + esc(l.role) + "' title='" + esc(title) + "' aria-label='" + esc(title) + "'>" + icon(ic) + "</button>";
124
- let actions = b("prompt", "chat", "Prompt " + l.role);
166
+ let actions = b("prompt", "chat", "Message " + roleLabel(l.role));
125
167
  // The running set (info.live), not the permit-holder inFlight: the exempt running director
126
168
  // has an abortable tick without holding a maxConcurrent permit (BUGS.md 2026-10-06).
127
169
  if (info.live) {
128
170
  actions += abortConfirming(l.role)
129
- ? "<button type='button' class='icon-btn rowaction confirming' data-action='abort' data-role='" + esc(l.role) + "' title='Click again to abort the running tick'>" + icon("stop") + "Abort?</button>"
130
- : b("abort", "stop", "Abort " + l.role + "'s running tick", "danger");
131
- } else actions += b("wake", "bolt", "Wake " + l.role + " now");
132
- if (paused) actions += b("resume", "play", "Resume " + l.role);
171
+ ? "<button type='button' class='icon-btn rowaction confirming' data-action='abort' data-role='" + esc(l.role) + "' title='Click again to stop this run'>" + icon("stop") + "Stop?</button>"
172
+ : b("abort", "stop", "Stop " + roleLabel(l.role) + "'s run", "danger");
173
+ } else actions += b("wake", "bolt", "Wake " + roleLabel(l.role) + " now");
174
+ if (paused) actions += b("resume", "play", "Resume " + roleLabel(l.role));
133
175
  else if (rolePauseMenuOpen(l.role)) {
134
176
  actions += ROLE_PAUSE_CHOICES.map((c) => "<button type='button' class='icon-btn rowaction' data-action='pause-for' data-for='" + c[0] +
135
- "' data-role='" + esc(l.role) + "' title='Pause " + esc(l.role) + " " + (c[0] ? "for " + c[1] : "until resumed") + "'>" + esc(c[1]) + "</button>").join("");
136
- } else actions += b("pause", "pause", "Pause " + l.role) + b("pause-for", "clock", "Pause " + l.role + " for a duration");
177
+ "' data-role='" + esc(l.role) + "' title='Pause " + esc(roleLabel(l.role)) + " " + (c[0] ? "for " + c[1] : "until resumed") + "'>" + esc(c[1]) + "</button>").join("");
178
+ } else actions += b("pause", "pause", "Pause " + roleLabel(l.role)) + b("pause-for", "clock", "Pause " + roleLabel(l.role) + " for a duration");
137
179
  return { info, cells: [name, status, activity, spent, "<div class='actions'>" + actions + "</div>"] };
138
180
  }
139
181
  // loops-table:end
140
- const LOOP_TDS = ["c-loop", "c-status", "c-activity", "c-today num", "c-actions"];
182
+ const LOOP_TDS = ["c-loop", "c-status", "c-activity", "c-today num", "c-actions"]; // vocabulary-ok: CSS class the stylesheet and tests match
141
183
  let loopKeys = "";
142
184
  // Rows are patched cell by cell: a working loop's elapsed time changes every second, and
143
185
  // rewriting only that cell keeps focus and hover on the row's buttons.
144
186
  function patchLoops(rows) {
145
- const tbody = $("loops");
187
+ const tbody = $("rolerows");
146
188
  const keys = rows.map((r) => r.key).join("|");
147
189
  if (keys !== loopKeys) {
148
190
  loopKeys = keys;
@@ -163,7 +205,7 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
163
205
  function renderLoops(d) {
164
206
  $("loopstable").classList.toggle("free", Boolean(d.budget && d.budget.free));
165
207
  const byGroup = LOOP_GROUPS.map(() => []);
166
- for (const l of sortLoops(d.loops || [])) {
208
+ for (const l of sortLoops(d.roles || [])) {
167
209
  const rank = loopRank(l.phase);
168
210
  byGroup[LOOP_GROUPS.findIndex((g) => g.slice(1).includes(rank))].push(l);
169
211
  }
@@ -174,13 +216,13 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
174
216
  rows.push({ key: "g" + gi, cls: "group", group: true, cells: [esc(g[0]) + "<span class='badge'>" + byGroup[gi].length + "</span>"] });
175
217
  for (const l of byGroup[gi]) {
176
218
  const { info, cells } = loopCells(l, d);
177
- rows.push({ key: "l:" + l.role, role: l.role, cls: "loop clickable" + (info.live ? " live" : "") + (selected === l.role ? " selected" : ""), cells });
219
+ rows.push({ key: "l:" + l.role, role: l.role, cls: "loop clickable" + (info.live ? " live" : "") + (selected === l.role ? " selected" : ""), cells }); // vocabulary-ok: CSS class the stylesheet and tests match
178
220
  }
179
221
  });
180
- if (!rows.length) rows.push({ key: "empty", cls: "group", group: true, cells: ["No loops are enabled in tumwater.json"] });
222
+ if (!rows.length) rows.push({ key: "empty", cls: "group", group: true, cells: ["No roles are enabled in tumwater.json"] });
181
223
  patchLoops(rows);
182
224
  }
183
- $("loops").addEventListener("click", (ev) => {
225
+ $("rolerows").addEventListener("click", (ev) => {
184
226
  const b = clickClosest(ev, "button.rowaction");
185
227
  if (b) { ev.preventDefault(); rowAction(b.dataset.action, b.dataset.role, b.dataset.for === undefined ? undefined : Number(b.dataset.for)); return; }
186
228
  const tr = clickClosest(ev, "tr[data-role]");
@@ -189,14 +231,14 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
189
231
  $("resetall").addEventListener("click", () =>
190
232
  postAction("/api/reset-counters", {}, (d) => (d && typeof d.message === "string" ? d.message : "counters reset")));
191
233
  $("wakeall").addEventListener("click", () =>
192
- postAction("/api/wake", {}, (d) => (d && typeof d.message === "string" ? d.message : "every loop woken")));
234
+ postAction("/api/wake", {}, (d) => (d && typeof d.message === "string" ? d.message : "every role woken")));
193
235
  // wake-later:start
194
236
  const WAKE_LATER_CHOICES = [[1800, "In 30 minutes"], [3600, "In 1 hour"], [7200, "In 2 hours"], [28800, "In 8 hours"]];
195
237
  function openWakeLaterMenu() {
196
238
  openPopover("wakelaterwrap", {
197
239
  menu: true,
198
240
  role: "menu",
199
- html: "<div class='menu-title'>Wake every loop</div>" +
241
+ html: "<div class='menu-title'>Wake every role</div>" +
200
242
  WAKE_LATER_CHOICES.map((c) => "<button type='button' class='menu-item' role='menuitem' data-wakein='" + c[0] + "'>" +
201
243
  icon("clock") + esc(c[1]) + "</button>").join("") +
202
244
  "<button type='button' class='menu-item' role='menuitem' data-wakecancel='1'>" + icon("x") + "Cancel scheduled wakes</button>",
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** A small, safe Markdown renderer for the dashboard, browser-side: the failure digest and the
2
3
  * backlog entries (PLANS/BUGS/QUESTIONS.md bodies) are Markdown the loops write, and reading
3
4
  * them as rendered headings, lists, and tables beats a pre-formatted text blob. It covers what