tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -29,97 +29,107 @@ export function createRoleEnabledEditor(session) {
29
29
  session.held.roleEnabled = false;
30
30
  restoreDraft();
31
31
  };
32
- const handleKey = (_str, key, ctx) => {
33
- if (key.ctrl && key.name === "v") {
34
- // Ctrl+V toggles enable/disable-edit mode on the Health view's prompt line (the same key
35
- // the Activity attach, Pending top, History revert, Inbox top, and Transcript
36
- // reset-counters arms use on their own views, guarded away here by the Health index). The
37
- // line edits one role id; Enter toggles that role's `roles.<base>.enabled` through the
38
- // same setConfigKey core the CLI, the Settings card and the drawer use. Entering saves
39
- // the prompt draft and blanks the line; leaving restores it. While open, Ctrl+V again
40
- // exits.
41
- if (roleEnabledMode) {
42
- exitRoleEnabledMode();
43
- return true;
44
- }
45
- if (ctx.view !== ctx.roleIds.length + 7)
46
- return false; // off Health, keep the key's other meanings
47
- if (session.editorEntryDenied("roleEnabled"))
48
- return true;
49
- restoreDraft = session.takeDraft();
50
- roleEnabledMode = true;
51
- session.held.roleEnabled = true;
52
- session.input = "";
53
- session.cursor = 0;
54
- session.flashMessage("enable or disable a role: type the role id, Enter to toggle, Esc to cancel");
32
+ // Ctrl+V toggles enable/disable-edit mode on the Health view's prompt line (the same key
33
+ // the Activity attach, Pending top, History revert, Inbox top, and Transcript
34
+ // reset-counters arms use on their own views, guarded away here by the Health index). The
35
+ // line edits one role id; Enter toggles that role's `roles.<base>.enabled` through the
36
+ // same setConfigKey core the CLI, the Settings card and the drawer use. Entering saves
37
+ // the prompt draft and blanks the line; leaving restores it. While open, Ctrl+V again
38
+ // exits.
39
+ const roleEnabledKey = (ctx) => {
40
+ if (roleEnabledMode) {
41
+ exitRoleEnabledMode();
55
42
  return true;
56
43
  }
57
- if (key.name === "escape" && roleEnabledMode) {
58
- // Esc cancels enable/disable-edit mode, restoring the previous prompt text and toggling
59
- // nothing.
60
- exitRoleEnabledMode();
61
- session.flashMessage("enable/disable cancelled");
44
+ if (ctx.view !== ctx.roleIds.length + 7)
45
+ return false; // off Health, keep the key's other meanings
46
+ if (session.editorEntryDenied("roleEnabled"))
47
+ return true;
48
+ restoreDraft = session.takeDraft();
49
+ roleEnabledMode = true;
50
+ session.held.roleEnabled = true;
51
+ session.input = "";
52
+ session.cursor = 0;
53
+ session.flashMessage("enable or disable a role: type the role id, Enter to toggle, Esc to cancel");
54
+ return true;
55
+ };
56
+ // Esc cancels enable/disable-edit mode, restoring the previous prompt text and toggling
57
+ // nothing.
58
+ const cancelRoleEnabledEdit = () => {
59
+ exitRoleEnabledMode();
60
+ session.flashMessage("enable/disable cancelled");
61
+ return true;
62
+ };
63
+ // Enter toggles the typed role's base role's `roles.<base>.enabled` — the same core, and
64
+ // the same director/safety rules, the CLI's `tumwater role <id> --enable|--disable`, the
65
+ // dashboard Settings card's Enable and the role drawer's two-click Disable use. A
66
+ // config-disabled role is enabled at once; an enabled one is armed (this is the first of
67
+ // the drawer's two clicks) and a second Enter on the same base disables it. An empty id,
68
+ // an unknown id, a director disable, or a failed write flashes and STAYS in the editor so the
69
+ // operator can fix the id.
70
+ const commitRoleEnabledEntry = () => {
71
+ const role = session.input.trim();
72
+ if (role === "") {
73
+ session.flashMessage("a role id is required");
62
74
  return true;
63
75
  }
64
- if (key.name === "return" && roleEnabledMode) {
65
- // Enter toggles the typed role's base role's `roles.<base>.enabled` — the same core, and
66
- // the same director/safety rules, the CLI's `tumwater role <id> --enable|--disable`, the
67
- // dashboard Settings card's Enable and the role drawer's two-click Disable use. A
68
- // config-disabled role is enabled at once; an enabled one is armed (this is the first of
69
- // the drawer's two clicks) and a second Enter on the same base disables it. An empty id,
70
- // an unknown id, a director disable, or a failed write flashes and STAYS in the editor so the
71
- // operator can fix the id.
72
- const role = session.input.trim();
73
- if (role === "") {
74
- session.flashMessage("a role id is required");
75
- return true;
76
- }
77
- const cfg = loadConfigSafe(session.root).config ?? defaultConfig();
78
- const validIds = knownRoleIds(cfg);
79
- if (!validIds.includes(role)) {
80
- session.flashMessage(unknownRoleMessage(role, validIds));
81
- return true;
82
- }
83
- const base = baseRoleOf(role);
84
- const enabled = cfg.roles[base]?.enabled !== false;
85
- if (base === DIRECTOR_ROLE && enabled) {
86
- // Only disabling is refused, matching the CLI (`--disable` fails for the director, while
87
- // `--enable` stays allowed) and the Settings card. The director is always enabled (the
88
- // CLI and this editor never write it disabled), so an enabled director is refused before
89
- // the disable arm is offered; a config-disabled director would still enable at once below.
90
- session.flashMessage("the director cannot be disabled");
91
- return true;
92
- }
93
- if (!enabled) {
94
- // A config-disabled role: enable it at once (the Settings card's Enable and the CLI's
95
- // --enable both enable directly, with no confirm step).
96
- const result = setConfigKey(session.root, `roles.${base}.enabled`, "true");
97
- if (!result.ok) {
98
- session.flashMessage(result.error);
99
- return true;
100
- }
101
- exitRoleEnabledMode();
102
- session.flashMessage(`${base}: enabled`);
103
- return true;
104
- }
105
- // An enabled role: the drawer's two-click Disable — the first Enter arms, a second Enter
106
- // on the same base writes `false`. The confirm is keyed to the base the line currently
107
- // names, so an id changed — or a key that edits it — between the two Enters re-arms
108
- // rather than disabling what the line no longer shows.
109
- if (confirmBase !== base) {
110
- confirmBase = base;
111
- session.flashMessage(`press Enter again to disable ${base} — any other role id or Esc keeps it enabled`);
112
- return true;
113
- }
114
- const result = setConfigKey(session.root, `roles.${base}.enabled`, "false");
76
+ const cfg = loadConfigSafe(session.root).config ?? defaultConfig();
77
+ const validIds = knownRoleIds(cfg);
78
+ if (!validIds.includes(role)) {
79
+ session.flashMessage(unknownRoleMessage(role, validIds));
80
+ return true;
81
+ }
82
+ const base = baseRoleOf(role);
83
+ const enabled = cfg.roles[base]?.enabled !== false;
84
+ if (base === DIRECTOR_ROLE && enabled) {
85
+ // Only disabling is refused, matching the CLI (`--disable` fails for the director, while
86
+ // `--enable` stays allowed) and the Settings card. The director is always enabled (the
87
+ // CLI and this editor never write it disabled), so an enabled director is refused before
88
+ // the disable arm is offered; a config-disabled director would still enable at once below.
89
+ session.flashMessage("the director cannot be disabled");
90
+ return true;
91
+ }
92
+ if (!enabled) {
93
+ // A config-disabled role: enable it at once (the Settings card's Enable and the CLI's
94
+ // --enable both enable directly, with no confirm step).
95
+ const result = setConfigKey(session.root, `roles.${base}.enabled`, "true");
115
96
  if (!result.ok) {
116
97
  session.flashMessage(result.error);
117
98
  return true;
118
99
  }
119
100
  exitRoleEnabledMode();
120
- session.flashMessage(`${base}: disabled`);
101
+ flashRoleEnabled(base, true);
121
102
  return true;
122
103
  }
104
+ // An enabled role: the drawer's two-click Disable — the first Enter arms, a second Enter
105
+ // on the same base writes `false`. The confirm is keyed to the base the line currently
106
+ // names, so an id changed — or a key that edits it — between the two Enters re-arms
107
+ // rather than disabling what the line no longer shows.
108
+ if (confirmBase !== base) {
109
+ confirmBase = base;
110
+ session.flashMessage(`press Enter again to disable ${base} — any other role id or Esc keeps it enabled`);
111
+ return true;
112
+ }
113
+ const result = setConfigKey(session.root, `roles.${base}.enabled`, "false");
114
+ if (!result.ok) {
115
+ session.flashMessage(result.error);
116
+ return true;
117
+ }
118
+ exitRoleEnabledMode();
119
+ flashRoleEnabled(base, false);
120
+ return true;
121
+ };
122
+ // The Enter success flash: names the base and which way it was toggled.
123
+ const flashRoleEnabled = (base, enabled) => {
124
+ session.flashMessage(enabled ? `${base}: enabled` : `${base}: disabled`);
125
+ };
126
+ const handleKey = (_str, key, ctx) => {
127
+ if (key.ctrl && key.name === "v")
128
+ return roleEnabledKey(ctx);
129
+ if (key.name === "escape" && roleEnabledMode)
130
+ return cancelRoleEnabledEdit();
131
+ if (key.name === "return" && roleEnabledMode)
132
+ return commitRoleEnabledEntry();
123
133
  return false;
124
134
  };
125
135
  return { handleKey, exit: exitRoleEnabledMode };
@@ -0,0 +1,87 @@
1
+ import { viewedRole } from "./tui-editor-ctx.js";
2
+ import { readEditorPath } from "./tui-editor-session.js";
3
+ /** Build a role-located file-read editor. It takes the line only on a role's Transcript /
4
+ * notes view (the caller's mayOpen guard) and only while no other editor holds the line, reads
5
+ * the typed path whole, and writes through the caller's shared setter. A refused write flashes
6
+ * and STAYS in edit mode so the operator can fix the path; success repaints the notes and
7
+ * restores the saved prompt text. */
8
+ export function createRoleFileEditor(session, spec) {
9
+ // The role pinned when the editor opened, or null when closed.
10
+ let editFor = null;
11
+ let restoreDraft = () => { };
12
+ // Leave the editor (Esc, the key again, or a successful Enter): restore the saved prompt
13
+ // text with no write.
14
+ const exit = () => {
15
+ if (editFor === null)
16
+ return;
17
+ editFor = null;
18
+ session.held[spec.held] = null;
19
+ restoreDraft();
20
+ };
21
+ // The Ctrl+<openCtrl> open/toggle arm: toggles the editor out while open; otherwise opens on
22
+ // the guarded view and elsewhere keeps its own meaning and falls through to the pane arms.
23
+ const openRoleFileEdit = (ctx) => {
24
+ // A view/state guard miss falls through so the key keeps its own meaning on the pane arms.
25
+ if (!spec.mayOpen(ctx))
26
+ return false;
27
+ // The key toggles the editor out while open; otherwise it opens on the guarded view and
28
+ // elsewhere keeps its own meaning and falls through to the pane arms.
29
+ if (session.editorEntryDenied(spec.held))
30
+ return true;
31
+ if (editFor !== null) {
32
+ exit();
33
+ }
34
+ else {
35
+ // Mid-recall, hand the arrows' saved draft back before the editor takes the line — the
36
+ // same settle the role editors do — so leaving restores the draft, not a history entry,
37
+ // with no stale recall state riding along.
38
+ const role = viewedRole(ctx); // defined: the open-arm range guard above pins the view into roleIds
39
+ restoreDraft = session.takeDraft();
40
+ editFor = role;
41
+ session.held[spec.held] = role;
42
+ session.input = "";
43
+ session.cursor = 0;
44
+ session.flashMessage(spec.openFlash(role));
45
+ }
46
+ return true;
47
+ };
48
+ // Esc cancels the editor, restoring the previous prompt text and writing nothing.
49
+ const cancelRoleFileEdit = () => {
50
+ exit();
51
+ session.flashMessage(spec.cancelFlash);
52
+ return true;
53
+ };
54
+ // Enter reads the typed path whole and commits it through the caller's shared setter — the
55
+ // same setConfigKey/saveRoleNote write the CLI's non-interactive replacements and the
56
+ // dashboard drawer make, so the caps and their wording cannot drift. A missing or unreadable
57
+ // path (readEditorPath) or a refused write flashes and STAYS in the editor so the operator
58
+ // can fix the path; success repaints the notes and exits with the shared success wording.
59
+ const submitRoleFileEdit = () => {
60
+ const role = editFor;
61
+ if (role === null)
62
+ return true;
63
+ const text = readEditorPath(session);
64
+ if (text === null)
65
+ return true;
66
+ const outcome = spec.commit(session, role, text);
67
+ if (outcome.ok) {
68
+ spec.refresh(session, role);
69
+ exit();
70
+ session.flashMessage(outcome.message);
71
+ }
72
+ else {
73
+ spec.flashFailure(session, outcome.reason);
74
+ }
75
+ return true;
76
+ };
77
+ const handleKey = (_str, key, ctx) => {
78
+ if (key.ctrl && key.name === spec.openCtrl)
79
+ return openRoleFileEdit(ctx);
80
+ if (key.name === "escape" && editFor !== null)
81
+ return cancelRoleFileEdit();
82
+ if (key.name === "return" && editFor !== null)
83
+ return submitRoleFileEdit();
84
+ return false;
85
+ };
86
+ return { handleKey, exit };
87
+ }
@@ -1,4 +1,5 @@
1
1
  import { baseRoleOf } from "../../roles/runner-ids.js";
2
+ import { viewedRole } from "./tui-editor-ctx.js";
2
3
  /** Build a role-pinned editor. It may take the line only on a role's Transcript view
3
4
  * (view 1..roleIds.length) or when it already holds it (the toggle-out arm), and only while no
4
5
  * other editor holds the line. An invalid value flashes and STAYS in edit mode so the operator
@@ -18,53 +19,60 @@ export function createRoleKeyEditor(session, spec) {
18
19
  restoreDraft();
19
20
  lockedRole = null;
20
21
  };
21
- const handleKey = (_str, key, ctx) => {
22
- if (key.ctrl && key.name === spec.openCtrl) {
23
- // While open the key toggles the editor out. Otherwise it opens only on a role's
24
- // Transcript view; elsewhere it keeps its own meaning and falls through to the pane arms.
25
- if (mode) {
26
- exit();
27
- return true;
28
- }
29
- if (!(ctx.view >= 1 && ctx.view <= ctx.roleIds.length) || spec.yieldsOn?.(ctx))
30
- return false;
31
- const role = ctx.roleIds[ctx.view - 1];
32
- if (session.editorEntryDenied(spec.mode))
33
- return true;
34
- // Pin the role first so the write below is safe against a mid-edit roster shift, then
35
- // pre-fill from the file's own entry — one fresh read per open is the smallest honest
36
- // source; a broken config degrades to no pre-fill and the write reports the error.
37
- lockedRole = role;
38
- const existing = spec.prefill(session, baseRoleOf(role));
39
- restoreDraft = session.takeDraft();
40
- mode = true;
41
- session.held[spec.mode] = true;
42
- session.input = existing;
43
- session.cursor = session.input.length;
44
- session.flashMessage(spec.openFlash(role));
45
- return true;
46
- }
47
- if (key.name === "escape" && mode) {
48
- // Esc cancels the editor, restoring the previous prompt text.
22
+ // The Ctrl+<key> arm: while open it toggles the editor out. Otherwise it opens only on a
23
+ // role's Transcript view; elsewhere it keeps its own meaning and falls through to the pane
24
+ // arms.
25
+ const openRoleKeyEdit = (ctx) => {
26
+ if (mode) {
49
27
  exit();
50
28
  return true;
51
29
  }
52
- if (key.name === "return" && mode) {
53
- // Enter commits the whitespace-trimmed line through the spec's shared setter. A rejected
54
- // value flashes and STAYS in edit mode so the operator can fix it; success restores the
55
- // saved prompt text.
56
- const role = lockedRole;
57
- const text = session.input.trim();
58
- const result = spec.commit(session, baseRoleOf(role), text);
59
- if (result.ok) {
60
- exit();
61
- session.flashMessage(result.noop && spec.noopFlash ? spec.noopFlash(role, text) : spec.savedFlash(role, text));
62
- }
63
- else {
64
- session.flashMessage(result.error); // broken config or write failure — stay in edit mode
65
- }
30
+ if (!(ctx.view >= 1 && ctx.view <= ctx.roleIds.length) || spec.yieldsOn?.(ctx))
31
+ return false;
32
+ const role = viewedRole(ctx);
33
+ if (session.editorEntryDenied(spec.mode))
66
34
  return true;
35
+ // Pin the role first so the write below is safe against a mid-edit roster shift, then
36
+ // pre-fill from the file's own entry — one fresh read per open is the smallest honest
37
+ // source; a broken config degrades to no pre-fill and the write reports the error.
38
+ lockedRole = role;
39
+ const existing = spec.prefill(session, baseRoleOf(role));
40
+ restoreDraft = session.takeDraft();
41
+ mode = true;
42
+ session.held[spec.mode] = true;
43
+ session.input = existing;
44
+ session.cursor = session.input.length;
45
+ session.flashMessage(spec.openFlash(role));
46
+ return true;
47
+ };
48
+ // Esc cancels the editor, restoring the previous prompt text.
49
+ const cancelRoleKeyEdit = () => {
50
+ exit();
51
+ return true;
52
+ };
53
+ // Enter commits the whitespace-trimmed line through the spec's shared setter. A rejected
54
+ // value flashes and STAYS in edit mode so the operator can fix it; success restores the
55
+ // saved prompt text.
56
+ const submitRoleKeyEdit = () => {
57
+ const role = lockedRole;
58
+ const text = session.input.trim();
59
+ const result = spec.commit(session, baseRoleOf(role), text);
60
+ if (result.ok) {
61
+ exit();
62
+ session.flashMessage(result.noop && spec.noopFlash ? spec.noopFlash(role, text) : spec.savedFlash(role, text));
63
+ }
64
+ else {
65
+ session.flashMessage(result.error); // broken config or write failure — stay in edit mode
67
66
  }
67
+ return true;
68
+ };
69
+ const handleKey = (_str, key, ctx) => {
70
+ if (key.ctrl && key.name === spec.openCtrl)
71
+ return openRoleKeyEdit(ctx);
72
+ if (key.name === "escape" && mode)
73
+ return cancelRoleKeyEdit();
74
+ if (key.name === "return" && mode)
75
+ return submitRoleKeyEdit();
68
76
  return false;
69
77
  };
70
78
  return { handleKey, exit };
@@ -20,65 +20,77 @@ export function createSearchEditor(session) {
20
20
  session.held.search = false;
21
21
  restoreDraft();
22
22
  };
23
+ // `/` opens a free-text search on the prompt line (the pager's search key; this project
24
+ // already reserves `/` for a meta action): over the History pane's run rows on History,
25
+ // over the Activity feed's events on Activity, and over the Backlog pane's active entries
26
+ // on Backlog, over the Inbox pane's queued prompts, and over a role's live transcript.
27
+ // Guarded to those views and to an empty line, so elsewhere it types into the prompt as
28
+ // today and a director draft is never disturbed. Pre-fills the applied pattern so Enter
29
+ // re-submits it and the operator can edit it; Esc cancels without applying.
30
+ const openSearchEdit = (ctx) => {
31
+ if (searchMode || session.anyEditorHeld() || session.input !== "" ||
32
+ !(ctx.view === 0 || ctx.view === ctx.roleIds.length + 1 || ctx.view === ctx.roleIds.length + 4 || ctx.view === ctx.roleIds.length + 6 ||
33
+ (ctx.view >= 1 && ctx.view <= ctx.roleIds.length && !ctx.noteMode && !ctx.changeMode)))
34
+ return false;
35
+ restoreDraft = session.takeDraft();
36
+ if (ctx.view === 0)
37
+ searchFor = "activity";
38
+ else if (ctx.view === ctx.roleIds.length + 1)
39
+ searchFor = "backlog";
40
+ else if (ctx.view === ctx.roleIds.length + 4)
41
+ searchFor = "history";
42
+ else if (ctx.view === ctx.roleIds.length + 6)
43
+ searchFor = "queued";
44
+ else
45
+ searchFor = "transcript";
46
+ searchMode = true;
47
+ session.held.search = true;
48
+ const applied = { activity: ctx.activityGrep, history: ctx.trailSearch ? ctx.trailGrep : ctx.historyGrep, backlog: ctx.backlogGrep, queued: ctx.queuedGrep, transcript: ctx.transcriptGrep }[searchFor];
49
+ const noun = { activity: "events", history: ctx.trailSearch ? "transcript" : "runs", backlog: "backlog", queued: "inbox", transcript: "transcript" }[searchFor];
50
+ session.input = applied ?? "";
51
+ session.cursor = session.input.length;
52
+ session.flashMessage(`search ${noun}: Enter to filter, Esc to cancel`);
53
+ return true;
54
+ };
55
+ // Esc cancels the History search without applying it; the applied pattern is unchanged.
56
+ const cancelSearchEdit = () => {
57
+ if (!searchMode)
58
+ return false;
59
+ exitHistorySearch();
60
+ return true;
61
+ };
62
+ // Enter applies the trimmed line to the pane the editor opened over: Activity sets
63
+ // activityGrep (an empty line clears it) and resets the scroll to the live tail — the
64
+ // offset indexed the old filtered list; History recomputes its row cache now (the pane
65
+ // is showing) and re-enters at its head; Backlog sets backlogGrep and clears its entry
66
+ // selection so the next frame starts in list mode; Inbox sets queuedGrep, clears its row
67
+ // selection, and re-reads the filtered rows. A successful apply restores the saved
68
+ // prompt draft through exitHistorySearch, the shared editor's exit.
69
+ const submitSearchEdit = () => {
70
+ if (!searchMode)
71
+ return false;
72
+ const applied = session.input.trim() === "" ? null : session.input.trim();
73
+ if (searchFor === "activity")
74
+ session.applyActivitySearch(applied);
75
+ else if (searchFor === "history")
76
+ session.applyHistorySearch(applied);
77
+ else if (searchFor === "backlog")
78
+ session.applyBacklogSearch(applied);
79
+ else if (searchFor === "queued")
80
+ session.applyQueuedSearch(applied);
81
+ else
82
+ session.applyTranscriptSearch(applied);
83
+ exitHistorySearch();
84
+ session.flashMessage(applied === null ? "search cleared" : `search: "${applied}"`);
85
+ return true;
86
+ };
23
87
  const handleKey = (str, key, ctx) => {
24
- if (!key.ctrl && str === "/" && !searchMode && !session.anyEditorHeld() &&
25
- (ctx.view === 0 || ctx.view === ctx.roleIds.length + 1 || ctx.view === ctx.roleIds.length + 4 || ctx.view === ctx.roleIds.length + 6 ||
26
- (ctx.view >= 1 && ctx.view <= ctx.roleIds.length && !ctx.noteMode && !ctx.changeMode)) && session.input === "") {
27
- // `/` opens a free-text search on the prompt line (the pager's search key; this project
28
- // already reserves `/` for a meta action): over the History pane's run rows on History,
29
- // over the Activity feed's events on Activity, and over the Backlog pane's active entries
30
- // on Backlog, over the Inbox pane's queued prompts, and over a role's live transcript.
31
- // Guarded to those views and to an empty line, so elsewhere it types into the prompt as
32
- // today and a director draft is never disturbed. Pre-fills the applied pattern so Enter
33
- // re-submits it and the operator can edit it; Esc cancels without applying.
34
- restoreDraft = session.takeDraft();
35
- if (ctx.view === 0)
36
- searchFor = "activity";
37
- else if (ctx.view === ctx.roleIds.length + 1)
38
- searchFor = "backlog";
39
- else if (ctx.view === ctx.roleIds.length + 4)
40
- searchFor = "history";
41
- else if (ctx.view === ctx.roleIds.length + 6)
42
- searchFor = "queued";
43
- else
44
- searchFor = "transcript";
45
- searchMode = true;
46
- session.held.search = true;
47
- const applied = { activity: ctx.activityGrep, history: ctx.trailSearch ? ctx.trailGrep : ctx.historyGrep, backlog: ctx.backlogGrep, queued: ctx.queuedGrep, transcript: ctx.transcriptGrep }[searchFor];
48
- const noun = { activity: "events", history: ctx.trailSearch ? "transcript" : "runs", backlog: "backlog", queued: "inbox", transcript: "transcript" }[searchFor];
49
- session.input = applied ?? "";
50
- session.cursor = session.input.length;
51
- session.flashMessage(`search ${noun}: Enter to filter, Esc to cancel`);
52
- return true;
53
- }
54
- if (key.name === "escape" && searchMode) {
55
- // Esc cancels the History search without applying it; the applied pattern is unchanged.
56
- exitHistorySearch();
57
- return true;
58
- }
59
- if (key.name === "return" && searchMode) {
60
- // Enter applies the trimmed line to the pane the editor opened over: Activity sets
61
- // activityGrep (an empty line clears it) and resets the scroll to the live tail — the
62
- // offset indexed the old filtered list; History recomputes its row cache now (the pane
63
- // is showing) and re-enters at its head; Backlog sets backlogGrep and clears its entry
64
- // selection so the next frame starts in list mode; Inbox sets queuedGrep, clears its row
65
- // selection, and re-reads the filtered rows. A successful apply restores the saved
66
- // prompt draft through exitHistorySearch, the shared editor's exit.
67
- const applied = session.input.trim() === "" ? null : session.input.trim();
68
- if (searchFor === "activity")
69
- session.applyActivitySearch(applied);
70
- else if (searchFor === "history")
71
- session.applyHistorySearch(applied);
72
- else if (searchFor === "backlog")
73
- session.applyBacklogSearch(applied);
74
- else if (searchFor === "queued")
75
- session.applyQueuedSearch(applied);
76
- else
77
- session.applyTranscriptSearch(applied);
78
- exitHistorySearch();
79
- session.flashMessage(applied === null ? "search cleared" : `search: "${applied}"`);
80
- return true;
81
- }
88
+ if (!key.ctrl && str === "/")
89
+ return openSearchEdit(ctx);
90
+ if (key.name === "escape" && searchMode)
91
+ return cancelSearchEdit();
92
+ if (key.name === "return" && searchMode)
93
+ return submitSearchEdit();
82
94
  return false;
83
95
  };
84
96
  return { handleKey, exit: exitHistorySearch };
@@ -7,6 +7,7 @@ const HOLDER_SUBJECTS = [
7
7
  ["notes", "notes edit"],
8
8
  ["brief", "brief edit"],
9
9
  ["retire", "retire role"],
10
+ ["release", "release test"],
10
11
  ["roleEnabled", "role state"],
11
12
  ["quiet", "quiet hours"],
12
13
  ["model", "model edit"],
@@ -48,6 +49,7 @@ export function createTuiEditorSession(deps) {
48
49
  notes: null,
49
50
  brief: false,
50
51
  retire: false,
52
+ release: false,
51
53
  roleEnabled: false,
52
54
  quiet: false,
53
55
  quietTeam: false,
@@ -72,6 +74,7 @@ export function createTuiEditorSession(deps) {
72
74
  refreshQueued: deps.refreshQueued,
73
75
  refreshNotes: deps.refreshNotes,
74
76
  refreshBrief: deps.refreshBrief,
77
+ refreshReport: deps.refreshReport,
75
78
  clearBacklogSelection: deps.clearBacklogSelection,
76
79
  applyHistorySearch: deps.applyHistorySearch,
77
80
  applyActivitySearch: deps.applyActivitySearch,
@@ -22,6 +22,7 @@ import { createAttachEditor } from "./tui-editor-attach.js";
22
22
  import { createNotesEditor } from "./tui-editor-notes.js";
23
23
  import { createBriefEditor } from "./tui-editor-brief.js";
24
24
  import { createRetireEditor } from "./tui-editor-retire.js";
25
+ import { createQuarantineReleaseEditor } from "./tui-editor-release.js";
25
26
  import { createRoleEnabledEditor } from "./tui-editor-role-enabled.js";
26
27
  import { createPromptEditor } from "./tui-editor-prompt.js";
27
28
  import { createPauseEditor } from "./tui-editor-pause.js";
@@ -70,6 +71,7 @@ export function createTuiEditors(deps) {
70
71
  createSearchEditor(session),
71
72
  createBriefEditor(session),
72
73
  createRetireEditor(session),
74
+ createQuarantineReleaseEditor(session),
73
75
  createRoleEnabledEditor(session),
74
76
  ];
75
77
  const handleKey = (str, key, ctx) => {
@@ -120,7 +122,7 @@ export function createTuiEditors(deps) {
120
122
  // above already returned), and budget, pause and search modes keep them out —
121
123
  // recalling a prompt into the cap/field text would be a paste, not a recall. No
122
124
  // history in this direction: fall through to applyKey, which ignores arrows as before.
123
- if (!session.held.budget && !session.held.concurrency && !session.held.checks && !session.held.check && !session.held.slots && !session.held.model && !session.held.quiet && !session.held.interval && !session.held.roleModel && !session.held.instances && !session.held.instructions && !session.held.attach && !session.held.pause && !session.held.search && !session.held.notes && !session.held.brief && !session.held.retire && !session.held.roleEnabled && !session.held.providerCaps && !session.held.notify && !session.held.backends && !session.held.fallback && !session.held.claudeReserve) {
125
+ if (!session.held.budget && !session.held.concurrency && !session.held.checks && !session.held.check && !session.held.slots && !session.held.model && !session.held.quiet && !session.held.interval && !session.held.roleModel && !session.held.instances && !session.held.instructions && !session.held.attach && !session.held.pause && !session.held.search && !session.held.notes && !session.held.brief && !session.held.retire && !session.held.release && !session.held.roleEnabled && !session.held.providerCaps && !session.held.notify && !session.held.backends && !session.held.fallback && !session.held.claudeReserve) {
124
126
  const recalled = recallPromptHistory(session.promptHistory, session.input, session.cursor, direction === "up" ? "older" : "newer");
125
127
  if (recalled) {
126
128
  session.promptHistory = recalled.history;
@@ -175,6 +177,7 @@ export function createTuiEditors(deps) {
175
177
  notesEditFor: session.held.notes,
176
178
  briefEditMode: session.held.brief,
177
179
  retireMode: session.held.retire,
180
+ releaseMode: session.held.release,
178
181
  roleEnabledMode: session.held.roleEnabled,
179
182
  quietMode: session.held.quiet,
180
183
  quietTeam: session.held.quietTeam,
@@ -65,6 +65,8 @@ export function hintLine(view, mode, width) {
65
65
  keys = [["Enter", "replace notes from file"], ["Esc", "cancel"], ["Ctrl+D", "quit"]];
66
66
  else if (mode.retireMode)
67
67
  keys = [["Enter", "retire role"], ["Esc", "cancel"], ["Ctrl+D", "quit"]];
68
+ else if (mode.releaseMode)
69
+ keys = [["Enter", "release test"], ["Esc", "cancel"], ["Ctrl+D", "quit"]];
68
70
  else if (mode.roleEnabledMode)
69
71
  keys = [["Enter", "enable/disable role"], ["Esc", "cancel"], ["Ctrl+D", "quit"]];
70
72
  else if (mode.budget)
@@ -111,19 +113,19 @@ export function hintLine(view, mode, width) {
111
113
  else if (view.kind === "backlog")
112
114
  keys = [["Enter", "send"], ["↑↓", "open entries"], ["/", "search entries"], ["Ctrl+O", "ready only"], ["Ctrl+X", "cancel entry"], ["Ctrl+R", "release entry"], ["Ctrl+V", "top entry"], ["Ctrl+E", "edit entry"], ["Ctrl+N", "new plan/bug"], ["Ctrl+A", "answer question"], ["PgUp/PgDn", "scroll"], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
113
115
  else if (view.kind === "usage" || view.kind === "failures")
114
- keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+G", view.scope ? `scope: ${view.scope}` : "scope: team"], ["Ctrl+E", view.window ? `window: ${view.window}` : "window: default"], ...(view.window ? [["Ctrl+P", "earlier window"]] : []), ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
116
+ keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+G", view.scope ? `scope: ${view.scope}` : "scope: team"], ["Ctrl+E", view.window ? `window: ${view.window}` : "window: default"], ...(view.window ? [["Ctrl+P", "earlier window"]] : []), ...(view.kind === "failures" ? [["Ctrl+X", "release test"]] : []), ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
115
117
  else if (view.kind === "history")
116
118
  keys = [["Enter", "send"], ["↑↓", "history"], ["Ctrl+L", "newest failure"], ["PgUp/PgDn", "scroll"], ["Ctrl+G", view.role ? `scope: ${view.role}` : "scope: team"], ["Ctrl+S", view.since ? `window: ${view.since}` : "window: default"], ...(!view.trail && view.since ? [["Ctrl+P", "earlier window"]] : []), ["Ctrl+O", view.result ? `result: ${view.result}` : "result: all"], ["Ctrl+X", view.model ? `model: ${view.model}` : "model: all"], ["Ctrl+Y", view.summary === "model" ? "rows" : view.summary === "role" ? "by model" : "summary"], ["Ctrl+N", view.trail ? "history" : "run trail"], ...(view.trail ? [["Ctrl+P", "older run"], ["Ctrl+R", view.trailTranscript === "plain" ? "prompts" : view.trailTranscript === "prompts" ? "hide transcript" : "transcript"], ["Ctrl+W", view.trailPatch ? "hide patch" : "patch"], ["Ctrl+V", "revert run"]] : []), ["/", view.trail && view.trailTranscript !== undefined && view.trailTranscript !== "off" ? (view.trailGrep ? `transcript search: ${view.trailGrep}` : "search transcript") : (view.grep ? `search: ${view.grep}` : "search")], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
117
119
  else if (view.kind === "pending")
118
120
  keys = [["Enter", "send"], ["↑↓", "select merge/review"], ["PgUp/PgDn", "scroll"], ["Ctrl+R", "refresh"], ["Ctrl+N", "overlaps"], ["Ctrl+G", "overlap role"], ["Ctrl+O", "review state"], ["Ctrl+X", "cancel merge"], ["Ctrl+V", "move to top"], ["Ctrl+A", "accept review"], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
119
121
  else if (view.kind === "health")
120
- keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+O", view.problems ? "problems only" : "all checks"], ["Ctrl+R", "re-run checks"], ["Ctrl+P", "re-probe models"], ["Ctrl+A", "commit backlog"], ["Ctrl+U", "re-sync install"], ["Ctrl+E", "reclaim disk"], ["Ctrl+N", "test notify"], ["Ctrl+K", "kill orphans"], ["Ctrl+S", "drop broken pins"], ["Ctrl+X", "retire role"], ["Ctrl+L", "retire leftovers"], ["Ctrl+V", "enable/disable role"], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
122
+ keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+O", view.problems ? "problems only" : "all checks"], ["Ctrl+R", "re-run checks"], ["Ctrl+A", "commit backlog"], ["Ctrl+U", "re-sync install"], ["Ctrl+E", "reclaim disk"], ["Ctrl+N", "test notify"], ["Ctrl+K", "kill orphans"], ["Ctrl+S", "drop broken pins"], ["Ctrl+X", "retire role"], ["Ctrl+L", "retire leftovers"], ["Ctrl+B", "archive backlog"], ["Ctrl+V", "enable/disable role"], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
121
123
  else if (view.kind === "brief")
122
124
  keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+E", "edit brief"], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
123
125
  else if (view.kind === "queued")
124
126
  keys = [["Enter", "send"], ["↑↓", "history"], ["Ctrl+V", "top"], ["Ctrl+E", "edit"], ["Ctrl+X", "cancel"], ["/", view.grep ? `search: ${view.grep}` : "search"], ["PgUp/PgDn", "scroll"], ["Ctrl+T", "next view"], ["Ctrl+D", "quit"]];
125
127
  else
126
- keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+B", "daily cap"], ["Ctrl+E", "notify hook"], ["Ctrl+A", "model"], ["Ctrl+O", view.notable ? "notable only" : view.type ? `type: ${eventTypeLabel(view.type)}` : "type: all"], ["Ctrl+G", view.role ? `scope: ${view.role}` : "scope: team"], ["/", view.grep ? `search: ${view.grep}` : "search"], ["Ctrl+P", "provider caps"], ["Ctrl+X", "agent backends"], ["Ctrl+T", "next view"], ["Ctrl+L", "fallback"], ["Ctrl+Y", "claude reserve"], ["Ctrl+F", "pause team"], ["Ctrl+Q", "stop later"], ["Ctrl+C", "interrupt director"], ["Ctrl+V", "attach image"], ["Ctrl+R", "restart"], ["Ctrl+D", "quit"]];
128
+ keys = [["Enter", "send"], ["↑↓", "history"], ["PgUp/PgDn", "scroll"], ["Ctrl+B", "daily cap"], ["Ctrl+E", "notify hook"], ["Ctrl+A", "model"], ["Ctrl+N", "run cap"], ["Ctrl+K", "check cap"], ["Ctrl+U", "declared check"], ["Ctrl+S", "quiet hours"], ["Ctrl+W", "worktree slots"], ["Ctrl+O", view.notable ? "notable only" : view.type ? `type: ${eventTypeLabel(view.type)}` : "type: all"], ["Ctrl+G", view.role ? `scope: ${view.role}` : "scope: team"], ["/", view.grep ? `search: ${view.grep}` : "search"], ["Ctrl+P", "provider caps"], ["Ctrl+X", "agent backends"], ["Ctrl+T", "next view"], ["Ctrl+L", "fallback"], ["Ctrl+Y", "claude reserve"], ["Ctrl+F", "pause team"], ["Ctrl+Q", "stop later"], ["Ctrl+C", "interrupt director"], ["Ctrl+V", "attach image"], ["Ctrl+R", "restart"], ["Ctrl+D", "quit"]];
127
129
  // The mode branches above name their own keys and keep the line, so every remaining
128
130
  // view hint always shows the send affordance. This comment marks that ordering.
129
131
  const spans = [];
@@ -172,6 +174,8 @@ export function promptPrefix(mode) {
172
174
  return [{ text: "brief", tone: "brand" }, { text: " › " }];
173
175
  if (mode.retireMode)
174
176
  return [{ text: "retire", tone: "brand" }, { text: " › " }];
177
+ if (mode.releaseMode)
178
+ return [{ text: "release", tone: "brand" }, { text: " › " }];
175
179
  if (mode.roleEnabledMode)
176
180
  return [{ text: "role state", tone: "brand" }, { text: " › " }];
177
181
  if (mode.notesEditFor)