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,6 +1,7 @@
1
+ // vocabulary: plain
1
2
  /** The dashboard's detail drawer, browser-side: a sheet on the right that opens over the page
2
3
  * without losing it. For a loop it shows the state, the loop's controls (prompt, wake now or later,
3
- * pause/resume, abort), what it is working on, its last result, the metrics the fleet table
4
+ * pause/resume, abort), what it is working on, its last result, the metrics the team table
4
5
  * leaves out (tokens, context, cost, timings), its recent ticks (/api/history filtered to the
5
6
  * loop), and its live transcript (/api/transcript, re-fetched on the 1 s poll and kept pinned
6
7
  * to the bottom while the reader is there). For a backlog entry it shows the entry in full
@@ -12,10 +13,19 @@
12
13
  export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind: "loop", role } | { kind: "entry", file, index } while open
13
14
  let drawerTicks = null; // the open loop's recent history rows (null until fetched)
14
15
  let drawerTicksAt = 0;
15
- let drawerChange = null; // the open loop's unlanded change: { role, state: "loading"|"ok"|"error", view, message }
16
+ let drawerChange = null; // the open loop's unmerged change: { role, state: "loading"|"ok"|"error", view, message }
16
17
  let drawerRole = null; // the open loop's /api/role payload: { role, state: "loading"|"ok"|"error", view, message }
17
18
  const openEntryKey = () => (drawer && drawer.kind === "entry" ? drawer.file + ":" + drawer.index : null);
18
- const openLoopRole = () => (drawer && drawer.kind === "loop" ? drawer.role : null);
19
+ const openLoopRole = () => (drawer && drawer.kind === "loop" ? drawer.role : null); // vocabulary-ok: the drawer kind stays "loop"
20
+ // One key/value row builder shared by the loop-metrics table (renderLoopDrawer) and the
21
+ // Configuration section (drawerConfigHtml): identical markup, formerly copy-pasted into two
22
+ // local consts (kv and row) so the row shape could drift between the two sections.
23
+ const kvRow = (k, v, title) => "<div><dt>" + esc(k) + "</dt><dd title='" + esc(title || v) + "'>" + esc(v) + "</dd></div>";
24
+ // A diff block in the Change section (the committed change and the dirty-tree uncommitted
25
+ // diff, drawerChangeHtml's two arms): trim the trailing newline the diff carriage-returned,
26
+ // escape the rest, and wrap it in the shared .diff pre. Formerly copy-pasted into both arms
27
+ // so the trim/escape could drift between them.
28
+ const diffPre = (text) => "<pre class='mono diff'>" + esc(String(text).replace(/\n+$/, "")) + "</pre>";
19
29
 
20
30
  function showDrawer(bodyHtml) {
21
31
  for (const id of ["drawerhead", "draweractions", "drawermeta", "drawerrole", "transcript", "entrybody"]) delete lastPaint[id];
@@ -29,8 +39,14 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
29
39
  drawer = null;
30
40
  entryEditing = null;
31
41
  noteEditing = null;
32
- confirmNoteClear = null;
33
- if (wasLoop && location.hash.startsWith("#loop/")) history.replaceState(null, "", "#" + activeView);
42
+ capEditing = null;
43
+ instructionsEditing = null;
44
+ quietEditing = null;
45
+ intervalEditing = null;
46
+ instancesEditing = null;
47
+ modelEditing = null;
48
+ disarmNoteClear();
49
+ if (wasLoop && (location.hash.startsWith("#role/") || location.hash.startsWith("#loop/"))) history.replaceState(null, "", "#" + activeView); // vocabulary-ok: legacy dashboard hash, still routed
34
50
  drawerTicks = null;
35
51
  drawerChange = null;
36
52
  drawerRole = null;
@@ -39,12 +55,12 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
39
55
  $("drawerhead").innerHTML = "";
40
56
  $("drawerbody").innerHTML = "";
41
57
  for (const id of ["drawerhead", "draweractions", "drawermeta", "drawerrole", "transcript", "entrybody"]) delete lastPaint[id];
42
- if (lastStatus) renderFleet(lastStatus); // drop the row highlight
58
+ if (lastStatus) renderTeam(lastStatus); // drop the row highlight
43
59
  }
44
60
 
45
61
  function openLoop(role) {
46
- drawer = { kind: "loop", role };
47
- const hash = "#loop/" + encodeURIComponent(role);
62
+ drawer = { kind: "loop", role }; // vocabulary-ok: the drawer kind stays "loop"
63
+ const hash = "#role/" + encodeURIComponent(role);
48
64
  if (location.hash !== hash) history.replaceState(null, "", hash);
49
65
  drawerTicks = null;
50
66
  drawerTicksAt = 0;
@@ -55,7 +71,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
55
71
  "<pre id='transcript' class='transcript'><span class='muted'>Loading…</span></pre></section>");
56
72
  if (lastStatus) {
57
73
  renderLoopDrawer(lastStatus);
58
- renderFleet(lastStatus);
74
+ renderTeam(lastStatus);
59
75
  }
60
76
  loadLoopTicks(true);
61
77
  loadDrawerChange(role);
@@ -70,16 +86,16 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
70
86
  drawer = { kind: "entry", file, index: Number(index) };
71
87
  entryEditing = null;
72
88
  showDrawer("<div id='entrybody'><div class='empty'>Loading…</div></div>");
73
- if (lastStatus) renderFleet(lastStatus);
89
+ if (lastStatus) renderTeam(lastStatus);
74
90
  refreshEntry();
75
91
  }
76
92
  function toggleEntry(file, index) {
77
93
  if (openEntryKey() === file + ":" + index) closeDrawer();
78
94
  else openEntry(file, index);
79
95
  }
80
- // The change the landing slot holds for a role — what a landing (or approved) row is about.
81
- function landingSummary(d, role) {
82
- const f = d.landQueue && d.landQueue.inFlight;
96
+ // The change the merge slot holds for a role — what a merge (or approved) row is about.
97
+ function mergeSummary(d, role) {
98
+ const f = d.mergeQueue && d.mergeQueue.inFlight;
83
99
  if (!f) return "";
84
100
  const change = (Array.isArray(f.changes) ? f.changes : [f]).find((c) => c.role === role);
85
101
  return change && change.summary ? change.summary : "";
@@ -88,35 +104,36 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
88
104
  function renderLoopDrawer(d) {
89
105
  const role = openLoopRole();
90
106
  if (!role) return;
91
- const l = (d.loops || []).find((x) => x.role === role);
107
+ const l = (d.roles || []).find((x) => x.role === role);
92
108
  if (!l) {
93
- paintPanel("drawerhead", "<div class='kicker'>Loop</div><h2 id='drawertitle' class='mono'>" + esc(role) +
94
- "</h2><span class='muted'>This loop is no longer part of the fleet.</span>");
109
+ paintPanel("drawerhead", "<div class='kicker'>Role</div><h2 id='drawertitle' class='mono'>" + esc(roleLabel(role)) +
110
+ "</h2><span class='muted'>This role is no longer part of the team.</span>");
95
111
  paintPanel("draweractions", "");
96
112
  paintPanel("drawermeta", drawerChangeSection());
97
113
  paintPanel("drawerrole", drawerRoleSection());
98
114
  return;
99
115
  }
100
116
  const info = phaseInfo(l.phase);
101
- const paused = (d.pausedRoles || []).includes(l.role);
117
+ const paused = (d.pausedLoops || []).includes(l.role);
102
118
  const detail = phaseDetail(l, info, d);
103
- paintPanel("drawerhead", "<div class='kicker'>Loop" + (l.custom ? " · user-defined" : "") + "</div>" +
104
- "<h2 id='drawertitle' class='mono'>" + esc(l.role) + "</h2><div class='chips'>" + pill(info) +
119
+ paintPanel("drawerhead", "<div class='kicker'>Role" + (l.custom ? " · user-defined" : "") + "</div>" +
120
+ "<h2 id='drawertitle' class='mono'>" + esc(roleLabel(l.role)) + "</h2><div class='chips'>" + pill(info) +
105
121
  (detail ? "<span class='muted'>" + esc(detail) + "</span>" : "") + "</div>");
106
122
 
107
123
  const btn = (action, ic, label, cls) => "<button type='button' class='btn btn-sm rowaction" + (cls ? " " + cls : "") + "' data-action='" + action +
108
124
  "' data-role='" + esc(l.role) + "'>" + icon(ic) + esc(label) + "</button>";
109
- let actions = btn("prompt", "chat", "Prompt this loop");
125
+ let actions = btn("prompt", "chat", "Message this role");
110
126
  // The running set (info.live), not the permit-holder inFlight: the exempt running director
111
127
  // has an abortable tick without holding a maxConcurrent permit (BUGS.md 2026-10-06).
112
- if (info.live) actions += abortConfirming(l.role) ? btn("abort", "stop", "Click again to abort", "btn-danger confirming") : btn("abort", "stop", "Abort this tick", "btn-danger");
128
+ if (info.live) actions += abortConfirming(l.role) ? btn("abort", "stop", "Click again to stop", "btn-danger confirming") : btn("abort", "stop", "Stop this run", "btn-danger");
113
129
  else {
114
130
  actions += btn("wake", "bolt", "Wake now");
115
131
  if (roleWakeMenuOpen(l.role)) {
116
132
  actions += ROLE_WAKE_CHOICES.map((c) => "<button type='button' class='btn btn-sm rowaction' data-action='wake-in' data-for='" + c[0] +
117
133
  "' data-role='" + esc(l.role) + "'>in " + esc(c[1]) + "</button>").join("");
118
134
  } else actions += btn("wake-in", "clock", "Wake later…");
119
- // Exact id only: the cancel matches ids exactly, so an instance named via its base role would cancel nothing.
135
+ // Exact id only: the cancel matches ids exactly, so an instance named via its base role
136
+ // would cancel nothing.
120
137
  if (scheduledWakesFor(l, d).some((w) => w.roles.includes(l.role))) actions += btn("wake-cancel", "x", "Cancel scheduled wake");
121
138
  }
122
139
  if (paused) actions += btn("resume", "play", "Resume");
@@ -125,44 +142,47 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
125
142
  "' data-role='" + esc(l.role) + "'>" + esc(c[1]) + "</button>").join("");
126
143
  } else actions += btn("pause", "pause", "Pause") + btn("pause-for", "clock", "Pause for…");
127
144
  actions += btn("reset-counters", "refresh", "Reset counters");
145
+ // The two-click Disable, the drawer half of the enable/disable pair (the Settings card's
146
+ // Enable re-enables). The director cannot be disabled, so it shows no Disable button.
147
+ if (l.role !== "director") actions += disableConfirming(l.role) ? btn("disable", "x", "Click again to disable", "btn-danger confirming") : btn("disable", "x", "Disable", "btn-danger");
128
148
 
129
- const working = info.live || info.key === "vetted";
149
+ const working = info.live || info.key === "vetted"; // vocabulary-ok: the persisted phase key
130
150
  const now = working
131
- ? "<section><div class='sec-head'><h3>" + (info.key === "working" ? "Working on" : info.key === "reviewing" ? "Under review" : "Landing") +
132
- "</h3></div><div class='note'>" + esc(l.currentWork || landingSummary(d, l.role) || (info.key === "working" ? "Starting up…" : "Waiting for its first words…")) + "</div></section>"
151
+ ? "<section><div class='sec-head'><h3>" + (info.key === "working" ? "Working on" : info.key === "reviewing" ? "Under review" : "Merging") +
152
+ "</h3></div><div class='note'>" + esc(l.currentWork || mergeSummary(d, l.role) || (info.key === "working" ? "Starting up…" : "Waiting for its first words…")) + "</div></section>"
133
153
  : "";
134
154
  const r = l.lastResult ? resultInfo(l.lastResult) : null;
135
155
  const why = resultWhy(l);
136
- const last = "<section><div class='sec-head'><h3>Last result</h3>" + (l.lastTickEndedAt ? "<span class='muted' style='font-size:13px'>" + esc(fmtAgo(l.lastTickEndedAt)) + "</span>" : "") +
137
- "</div><div class='note'>" + (r ? "<span class='res t-" + r.tone + "'>" + esc(r.label) + "</span>" + (why ? " — " + esc(why) : "") : "<span class='muted'>No completed ticks yet.</span>") + "</div></section>";
138
- const kv = (k, v, title) => "<div><dt>" + esc(k) + "</dt><dd title='" + esc(title || v) + "'>" + esc(v) + "</dd></div>";
156
+ const last = "<section><div class='sec-head'><h3>Last result</h3>" + (l.lastRunEndedAt ? "<span class='muted' style='font-size:13px'>" + esc(fmtAgo(l.lastRunEndedAt)) + "</span>" : "") +
157
+ "</div><div class='note'>" + (r ? "<span class='res t-" + r.tone + "'>" + esc(r.label) + "</span>" + (why ? " — " + esc(why) : "") : "<span class='muted'>No completed runs yet.</span>") + "</div></section>";
139
158
  const next = fmtNextRun(l, d.running);
140
159
  // fmtNextRun's shapes: "-", "now" (possibly with a " ×N" yield suffix), and a bare or
141
160
  // "backoff "-prefixed countdown. A backoff countdown keeps the label but as a suffix;
142
161
  // a suffixed "now" passes through as-is — it is due now, not a future time.
143
162
  const nextNote = next === "-" ? "—"
144
163
  : next === "now" || next.startsWith("now ") ? next
145
- : next.startsWith("backoff ") ? "in " + next.slice(8) + " (backoff)"
164
+ : next.startsWith("sleeping ") ? "in " + next.slice(9) + " (sleeping)"
146
165
  : "in " + next;
147
- const tickWord = info.live ? "this tick" : "last tick";
148
- const stats = "<dl class='kv'>" + kv("Commits", String(l.commits)) + kv("Ticks", String(l.ticks)) +
149
- kv("Tokens, " + tickWord, fmtTokens(l.generated)) + kv("Peak context, " + tickWord, fmtTokens(l.peakCtx)) +
150
- kv("Spent today", fmtUsd(l.todayUsd)) + kv("Spent in total", fmtUsd(l.costUsd)) +
151
- kv("Last tick ended", l.lastTickEndedAt ? fmtAgo(l.lastTickEndedAt) : "never", fmtLastTick(l.lastTickEndedAt)) +
152
- kv("Next run", nextNote) + "</dl>";
166
+ const tickWord = info.live ? "this run" : "last run";
167
+ const stats = "<dl class='kv'>" + kvRow("Commits", String(l.commits)) + kvRow("Runs", String(l.runs)) +
168
+ kvRow("Tokens, " + tickWord, fmtTokens(l.generated)) + kvRow("Peak context, " + tickWord, fmtTokens(l.peakCtx)) +
169
+ kvRow("Spent today", fmtUsd(l.todayUsd)) + kvRow("Spent in total", fmtUsd(l.costUsd)) +
170
+ kvRow("Last run ended", l.lastRunEndedAt ? fmtAgo(l.lastRunEndedAt) : "never", fmtLastTick(l.lastRunEndedAt)) +
171
+ kvRow("Last merged", l.lastMergedAt ? fmtAgo(l.lastMergedAt) : "never") +
172
+ kvRow("Next run", nextNote) + "</dl>";
153
173
  const ticks = drawerTicks === null
154
174
  ? "<p class='muted'>Loading…</p>"
155
175
  : drawerTicks.length
156
- ? "<ul class='ticks'>" + drawerTicks.map((t) => {
176
+ ? "<ul class='ticks'>" + drawerTicks.map((t) => { // vocabulary-ok: the ticks class stays
157
177
  const res = resultInfo(t.result);
158
- return "<li><span class='when' title='" + esc(t.time) + "'>#" + esc(t.tick) + " · " + esc(fmtAgo(t.ts)) + "</span>" +
178
+ return "<li><span class='when' title='" + esc(t.time) + "'>#" + esc(t.run) + " · " + esc(fmtAgo(t.ts)) + "</span>" +
159
179
  "<span class='clamp1' title='" + esc(t.detail) + "'><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span> " + esc(t.detail) + "</span>" +
160
180
  "<span class='when'>" + (t.durationMs === null ? "" : esc(fmtSpan(Number(t.durationMs)))) + "</span></li>";
161
181
  }).join("") + "</ul>"
162
- : "<p class='muted'>No completed ticks in the log yet.</p>";
182
+ : "<p class='muted'>No completed runs in the log yet.</p>";
163
183
  paintPanel("draweractions", actions);
164
184
  paintPanel("drawermeta", now + last + stats +
165
- "<section><div class='sec-head'><h3>Recent ticks</h3></div>" + ticks + "</section>" + drawerChangeSection());
185
+ "<section><div class='sec-head'><h3>Recent runs</h3></div>" + ticks + "</section>" + drawerChangeSection());
166
186
  // The notebook/prompt live in their own panel, outside drawermeta: the poll rewrites
167
187
  // drawermeta whenever its countdown or tick-age text changes, and an innerHTML rewrite
168
188
  // would collapse an opened Next tick prompt. This panel's markup is stable while its
@@ -170,7 +190,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
170
190
  paintPanel("drawerrole", drawerRoleSection());
171
191
  }
172
192
 
173
- // The open loop's unlanded change, as /api/diff?role serves it: the full patch the CLI's
193
+ // The open loop's unmerged change, as /api/diff?role serves it: the full patch the CLI's
174
194
  // tumwater diff --role prints. Degrades like the collector — a missing worktree, a missing
175
195
  // base branch, or no work each get a plain statement, and an unreadable payload says so
176
196
  // rather than rendering a half-view. Pure of side effects; the view paints what it returns.
@@ -178,17 +198,17 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
178
198
  if (!view || typeof view !== "object") return "<p class='muted'>Pending change unavailable.</p>";
179
199
  const commits = Array.isArray(view.commits) ? view.commits : [];
180
200
  const dirty = Array.isArray(view.dirtyFiles) ? view.dirtyFiles : [];
181
- if (view.state === "absent") return "<p class='muted'>No worktree yet — this loop has not run.</p>";
201
+ if (view.state === "absent") return "<p class='muted'>No workspace yet — this role has not run.</p>";
182
202
  if (view.state === "no-base") return "<p class='muted'>Main branch " + esc(view.mainBranch) + " does not exist.</p>";
183
203
  if (view.state !== "ready" || (view.ahead === 0 && dirty.length === 0)) return "<p class='muted'>No unlanded work.</p>";
184
204
  let html = "<p class='note'>" + esc(view.branch) + " · " + esc(plural(view.ahead, "commit")) +
185
205
  " ahead of " + esc(view.mainBranch) +
186
206
  (dirty.length ? " · " + esc(plural(dirty.length, "uncommitted file")) : "") + "</p>";
187
207
  html += commitRowsHtml(commits);
188
- if (view.diff) html += "<pre class='mono diff'>" + esc(String(view.diff).replace(/\n+$/, "")) + "</pre>";
208
+ if (view.diff) html += diffPre(view.diff);
189
209
  if (dirty.length) {
190
210
  html += "<p class='note'>Uncommitted: " + esc(dirty.join(", ")) + "</p>";
191
- if (view.uncommittedDiff) html += "<pre class='mono diff'>" + esc(String(view.uncommittedDiff).replace(/\n+$/, "")) + "</pre>";
211
+ if (view.uncommittedDiff) html += diffPre(view.uncommittedDiff);
192
212
  }
193
213
  return html;
194
214
  }
@@ -218,7 +238,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
218
238
  }
219
239
  if (lastStatus) renderLoopDrawer(lastStatus);
220
240
  }
221
- // The open loop's unlanded change, as /api/diff?role serves it.
241
+ // The open loop's unmerged change, as /api/diff?role serves it.
222
242
  function loadDrawerChange(role) {
223
243
  return loadDrawerSection(role, "/api/diff?role=", "Pending change unavailable", (v) => { drawerChange = v; });
224
244
  }
@@ -235,11 +255,12 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
235
255
  const r = drawerRole;
236
256
  let body;
237
257
  if (!r || r.state === "loading") body = "<p class='muted'>Loading…</p>";
238
- else if (r.state === "error") body = "<p class='muted'>" + esc(r.message || "Notebook and prompt unavailable") + "</p>";
258
+ else if (r.state === "error") body = "<p class='muted'>" + esc(r.message || "Notes and prompt unavailable") + "</p>";
239
259
  else {
240
260
  const view = r.view && typeof r.view === "object" ? r.view : {};
241
- const note = typeof view.note === "string" && view.note.trim() !== "" ? view.note : null;
261
+ const note = typeof view.notes === "string" && view.notes.trim() !== "" ? view.notes : null;
242
262
  const nextPrompt = typeof view.nextPrompt === "string" && view.nextPrompt.trim() !== "" ? view.nextPrompt : null;
263
+ const outcomes = Array.isArray(view.recentOutcomes) ? view.recentOutcomes : [];
243
264
  const canEdit = r.role !== "director";
244
265
  const editing = canEdit && noteEditing && noteEditing.role === r.role;
245
266
  const buttons = !canEdit ? "" : "<span style='margin-left:auto'><button type='button' class='btn btn-sm' data-act='note-edit' data-arg='" + esc(r.role) + "'>Edit</button>" +
@@ -248,31 +269,267 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
248
269
  ? "<textarea id='noteedittext' rows='12' class='mono' style='width:100%'>" + esc(noteEditing.text) + "</textarea>" +
249
270
  "<div><button type='button' class='btn btn-sm btn-primary' data-act='note-save' data-arg='" + esc(r.role) + "'>Save</button> " +
250
271
  "<button type='button' class='btn btn-sm' data-act='note-discard' data-arg='" + esc(r.role) + "'>Discard</button></div>"
251
- : note ? "<pre class='mono'>" + esc(note) + "</pre>" : "<p class='muted'>No notebook yet.</p>";
252
- body = "<div class='sec-head'><h3>Notebook</h3>" + (editing ? "" : buttons) + "</div>" + noteBody +
253
- "<details><summary>Next tick prompt</summary>" + (nextPrompt ? "<pre class='mono'>" + esc(nextPrompt) + "</pre>" : "<p class='muted'>Nothing to run (empty inbox).</p>") + "</details>";
272
+ : note ? "<pre class='mono'>" + esc(note) + "</pre>" : "<p class='muted'>No notes yet.</p>";
273
+ body = "<div class='sec-head'><h3>Notes</h3>" + (editing ? "" : buttons) + "</div>" + noteBody +
274
+ "<details><summary>Next run's prompt</summary>" + (nextPrompt ? "<pre class='mono'>" + esc(nextPrompt) + "</pre>" : "<p class='muted'>Nothing to run (empty inbox).</p>") + "</details>" +
275
+ // The loop's recent tick outcomes (rolePayload's recentOutcomes, read from the event
276
+ // log), rendered like the Recent ticks list above: each with its relative age, result
277
+ // label, summary, and duration. Mirrors the Recent tick outcomes section of
278
+ // tumwater role <id> — no backticks allowed here (this whole script is one big
279
+ // String.raw template), so the page and the CLI read one loop's outcomes identically.
280
+ (outcomes.length ? "<section><div class='sec-head'><h3>Recent outcomes" +
281
+ "<span class='muted' style='font-size:13px'>· " + outcomes.length + "</span></h3></div>" +
282
+ "<ul class='ticks'>" + outcomes.map((o) => { // vocabulary-ok: the ticks class stays
283
+ const info = resultInfo(o.result);
284
+ const detail = typeof o.summary === "string" && o.summary.trim() !== "" ? o.summary : "";
285
+ return "<li><span class='when' title='" + esc(new Date(Number(o.ts)).toLocaleString()) + "'>" + esc(fmtAgo(Number(o.ts))) + "</span>" +
286
+ "<span class='clamp1' title='" + esc(detail) + "'><span class='res t-" + info.tone + "'>" + esc(info.label) + "</span>" + (detail ? " " + esc(detail) : "") + "</span>" +
287
+ "<span class='when'>" + (o.durationMs == null ? "" : esc(fmtSpan(Number(o.durationMs)))) + "</span></li>";
288
+ }).join("") + "</ul></section>" : "") +
289
+ drawerConfigHtml(view, r.role);
254
290
  }
255
291
  return "<section>" + body + "</section>";
256
292
  }
293
+ // A Configuration section built from the same /api/role payload the CLI's tumwater role
294
+ // <id> renders (rolePayload), so the drawer's and the terminal's rows read one document and
295
+ // cannot drift: what the loop RUNS ON (model/tier, provider cap, image reading, the
296
+ // fallback pairs, billing, min tick interval) and CARRIES (queued prompts, the instructions
297
+ // override, find text). Each row is guarded by a present/own check so an absent field
298
+ // renders no row — a cap-less team shows no Provider row, no fallback shows no fallback
299
+ // rows, and a notes-only payload adds an empty (omitted) section. Labels and wording mirror
300
+ // role-render.ts's family; no backticks allowed here (this whole script is one big
301
+ // String.raw template).
302
+ function drawerConfigHtml(view, role) {
303
+ const pair = (p, m) => [p, m].filter(Boolean).join("/");
304
+ const singleLine = (s) => String(s).replace(/\s+/g, " ").trim();
305
+ // A verbatim value's single-line clamped rendering (80 chars + ellipsis) with the full
306
+ // text riding the title tooltip — the same clamp+tooltip idiom the tick summaries use.
307
+ const clamp = (t) => { const one = singleLine(t); return one.length > 80 ? one.slice(0, 80) + "…" : one; };
308
+ const rows = [];
309
+ // A role that runs on pi's own saved default carries no provider/model but a piDefault
310
+ // selector (rolePayload's resolvedModelFields leaves both undefined), and the CLI's
311
+ // Model line names that default (<agent> default (<selector>), agentDefault). Mirror
312
+ // that branch so the drawer names the model a default pi team actually runs on.
313
+ const defaultLine = typeof view.piDefault === "string"
314
+ ? (view.backend && view.backend !== "pi" ? view.backend : "pi") + " default (" + view.piDefault + ")"
315
+ : "";
316
+ const modelLine = defaultLine !== "" ? defaultLine : pair(view.provider, view.model);
317
+ if (modelLine !== "" || view.modelTier !== undefined) {
318
+ rows.push(kvRow("Model", (modelLine !== "" ? modelLine + " " : "") + (view.modelTier !== undefined ? "(" + view.modelTier + " tier)" : "").trim()));
319
+ }
320
+ // The per-role model triple (roles.<base>.provider/.model/.thinking, the file's own raw
321
+ // overrides rolePayload's providerOverride/modelOverride/thinkingOverride carry) — the
322
+ // one per-role write the Model row above still renders read-only, the dashboard half of
323
+ // the CLI's --provider/--model/--thinking. An always-shown row (every role, the director
324
+ // included, since the CLI writes its triple too) whose body names the payload's raw
325
+ // overrides, or no overrides, and whose control is a model-edit button (Edit/Set model);
326
+ // while the editor is open for this role, three text inputs prefilled with the raw
327
+ // overrides (empty when unset) plus Save/Discard replace the body, mirroring the interval
328
+ // editor's markup.
329
+ const rawTriple = [];
330
+ if (typeof view.providerOverride === "string" && view.providerOverride !== "") rawTriple.push("provider: " + view.providerOverride);
331
+ if (typeof view.modelOverride === "string" && view.modelOverride !== "") rawTriple.push("model: " + view.modelOverride);
332
+ if (typeof view.thinkingOverride === "string" && view.thinkingOverride !== "") rawTriple.push("thinking: " + view.thinkingOverride);
333
+ const modelBody = rawTriple.length ? rawTriple.join(" · ") : "no overrides";
334
+ const editModel = modelEditing && modelEditing.role === role;
335
+ const modelControl = editModel
336
+ ? "<input id='modeleditprovider' type='text' placeholder='provider' value='" + esc(modelEditing.provider) + "'>" +
337
+ "<input id='modeleditmodel' type='text' placeholder='model' value='" + esc(modelEditing.model) + "'>" +
338
+ "<input id='modeleditthinking' type='text' placeholder='thinking' value='" + esc(modelEditing.thinking) + "'>" +
339
+ "<div><button type='button' class='btn btn-sm btn-primary' data-act='model-save' data-arg='" + esc(role) + "'>Save</button> " +
340
+ "<button type='button' class='btn btn-sm' data-act='model-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
341
+ : "<button type='button' class='btn btn-sm' data-act='model-edit' data-arg='" + esc(role) + "'>" + (rawTriple.length ? "Edit" : "Set model") + "</button>";
342
+ rows.push("<div><dt>Model overrides</dt><dd title='" + esc(modelBody) + "'>" + esc(modelBody) + modelControl + "</dd></div>");
343
+ // Context window: the resolved primary seam's declared window - the same seam '- Next run:'
344
+ // previews. Guard optional to keep an older payload that lacks the field rendering as before.
345
+ if (view.contextWindow !== undefined) {
346
+ rows.push(kvRow("Context window", view.contextWindow === null ? "unknown" : fmtTokens(view.contextWindow) + " tokens"));
347
+ }
348
+ if (view.providerKey !== undefined && view.providerCap !== undefined) {
349
+ rows.push(kvRow("Provider", view.providerKey + " (cap " + view.providerCap + ")"));
350
+ }
351
+ if (view.imageInput !== undefined) {
352
+ const images = view.imageInput === true ? "yes"
353
+ : view.imageInput === false ? (view.imageReroute ? "no — reruns on " + view.imageReroute : "no")
354
+ : "unknown";
355
+ rows.push(kvRow("Images", images));
356
+ }
357
+ if (view.modelFallback && typeof view.modelFallback === "object" && view.modelFallback.model !== undefined) {
358
+ const fb = view.modelFallback;
359
+ const when = typeof fb.since === "number" ? " since " + fmtAgo(fb.since) : "";
360
+ rows.push(kvRow("Model fallback", pair(fb.provider, fb.model) + " — on fallback" + when + " (primary failing: " + (fb.reason ?? "") + ")"));
361
+ }
362
+ if (view.fallback && typeof view.fallback === "object" && view.fallback.model !== undefined) {
363
+ rows.push(kvRow("Budget fallback", pair(view.fallback.provider, view.fallback.model) + " (" + (view.fallbackFree ? "free" : "priced") + ")"));
364
+ }
365
+ if (view.billing !== undefined) {
366
+ const window = view.billing === "subscription" && view.billingWindow ? " (" + view.billingWindow + " window)" : "";
367
+ rows.push(kvRow("Billing", (view.billing === "api" ? "API" : view.billing) + window));
368
+ }
369
+ // The min run interval (rolePayload's minRunIntervalSeconds), worded like role-render.ts's
370
+ // Min run interval line, plus a control to set/clear the per-role override — the last
371
+ // per-role hold the drawer edits. Shown whenever the payload names an interval (always —
372
+ // the effective value is the role's override or the team default); the body tags whether
373
+ // the shown value is the role's own override or the team default. While the editor is open
374
+ // for this role, an input prefilled with the override (empty when none) and Save/Discard
375
+ // replace the sentence, mirroring the daily-cap editor's markup; a role with no override
376
+ // still seeds an empty input, so an empty buffer on Save clears.
377
+ if (typeof view.minRunIntervalSeconds === "number") {
378
+ const editing = intervalEditing && intervalEditing.role === role;
379
+ const intervalBody = String(view.minRunIntervalSeconds) + "s" +
380
+ (typeof view.minRunIntervalOverride === "number" ? " (override)" : " (team default)");
381
+ const seed = intervalEditing ? intervalEditing.value : null;
382
+ const intervalControl = editing
383
+ ? "<input id='intervaleditinput' type='number' min='0' step='1' value='" + (seed === "" || seed == null ? "" : String(seed)) + "' placeholder='team default'>" +
384
+ "<div><button type='button' class='btn btn-sm btn-primary' data-act='interval-save' data-arg='" + esc(role) + "'>Save</button> " +
385
+ "<button type='button' class='btn btn-sm' data-act='interval-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
386
+ : "<button type='button' class='btn btn-sm' data-act='interval-edit' data-arg='" + esc(role) + "'>" + (typeof view.minRunIntervalOverride === "number" ? "Edit" : "Set interval") + "</button>";
387
+ rows.push("<div><dt>Min run interval</dt><dd title='" + esc(intervalBody) + "'>" + esc(intervalBody) + intervalControl + "</dd></div>");
388
+ }
389
+ // The parallel-runner count (roles.<base>.instances), worded like role-render.ts's
390
+ // Instances line — the last per-role hold the drawer edits. Rendered only for a role that
391
+ // may split (its base, an instance strip, is feature or bugfix): the count is meaningless
392
+ // for any other role, which role-view never carries. The body reads the payload's numeric
393
+ // count (roles.<base>.instances) tagged (set) or falls back to 1 tagged (default) when the
394
+ // file leaves it unset — role-view carries instances only when the file sets it. While the
395
+ // editor is open for this role, an input prefilled with the count (empty when none) and
396
+ // Save/Discard
397
+ // replace the sentence, mirroring the interval editor's markup; a role with no count still
398
+ // seeds an empty input, so an empty buffer on Save clears.
399
+ const splitBase = role.replace(/-\d+$/, "");
400
+ if (splitBase === "feature" || splitBase === "bugfix") {
401
+ const hasInstances = typeof view.instances === "number";
402
+ const instCount = hasInstances ? view.instances : 1;
403
+ const instBody = String(instCount) + (hasInstances ? " (set)" : " (default)");
404
+ const editingInst = instancesEditing && instancesEditing.role === role;
405
+ const seed = instancesEditing ? instancesEditing.value : null;
406
+ const instControl = editingInst
407
+ ? "<input id='instanceseditinput' type='number' min='1' max='8' step='1' value='" + (seed === "" || seed == null ? "" : String(seed)) + "' placeholder='default 1'>" +
408
+ "<div><button type='button' class='btn btn-sm btn-primary' data-act='instances-save' data-arg='" + esc(role) + "'>Save</button> " +
409
+ "<button type='button' class='btn btn-sm' data-act='instances-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
410
+ : "<button type='button' class='btn btn-sm' data-act='instances-edit' data-arg='" + esc(role) + "'>" + (hasInstances ? "Edit" : "Set instances") + "</button>";
411
+ rows.push("<div><dt>Instances</dt><dd title='" + esc(instBody) + "'>" + esc(instBody) + instControl + "</dd></div>");
412
+ }
413
+ // The windows governing the loop, worded like role-render.ts's Quiet hours line; the end time
414
+ // mirrors quietWindowEnd (browser code cannot import it). A non-director role's own window
415
+ // is an always-shown editable row (the Daily cap row's rationale: the control sits where
416
+ // the hold is shown) — including when the payload carries no role-scope window ("no own
417
+ // window"); any other window stays a read-only row worded exactly as before ("team window").
418
+ const own = Array.isArray(view.quietHours) ? view.quietHours.find(function (w) { return w && w.scope === "role"; }) : undefined;
419
+ const ownWindow = own && typeof own.window === "string" ? own.window : "";
420
+ if (role !== "director") {
421
+ const ownEnd = String(ownWindow).split("-")[1];
422
+ const ownState = own && own.inWindow
423
+ ? "holding now — quiet until " + (ownEnd ? ownEnd.trim() : "")
424
+ : "not holding now";
425
+ const editQuiet = quietEditing && quietEditing.role === role;
426
+ const quietBody = ownWindow !== "" ? ownWindow + " (own window; " + ownState + ")" : "no own window";
427
+ const quietControl = editQuiet
428
+ ? "<input id='quieteditinput' type='text' placeholder='23:00-07:00' value='" + esc(quietEditing.window) + "'>" +
429
+ "<div><button type='button' class='btn btn-sm btn-primary' data-act='quiet-save' data-arg='" + esc(role) + "'>Save</button> " +
430
+ "<button type='button' class='btn btn-sm' data-act='quiet-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
431
+ : "<button type='button' class='btn btn-sm' data-act='quiet-edit' data-arg='" + esc(role) + "'>" + (ownWindow !== "" ? "Edit" : "Set quiet hours") + "</button>";
432
+ rows.push("<div><dt>Quiet hours</dt><dd title='" + esc(quietBody) + "'>" + esc(quietBody) + quietControl + "</dd></div>");
433
+ }
434
+ if (Array.isArray(view.quietHours)) view.quietHours.forEach(function (w) {
435
+ if (!w || w.scope === "role" || typeof w.window !== "string") return;
436
+ const end = String(w.window).split("-")[1];
437
+ const state = w.inWindow ? "holding now — quiet until " + (end ? end.trim() : "") : "not holding now";
438
+ rows.push(kvRow("Quiet hours", w.window + " (team window; " + state + ")"));
439
+ });
440
+ if (typeof view.inboxCount === "number") rows.push(kvRow("Queued prompts", String(view.inboxCount)));
441
+ // The loop's own spend over its retained event window (rolePayload's usage, summed from the
442
+ // same run_end events recentOutcomes reads): how much the loop burned on tokens and cost,
443
+ // with a notional suffix only when a claude-subscription seam burned non-zero notional spend
444
+ // — mirroring role-render.ts's Spend line, so the drawer and tumwater role <id> read one
445
+ // loop's spend identically. Each numeric field is coerced (foreign shapes read as 0) and an
446
+ // all-zero usage is absent on the payload, so this row never renders for a never-costly loop.
447
+ if (view.usage && typeof view.usage === "object") {
448
+ const u = view.usage;
449
+ const real = Number(u.costUsd) || 0;
450
+ const tokens = Number(u.tokens) || 0;
451
+ const notional = Number(u.notionalCostUsd) || 0;
452
+ let spend = fmtUsd(real) + " · " + fmtTokens(tokens) + " tokens";
453
+ if (notional !== 0) spend += " · " + fmtUsd(notional) + " notional";
454
+ rows.push(kvRow("Spend", spend));
455
+ }
456
+ // The per-role daily cap (rolePayload's dailyCap), worded like role-render.ts's Daily cap line.
457
+ // The per-role daily cap (rolePayload's dailyCap), worded like role-render.ts's Daily cap
458
+ // line. Shown for every non-director role — always, cap or not — because the "Set a cap"/
459
+ // "Edit cap" control is the end of the amber role-cap alert's path: the alert's remedy
460
+ // leads here. A director shows no row (role-view.ts's guard exempts it from a cap). While
461
+ // the cap editor is open for this role, an input prefilled with the current cap (empty for
462
+ // none) and Save/Discard replace the sentence, mirroring the notebook editor's markup.
463
+ if (role !== "director") {
464
+ const dc = view.dailyCap && typeof view.dailyCap === "object" ? view.dailyCap : null;
465
+ const editing = capEditing && capEditing.role === role;
466
+ const capBody = dc
467
+ ? fmtUsd(Number(dc.spentUsd) || 0) + " of " + fmtUsd(Number(dc.capUsd) || 0) + " spent today (" + (dc.over ? "holding now — no new runs until local midnight" : "not holding") + ")" +
468
+ (Array.isArray(dc.roleIds) && dc.roleIds.length > 1 ? " · shared by " + dc.roleIds.join(", ") : "")
469
+ : "no cap";
470
+ const capControl = editing
471
+ ? "<input id='capeditinput' type='number' min='0' step='0.01' value='" + (capEditing.value ? String(capEditing.value) : "") + "' placeholder='no cap'>" +
472
+ "<div><button type='button' class='btn btn-sm btn-primary' data-act='cap-save' data-arg='" + esc(role) + "'>Save</button> " +
473
+ "<button type='button' class='btn btn-sm' data-act='cap-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
474
+ : "<button type='button' class='btn btn-sm' data-act='cap-edit' data-arg='" + esc(role) + "'>" + (dc ? "Edit cap" : "Set a cap") + "</button>";
475
+ rows.push("<div><dt>Daily cap</dt><dd title='" + esc(capBody) + "'>" + esc(capBody) + capControl + "</dd></div>");
476
+ }
477
+ // The standing instructions (roles.<base>.instructions, resolved through the base role),
478
+ // worded like role-render.ts's Instructions override. Shown for every role — always,
479
+ // override or not — because the Edit/Save control is how the director's and instance
480
+ // runs' instructions get written from the dashboard: the same always-shown rationale as
481
+ // the Daily cap row above. While the editor is open for this role, a textarea prefilled
482
+ // with the current text and Save/Discard replace the sentence, mirroring the notebook
483
+ // editor's markup; a role with no override still seeds an empty textarea, so an empty
484
+ // buffer on Save clears.
485
+ const instEditing = instructionsEditing && instructionsEditing.role === role;
486
+ const instText = typeof view.instructions === "string" ? view.instructions : "";
487
+ const instBody = instText.trim() !== "" ? clamp(instText) : "No standing instructions.";
488
+ const instControl = instEditing
489
+ ? "<textarea id='instructionsedittext' rows='10' class='mono' style='width:100%'>" + esc(instructionsEditing.text) + "</textarea>" +
490
+ "<div><button type='button' class='btn btn-sm btn-primary' data-act='instructions-save' data-arg='" + esc(role) + "'>Save</button> " +
491
+ "<button type='button' class='btn btn-sm' data-act='instructions-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
492
+ : "<button type='button' class='btn btn-sm' data-act='instructions-edit' data-arg='" + esc(role) + "'>Edit</button>";
493
+ rows.push("<div><dt>Instructions override</dt><dd title='" + esc(instText) + "'>" + esc(instBody) + instControl + "</dd></div>");
494
+ if (typeof view.find === "string" && view.find.trim() !== "") rows.push(kvRow("Find text", clamp(view.find), view.find));
495
+ return rows.length ? "<section><div class='sec-head'><h3>Configuration</h3></div><dl class='kv'>" + rows.join("") + "</dl></section>" : "";
496
+ }
257
497
  // An open notebook edit holds the draft's starting text, not the live payload, so a poll
258
498
  // refetch neither wipes the textarea nor changes the painted panel string. Clear waits for a
259
499
  // confirming click like entry cancel: the first click arms it for 5 s, the second posts.
260
500
  let noteEditing = null; // { role, text }
261
- let confirmNoteClear = null; // { role, until }
501
+ let capEditing = null; // { role, value }
502
+ let instructionsEditing = null; // { role, text }
503
+ let quietEditing = null; // { role, window }
504
+ let intervalEditing = null; // { role, value } — value is the seeded override (number) or "" when none
505
+ let instancesEditing = null; // { role, value } — value is the seeded count (number) or "" when none
506
+ let modelEditing = null; // { role, provider, model, thinking } — the seeded raw overrides ("" when unset)
262
507
  function repaintDrawerRole() { if (lastStatus) renderLoopDrawer(lastStatus); }
263
- function startNoteEdit(role, text) { noteEditing = { role, text }; confirmNoteClear = null; repaintDrawerRole(); }
508
+ // The notebook clear and the backlog entry cancel each build a two-click confirm slot from
509
+ // the shared confirmSlot factory (defined in the loops splice, an earlier one), so the arm,
510
+ // guard, and disarm logic lives once.
511
+ const noteClear = confirmSlot(repaintDrawerRole);
512
+ const noteClearArmed = (role) => noteClear.armed(role);
513
+ function armNoteClear(role) { noteClear.arm(role); }
514
+ function disarmNoteClear() { noteClear.disarm(); }
515
+ function startNoteEdit(role, text) { noteEditing = { role, text }; disarmNoteClear(); repaintDrawerRole(); }
264
516
  function stopNoteEdit() { noteEditing = null; repaintDrawerRole(); }
265
- const noteClearArmed = (role) => Boolean(confirmNoteClear && confirmNoteClear.role === role && confirmNoteClear.until > Date.now());
266
- function disarmNoteClear() { confirmNoteClear = null; }
267
- function armNoteClear(role) {
268
- confirmNoteClear = { role, until: Date.now() + 5000 };
269
- repaintDrawerRole();
270
- setTimeout(() => { if (confirmNoteClear && confirmNoteClear.role === role && confirmNoteClear.until <= Date.now()) { confirmNoteClear = null; repaintDrawerRole(); } }, 5100);
271
- }
517
+ function startCapEdit(role, capUsd) { capEditing = { role, value: capUsd ? Number(capUsd) || 0 : 0 }; repaintDrawerRole(); }
518
+ function stopCapEdit() { capEditing = null; repaintDrawerRole(); }
519
+ function startInstructionsEdit(role, text) { instructionsEditing = { role, text: String(text ?? "") }; repaintDrawerRole(); }
520
+ function stopInstructionsEdit() { instructionsEditing = null; repaintDrawerRole(); }
521
+ function startIntervalEdit(role, value) { intervalEditing = { role, value: value === "" || value == null ? "" : Number(value) }; repaintDrawerRole(); }
522
+ function stopIntervalEdit() { intervalEditing = null; repaintDrawerRole(); }
523
+ function startInstancesEdit(role, value) { instancesEditing = { role, value: value === "" || value == null ? "" : Number(value) }; repaintDrawerRole(); }
524
+ function stopInstancesEdit() { instancesEditing = null; repaintDrawerRole(); }
525
+ function startModelEdit(role, seed) { modelEditing = { role, provider: String((seed && seed.provider) ?? ""), model: String((seed && seed.model) ?? ""), thinking: String((seed && seed.thinking) ?? "") }; repaintDrawerRole(); }
526
+ function stopModelEdit() { modelEditing = null; repaintDrawerRole(); }
527
+ function startQuietEdit(role, window) { quietEditing = { role, window: String(window ?? "") }; repaintDrawerRole(); }
528
+ function stopQuietEdit() { quietEditing = null; repaintDrawerRole(); }
272
529
  // The notebook fetch shares loadDrawerSection; closing the drawer clears drawerRole, so
273
530
  // reopening refetches.
274
531
  function loadDrawerRole(role) {
275
- return loadDrawerSection(role, "/api/role?role=", "Notebook and prompt unavailable", (v) => { drawerRole = v; });
532
+ return loadDrawerSection(role, "/api/role?role=", "Notes and prompt unavailable", (v) => { drawerRole = v; });
276
533
  }
277
534
 
278
535
  async function loadLoopTicks(force) {
@@ -309,7 +566,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
309
566
  const pre = $("transcript");
310
567
  if (openLoopRole() !== role || !pre) return;
311
568
  const lines = Array.isArray(d.lines) ? d.lines : [];
312
- const html = lines.length ? transcriptHtml(lines) : "<span class='muted'>No transcript yet — it fills in once this loop's next tick starts.</span>";
569
+ const html = lines.length ? transcriptHtml(lines) : "<span class='muted'>No transcript yet — it fills in once this role's next run starts.</span>";
313
570
  // Stay pinned to the newest line while the reader is at the bottom; leave them be if they
314
571
  // scrolled up to read.
315
572
  const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 12;
@@ -317,17 +574,13 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
317
574
  }
318
575
 
319
576
  // A plan/bug cancel waits for its confirming click, the confirmAbort shape: the first click
320
- // arms it for 5 s, a second inside the window fires, and a timeout disarms.
321
- let confirmEntryCancel = null; // { key, until }
322
- // The arm records the title too: a list that shifted between the clicks must re-arm, not
323
- // cancel whatever entry now sits at that position.
324
- const entryCancelArmed = (key, title) => Boolean(confirmEntryCancel && confirmEntryCancel.key === key && (title === undefined || confirmEntryCancel.title === title) && confirmEntryCancel.until > Date.now());
325
- function disarmEntryCancel() { confirmEntryCancel = null; }
326
- function armEntryCancel(key, title) {
327
- confirmEntryCancel = { key, title, until: Date.now() + 5000 };
328
- refreshEntry();
329
- setTimeout(() => { if (confirmEntryCancel && confirmEntryCancel.key === key && confirmEntryCancel.until <= Date.now()) { confirmEntryCancel = null; refreshEntry(); } }, 5100);
330
- }
577
+ // arms it for 5 s, a second inside the window fires, and a timeout disarms. The slot records
578
+ // the title too: a list that shifted between the clicks must re-arm, not cancel whatever
579
+ // entry now sits at that position.
580
+ const entryCancel = confirmSlot(refreshEntry);
581
+ const entryCancelArmed = (key, title) => entryCancel.armed(key, title);
582
+ function disarmEntryCancel() { entryCancel.disarm(); }
583
+ function armEntryCancel(key, title) { entryCancel.arm(key, title); }
331
584
  // An open edit form: painted once, so a poll never overwrites the operator's draft; the
332
585
  // server's title check catches an entry that changed underneath it at save time.
333
586
  let entryEditing = null; // { key, title, painted }
@@ -336,7 +589,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
336
589
  refreshEntry();
337
590
  }
338
591
  function stopEntryEdit() { entryEditing = null; }
339
- const ENTRY_KINDS = { plans: "Planned feature", bugs: "Open bug", questions: "Open question" };
592
+ const ENTRY_KINDS = { plans: "Planned feature", bugs: "Open bug", questions: "Open question", done: "Done plan", fixed: "Fixed bug", answered: "Answered question" };
340
593
  async function refreshEntry() {
341
594
  if (!drawer || drawer.kind !== "entry") return;
342
595
  const file = drawer.file;
@@ -363,21 +616,23 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
363
616
  "<textarea id='entryeditbody' rows='14'>" + esc(d.body || "") + "</textarea>");
364
617
  return;
365
618
  }
366
- const action = file === "questions"
619
+ const action = file === "done" || file === "fixed" || file === "answered" ? "" : file === "questions"
367
620
  ? "<textarea id='answertext' rows='3' placeholder='Your answer — recorded in QUESTIONS.md'></textarea>" +
368
621
  "<button type='button' class='btn btn-sm btn-primary' data-act='answer-record' data-arg='" + index + "'>" + icon("chat") + "Record answer</button>" +
369
622
  "<button type='button' class='btn btn-sm' data-act='answer' data-arg='" + index + "'>Ask the director instead</button>"
370
623
  : "<button type='button' class='btn btn-sm' data-act='mention' data-arg='" + esc(file + ":" + index) + "'>" + icon("chat") + "Ask the director about this</button>" +
624
+ (d.refused || d.waiting ? "<button type='button' class='btn btn-sm btn-primary' data-act='entry-release' data-arg='" + esc(file + ":" + index) + "'>Release this entry</button>" : "") +
625
+ (index > 0 ? "<button type='button' class='btn btn-sm' data-act='entry-top' data-arg='" + esc(file + ":" + index) + "'>Move to top</button>" : "") +
371
626
  "<button type='button' class='btn btn-sm' data-act='entry-edit' data-arg='" + esc(file + ":" + index) + "'>Edit this entry</button>" +
372
627
  "<button type='button' class='btn btn-sm btn-danger' data-act='entry-cancel' data-arg='" + esc(file + ":" + index) + "'>" + (entryCancelArmed(file + ":" + index) ? "Click again to cancel" : "Cancel this entry") + "</button>";
373
628
  // The body is model-written Markdown: renderMarkdown escapes every piece of it.
374
- paintPanel("entrybody", "<div class='drawer-actions'>" + action + "</div><div class='md'>" +
629
+ paintPanel("entrybody", (action ? "<div class='drawer-actions'>" + action + "</div>" : "") + "<div class='md'>" +
375
630
  (d.body ? renderMarkdown(d.body) : "<p class='muted'>No details for this entry.</p>") + "</div>");
376
631
  }
377
632
 
378
633
  async function refreshDrawer() {
379
634
  if (!drawer) return;
380
- if (drawer.kind === "loop") {
635
+ if (drawer.kind === "loop") { // vocabulary-ok: the drawer kind stays "loop"
381
636
  if (lastStatus) renderLoopDrawer(lastStatus);
382
637
  await refreshTranscript(false);
383
638
  } else {