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
@@ -17,77 +17,87 @@ export function createQueuedEditor(session) {
17
17
  session.held.queued = null;
18
18
  restoreDraft();
19
19
  };
20
- const handleKey = (_str, key, ctx) => {
21
- if (key.ctrl && key.name === "e" &&
22
- !session.anyEditorHeld() &&
23
- ctx.view === ctx.roleIds.length + 6 && ctx.selectedQueued !== null) {
24
- // Ctrl+E opens the Queued pane's in-place editor on the selected row (mnemonic:
25
- // **E**dit) through the same readQueuedPromptBody/editQueuedFile pair the dashboard's
26
- // Edit uses: the body is pre-filled marker- and attachment-stripped, and Enter writes
27
- // it back to the same queue file, so its position, enqueue stamp, deferral, and
28
- // attached images survive. A row that vanished between selection and this press (the
29
- // role dequeued it, or another operator cancelled it) flashes the gone notice and
30
- // refreshes the list. A no-selection press (list mode) falls through to the report
31
- // window below, exactly as before.
32
- try {
33
- const position = Math.min(ctx.selectedQueued, ctx.queuedRows.length - 1);
34
- const row = ctx.queuedRows[position];
35
- if (row !== undefined) {
36
- const body = readQueuedPromptBody(session.root, row.role, row.file);
37
- if (body === null) {
38
- session.flashMessage(`inbox message #${row.position} for ${row.role} is gone`);
39
- session.refreshQueued();
40
- }
41
- else {
42
- // Mid-recall, the line holds a history entry while the real draft sits in the
43
- // recall state: settle first, so the queued editor saves (and later restores)
44
- // the draft the operator was writing.
45
- restoreDraft = session.takeDraft();
46
- queuedEditFor = { role: row.role, file: row.file };
47
- session.held.queued = queuedEditFor;
48
- session.input = body;
49
- session.cursor = body.length;
50
- session.flashMessage(`editing inbox message #${row.position} for ${row.role} — Enter to save, Esc to cancel`);
51
- }
20
+ // Ctrl+E opens the Queued pane's in-place editor on the selected row (mnemonic:
21
+ // **E**dit) through the same readQueuedPromptBody/editQueuedFile pair the dashboard's
22
+ // Edit uses: the body is pre-filled marker- and attachment-stripped, and Enter writes
23
+ // it back to the same queue file, so its position, enqueue stamp, deferral, and
24
+ // attached images survive. A row that vanished between selection and this press (the
25
+ // role dequeued it, or another operator cancelled it) flashes the gone notice and
26
+ // refreshes the list. A no-selection press (list mode) falls through to the report
27
+ // window below, exactly as before.
28
+ const queuedOpenEdit = (ctx) => {
29
+ if (session.anyEditorHeld() ||
30
+ ctx.view !== ctx.roleIds.length + 6 ||
31
+ ctx.selectedQueued === null) {
32
+ return false;
33
+ }
34
+ try {
35
+ const position = Math.min(ctx.selectedQueued, ctx.queuedRows.length - 1);
36
+ const row = ctx.queuedRows[position];
37
+ if (row !== undefined) {
38
+ const body = readQueuedPromptBody(session.root, row.role, row.file);
39
+ if (body === null) {
40
+ session.flashMessage(`inbox message #${row.position} for ${row.role} is gone`);
41
+ session.refreshQueued();
42
+ }
43
+ else {
44
+ // Mid-recall, the line holds a history entry while the real draft sits in the
45
+ // recall state: settle first, so the queued editor saves (and later restores)
46
+ // the draft the operator was writing.
47
+ restoreDraft = session.takeDraft();
48
+ queuedEditFor = { role: row.role, file: row.file };
49
+ session.held.queued = queuedEditFor;
50
+ session.input = body;
51
+ session.cursor = body.length;
52
+ session.flashMessage(`editing inbox message #${row.position} for ${row.role} — Enter to save, Esc to cancel`);
52
53
  }
53
54
  }
54
- catch (err) {
55
- session.flashError(err);
56
- }
57
- return true;
58
55
  }
59
- if (key.name === "escape" && queuedEditFor) {
60
- // Esc cancels the Queued pane's in-place editor, restoring the previous prompt text.
61
- exitQueuedEdit();
62
- return true;
56
+ catch (err) {
57
+ session.flashError(err);
63
58
  }
64
- if (key.name === "return" && queuedEditFor) {
65
- // Enter saves the queued editor's line through the same file-addressed
66
- // editQueuedFile pair the dashboard's Edit uses: the write targets the queue file
67
- // the row named, so a concurrent dequeue cannot land it on the wrong prompt. An
68
- // invalid (whitespace-only) line flashes and STAYS in the editor; a gone outcome
69
- // is the concurrent-dequeue race, flashed honestly and returned to the list; a
70
- // throw keeps the text in the editor for a retry.
71
- const parsed = parseRolePromptInput(session.input);
72
- if (!parsed.ok) {
73
- session.flashMessage(parsed.error);
59
+ return true;
60
+ };
61
+ // Esc cancels the Queued pane's in-place editor, restoring the previous prompt text.
62
+ const cancelQueuedEdit = () => {
63
+ exitQueuedEdit();
64
+ return true;
65
+ };
66
+ // Enter saves the queued editor's line through the same file-addressed
67
+ // editQueuedFile pair the dashboard's Edit uses: the write targets the queue file
68
+ // the row named, so a concurrent dequeue cannot land it on the wrong prompt. An
69
+ // invalid (whitespace-only) line flashes and STAYS in the editor; a gone outcome
70
+ // is the concurrent-dequeue race, flashed honestly and returned to the list; a
71
+ // throw keeps the text in the editor for a retry.
72
+ const commitQueuedEdit = () => {
73
+ const parsed = parseRolePromptInput(session.input);
74
+ if (!parsed.ok) {
75
+ session.flashMessage(parsed.error);
76
+ }
77
+ else {
78
+ // The dispatch guards on `queuedEditFor`, so it is never null when this arm runs.
79
+ const target = queuedEditFor;
80
+ try {
81
+ const outcome = editQueuedFile(session.root, target.role, target.file, parsed.value);
82
+ exitQueuedEdit();
83
+ session.refreshQueued();
84
+ session.flashMessage(outcome.status === "edited"
85
+ ? `edited inbox message for ${target.role}`
86
+ : "the inbox message was already gone");
74
87
  }
75
- else {
76
- const target = queuedEditFor;
77
- try {
78
- const outcome = editQueuedFile(session.root, target.role, target.file, parsed.value);
79
- exitQueuedEdit();
80
- session.refreshQueued();
81
- session.flashMessage(outcome.status === "edited"
82
- ? `edited inbox message for ${target.role}`
83
- : "the inbox message was already gone");
84
- }
85
- catch (err) {
86
- session.flashError(err);
87
- }
88
+ catch (err) {
89
+ session.flashError(err);
88
90
  }
89
- return true;
90
91
  }
92
+ return true;
93
+ };
94
+ const handleKey = (_str, key, ctx) => {
95
+ if (key.ctrl && key.name === "e")
96
+ return queuedOpenEdit(ctx);
97
+ if (key.name === "escape" && queuedEditFor)
98
+ return cancelQueuedEdit();
99
+ if (key.name === "return" && queuedEditFor)
100
+ return commitQueuedEdit();
91
101
  return false;
92
102
  };
93
103
  return { handleKey, exit: exitQueuedEdit };
@@ -2,6 +2,7 @@ import { setConfigKey, unsetConfigKey } from "../../config/config-write.js";
2
2
  import { loadConfigSafe } from "../../config/config.js";
3
3
  import { DIRECTOR_ROLE } from "../../roles/roles.js";
4
4
  import { baseRoleOf } from "../../roles/runner-ids.js";
5
+ import { viewedRole } from "./tui-editor-ctx.js";
5
6
  /** The Ctrl+O per-role / Ctrl+S team-wide quiet-hours editor on the shared prompt line, the
6
7
  * terminal twin of the dashboard's `Quiet hours` rows and `tumwater role <id> --quiet-hours`
7
8
  * / `tumwater config set quietHours`. It owns its mode flags, its saved-draft pair, and the
@@ -34,123 +35,136 @@ export function createQuietEditor(session) {
34
35
  restoreDraft();
35
36
  lockedRole = null;
36
37
  };
37
- const handleKey = (_str, key, ctx) => {
38
- if (key.ctrl && key.name === "o") {
39
- // Ctrl+O edits the viewed role's own quiet-hours window (mnemonic: the silent *o*ur
40
- // hours) on its Transcript view. Every non-transcript view keeps Ctrl+O — the guard
41
- // lets the key fall through to the pane arms (Activity's event-type cycle, History's
42
- // outcome cycle, Health's problems-only toggle) unless the quiet editor already holds
43
- // the line. While a scope is open, Ctrl+O toggles only the per-role scope out; in the
44
- // team scope it is consumed and does nothing. Entering pre-fills the window from
45
- // `quietHoursPerRole.<base>` (empty when unset — empty means "off" on save) and saves
46
- // the prompt text; leaving restores it. The director is exempt — its
47
- // quietHoursPerRole.director entry is inert (the gate and doctor both exempt it).
48
- if (quietMode) {
49
- // Consumed either way: in the per-role scope it toggles the editor out, in the team
50
- // scope Ctrl+O leaves the team editor open.
51
- if (!quietTeam)
52
- exitQuietMode();
53
- return true;
54
- }
55
- if (!(ctx.view >= 1 && ctx.view <= ctx.roleIds.length))
56
- return false;
57
- const role = ctx.roleIds[ctx.view - 1];
58
- if (session.editorEntryDenied("quiet"))
59
- return true;
60
- if (role === DIRECTOR_ROLE) {
61
- session.flashMessage("the director has no quiet hours");
62
- return true;
63
- }
64
- // Pin the role first so the write below is safe against a mid-edit roster shift, then
65
- // pre-fill from the file's own entry — one fresh read per open is the smallest honest
66
- // source; a broken config degrades to no pre-fill and the write reports the error.
67
- lockedRole = role;
68
- const existing = loadConfigSafe(session.root).config?.quietHoursPerRole?.[baseRoleOf(role)] ?? "";
69
- restoreDraft = session.takeDraft();
70
- quietMode = true;
71
- quietTeam = false;
72
- session.held.quiet = true;
73
- session.held.quietTeam = false;
74
- session.input = existing;
75
- session.cursor = session.input.length;
76
- session.flashMessage(`edit ${role}'s quiet hours (e.g. 23:00-07:00, empty clears): Enter to save, Esc to cancel`);
38
+ // The shared way both open paths put the editor on the line: take the prompt draft to
39
+ // restore on exit, raise the mode flags and the held registry for the chosen scope, pre-fill
40
+ // the prompt text (cursor at its end) and flash the guidance line.
41
+ const openQuietEditor = (team, existing, message) => {
42
+ restoreDraft = session.takeDraft();
43
+ quietMode = true;
44
+ quietTeam = team;
45
+ session.held.quiet = true;
46
+ session.held.quietTeam = team;
47
+ session.input = existing;
48
+ session.cursor = session.input.length;
49
+ session.flashMessage(message);
50
+ };
51
+ // The Ctrl+O arm: edits the viewed role's own quiet-hours window (mnemonic: the silent *o*ur
52
+ // hours) on its Transcript view. Every non-transcript view keeps Ctrl+O — the guard lets the
53
+ // key fall through to the pane arms (Activity's event-type cycle, History's outcome cycle,
54
+ // Health's problems-only toggle) unless the quiet editor already holds the line. While a
55
+ // scope is open, Ctrl+O toggles only the per-role scope out; in the team scope it is consumed
56
+ // and does nothing. Entering pre-fills the window from `quietHoursPerRole.<base>` (empty when
57
+ // unset — empty means "off" on save) and saves the prompt text; leaving restores it. The
58
+ // director is exempt — its quietHoursPerRole.director entry is inert (the gate and doctor
59
+ // both exempt it).
60
+ const roleQuietKey = (ctx) => {
61
+ if (quietMode) {
62
+ // Consumed either way: in the per-role scope it toggles the editor out, in the team
63
+ // scope Ctrl+O leaves the team editor open.
64
+ if (!quietTeam)
65
+ exitQuietMode();
77
66
  return true;
78
67
  }
79
- if (key.ctrl && key.name === "s") {
80
- // Ctrl+S edits the team-wide quiet-hours window on the Activity view (view 0) — the
81
- // terminal twin of the dashboard Settings `quietHours` row and `tumwater config set
82
- // quietHours`, so an operator can put the whole team to sleep for a nightly window. On
83
- // every non-Activity view Ctrl+S keeps its other meaning — the guard lets the key fall
84
- // through to the interval editor (a role's Transcript) and the pane arms (History's
85
- // trailing-window cycle). While a scope is open, Ctrl+S toggles only the team scope out;
86
- // in the per-role scope it is consumed and does nothing (so it never opens the interval
87
- // editor over the quiet editor). Entering pre-fills the window from the FILE's top-level
88
- // `quietHours` (empty when unset — empty means "off" on save) and saves the prompt text;
89
- // leaving restores it. The team window owns no role, so lockedRole stays null.
90
- if (quietMode) {
91
- // In the team scope Ctrl+S toggles the editor out. In the per-role scope it must not
92
- // touch the per-role editor, but it also must not silently vanish: fall through so the
93
- // interval editor (the other Ctrl+S arm, which runs after this one) flashes the way out,
94
- // naming the per-role quiet editor as the holder.
95
- if (!quietTeam)
96
- return false;
97
- exitQuietMode();
98
- return true;
99
- }
100
- if (ctx.view !== 0)
101
- return false;
102
- if (session.editorEntryDenied("quiet"))
103
- return true;
104
- const existing = loadConfigSafe(session.root).config?.quietHours ?? "";
105
- restoreDraft = session.takeDraft();
106
- quietMode = true;
107
- quietTeam = true;
108
- session.held.quiet = true;
109
- session.held.quietTeam = true;
110
- session.input = existing;
111
- session.cursor = session.input.length;
112
- session.flashMessage("edit the team's quiet hours (e.g. 23:00-07:00, empty clears): Enter to save, Esc to cancel");
68
+ if (!(ctx.view >= 1 && ctx.view <= ctx.roleIds.length))
69
+ return false;
70
+ const role = viewedRole(ctx);
71
+ if (session.editorEntryDenied("quiet"))
72
+ return true;
73
+ if (role === DIRECTOR_ROLE) {
74
+ session.flashMessage("the director has no quiet hours");
113
75
  return true;
114
76
  }
115
- if (key.name === "escape" && quietMode) {
116
- // Esc cancels quiet-edit mode, restoring the previous prompt text.
77
+ // Pin the role first so the write below is safe against a mid-edit roster shift, then
78
+ // pre-fill from the file's own entry — one fresh read per open is the smallest honest
79
+ // source; a broken config degrades to no pre-fill and the write reports the error.
80
+ lockedRole = role;
81
+ const existing = loadConfigSafe(session.root).config?.quietHoursPerRole?.[baseRoleOf(role)] ?? "";
82
+ openQuietEditor(false, existing, `edit ${role}'s quiet hours (e.g. 23:00-07:00, empty clears): Enter to save, Esc to cancel`);
83
+ return true;
84
+ };
85
+ // The Ctrl+S arm: edits the team-wide quiet-hours window on the Activity view (view 0) — the
86
+ // terminal twin of the dashboard Settings `quietHours` row and `tumwater config set
87
+ // quietHours`, so an operator can put the whole team to sleep for a nightly window. On every
88
+ // non-Activity view Ctrl+S keeps its other meaning — the guard lets the key fall through to
89
+ // the interval editor (a role's Transcript) and the pane arms (History's trailing-window
90
+ // cycle). While a scope is open, Ctrl+S toggles only the team scope out; in the per-role
91
+ // scope it is consumed and does nothing (so it never opens the interval editor over the
92
+ // quiet editor). Entering pre-fills the window from the FILE's top-level `quietHours` (empty
93
+ // when unset — empty means "off" on save) and saves the prompt text; leaving restores it.
94
+ // The team window owns no role, so lockedRole stays null.
95
+ const teamQuietKey = (ctx) => {
96
+ if (quietMode) {
97
+ // In the team scope Ctrl+S toggles the editor out. In the per-role scope it must not
98
+ // touch the per-role editor, but it also must not silently vanish: fall through so the
99
+ // interval editor (the other Ctrl+S arm, which runs after this one) flashes the way out,
100
+ // naming the per-role quiet editor as the holder.
101
+ if (!quietTeam)
102
+ return false;
117
103
  exitQuietMode();
118
104
  return true;
119
105
  }
120
- if (key.name === "return" && quietMode) {
121
- // Enter commits the window through the shared setter family the CLI's `--quiet-hours`
122
- // and `tumwater config set quietHours` / the dashboard rows use: an empty or
123
- // whitespace-only line clears the key (`unsetConfigKey`), otherwise `setConfigKey` writes
124
- // the dotted `quietHoursPerRole.<base>` entry (per-role scope) or the top-level
125
- // `quietHours` (team scope) as a JSON string. checkQuietHours (naming the dotted key)
126
- // rejects a malformed per-role window and validateConfig guards the team write; both
127
- // write tumwater.json atomically and the running orchestrator picks the change up on its
128
- // next poll. An invalid window flashes and STAYS in edit mode so the operator can fix
129
- // it; success restores the saved prompt text.
130
- const cfgKey = quietTeam ? "quietHours" : `quietHoursPerRole.${baseRoleOf(lockedRole)}`;
131
- const text = session.input.trim();
132
- const result = text === ""
133
- ? unsetConfigKey(session.root, cfgKey)
134
- : setConfigKey(session.root, cfgKey, JSON.stringify(text));
135
- if (result.ok) {
136
- const team = quietTeam;
137
- const role = lockedRole;
138
- exitQuietMode();
139
- const scope = team ? "team" : `${role}`;
140
- const noop = text === "" ? result.oldValue === undefined : "unchanged" in result;
141
- session.flashMessage(noop
142
- ? text === ""
143
- ? `${scope}: quiet hours is not set in tumwater.json, so nothing was cleared`
144
- : `${scope}: quiet hours is already ${text}, so tumwater.json was not written`
145
- : text === ""
146
- ? `${scope}: quiet hours cleared`
147
- : `${scope}: quiet hours set to ${text}`);
148
- }
149
- else {
150
- session.flashMessage(result.error); // broken config or write failure — stay in edit mode
151
- }
106
+ if (ctx.view !== 0)
107
+ return false;
108
+ if (session.editorEntryDenied("quiet"))
109
+ return true;
110
+ const existing = loadConfigSafe(session.root).config?.quietHours ?? "";
111
+ openQuietEditor(true, existing, "edit the team's quiet hours (e.g. 23:00-07:00, empty clears): Enter to save, Esc to cancel");
112
+ return true;
113
+ };
114
+ // The Esc arm: cancels quiet-edit mode, restoring the previous prompt text.
115
+ const cancelQuietEdit = () => {
116
+ exitQuietMode();
117
+ return true;
118
+ };
119
+ // The Enter arm: commits the window through the shared setter family the CLI's `--quiet-hours`
120
+ // and `tumwater config set quietHours` / the dashboard rows use: an empty or whitespace-only
121
+ // line clears the key (`unsetConfigKey`), otherwise `setConfigKey` writes the dotted
122
+ // `quietHoursPerRole.<base>` entry (per-role scope) or the top-level `quietHours` (team scope)
123
+ // as a JSON string. checkQuietHours (naming the dotted key) rejects a malformed per-role
124
+ // window and validateConfig guards the team write; both write tumwater.json atomically and the
125
+ // running orchestrator picks the change up on its next poll. An invalid window flashes and
126
+ // STAYS in edit mode so the operator can fix it; success restores the saved prompt text.
127
+ const commitQuietEntry = () => {
128
+ const cfgKey = quietTeam ? "quietHours" : `quietHoursPerRole.${baseRoleOf(lockedRole)}`;
129
+ const text = session.input.trim();
130
+ const result = text === ""
131
+ ? unsetConfigKey(session.root, cfgKey)
132
+ : setConfigKey(session.root, cfgKey, JSON.stringify(text));
133
+ if (result.ok) {
134
+ flashQuietOutcome(result, text);
152
135
  return true;
153
136
  }
137
+ session.flashMessage(result.error); // broken config or write failure — stay in edit mode
138
+ return true;
139
+ };
140
+ // The one success flash, shared by both scopes: reads the scope and role before exiting so
141
+ // the message still names the window that was just written, computes the no-op case (nothing
142
+ // was set and an empty line was cleared, or the value was already in place) and flashes the
143
+ // matching line. Both setConfigKey's and unsetConfigKey's success members carry `oldValue`, so
144
+ // the shared structural type accepts either branch's `result`.
145
+ const flashQuietOutcome = (result, text) => {
146
+ const team = quietTeam;
147
+ const role = lockedRole;
148
+ exitQuietMode();
149
+ const scope = team ? "team" : `${role}`;
150
+ const noop = text === "" ? result.oldValue === undefined : "unchanged" in result;
151
+ session.flashMessage(noop
152
+ ? text === ""
153
+ ? `${scope}: quiet hours is not set in tumwater.json, so nothing was cleared`
154
+ : `${scope}: quiet hours is already ${text}, so tumwater.json was not written`
155
+ : text === ""
156
+ ? `${scope}: quiet hours cleared`
157
+ : `${scope}: quiet hours set to ${text}`);
158
+ };
159
+ const handleKey = (_str, key, ctx) => {
160
+ if (key.ctrl && key.name === "o")
161
+ return roleQuietKey(ctx);
162
+ if (key.ctrl && key.name === "s")
163
+ return teamQuietKey(ctx);
164
+ if (key.name === "escape" && quietMode)
165
+ return cancelQuietEdit();
166
+ if (key.name === "return" && quietMode)
167
+ return commitQuietEntry();
154
168
  return false;
155
169
  };
156
170
  return {
@@ -0,0 +1,85 @@
1
+ import { releaseQuarantinedTest } from "../../review/known-flakes.js";
2
+ /** The Failures view's Ctrl+X release editor on the shared prompt line, the terminal twin of
3
+ * `tumwater quarantine --release <n>` and the dashboard Failures card's Release button. Like
4
+ * the retire editor it owns its `releaseMode` flag, its saved-draft pair, and the handleKey
5
+ * arms for its key (the toggle with the `heldEditorFlash(entering)` guard clause — the shared
6
+ * guard priority lives in the session module — plus Esc and Enter). Guarded to the Failures
7
+ * view (`ctx.view === roleIds.length + 3`), so every other view's Ctrl+X arm (Transcript clears
8
+ * notes, Backlog/Inbox/Pending cancel, History model filter, Activity backends) keeps the key.
9
+ * Enter runs the same `releaseQuarantinedTest(session.root, test)` core the CLI and the
10
+ * dashboard button call, with a typed test path as its confirmation (no two-press arm): the
11
+ * exact path the Failures list shows, Enter to release, Esc to cancel. A success removes the
12
+ * entry from `.tumwater/state/quarantine.json`, exits the editor, flashes the outcome, and
13
+ * repaints the Failures body; a name not in the list flashes `not in the flake quarantine` and
14
+ * STAYS open so the operator can fix the name; an empty name flashes and stays. */
15
+ export function createQuarantineReleaseEditor(session) {
16
+ let releaseMode = false;
17
+ let restoreDraft = () => { };
18
+ // Leave release-edit mode (Esc, Enter's success path, or Ctrl+T): restore the saved prompt
19
+ // text.
20
+ const exitReleaseMode = () => {
21
+ if (!releaseMode)
22
+ return;
23
+ releaseMode = false;
24
+ session.held.release = false;
25
+ restoreDraft();
26
+ };
27
+ // Open release-edit mode on the Failures view's prompt line (Ctrl+X). The Failures index
28
+ // guard keeps the same key's other arms (Transcript notes-clear, Backlog cancel, and the
29
+ // like) on their own views; a guard miss returns false so the key falls through. The line
30
+ // edits one quarantined test path, typed exactly as the Failures list shows it; Enter
31
+ // releases that test through the same releaseQuarantinedTest core the CLI and the dashboard
32
+ // button use. Entering saves the prompt draft and blanks the line; leaving restores it.
33
+ const openReleaseEdit = (ctx) => {
34
+ if (ctx.view !== ctx.roleIds.length + 3 || releaseMode)
35
+ return false;
36
+ if (session.editorEntryDenied("release"))
37
+ return true;
38
+ // Mid-recall, hand the arrows' saved draft back before the release editor takes the line —
39
+ // the same settle the retire and role editors do — so leaving restores the draft, not a
40
+ // history entry, with no stale recall state riding along.
41
+ restoreDraft = session.takeDraft();
42
+ releaseMode = true;
43
+ session.held.release = true;
44
+ session.input = "";
45
+ session.cursor = 0;
46
+ session.flashMessage("release a quarantined test: type its test path as the Failures list shows it, Enter to release, Esc to cancel");
47
+ return true;
48
+ };
49
+ // Esc cancels release-edit mode, restoring the previous prompt text and removing nothing.
50
+ const cancelReleaseEdit = () => {
51
+ exitReleaseMode();
52
+ session.flashMessage("release cancelled");
53
+ return true;
54
+ };
55
+ // Enter releases the typed test path through releaseQuarantinedTest — the same core, and
56
+ // the same single-press confirmation, the CLI's `tumwater quarantine --release <n>` and
57
+ // the dashboard Failures card's Release button use — then flashes the shared outcome
58
+ // wording and repaints the Failures body so the released line is gone. An empty name, or
59
+ // one not in the quarantine, flashes and STAYS in the editor so the operator can fix it.
60
+ const submitReleaseEdit = () => {
61
+ const test = session.input.trim();
62
+ if (test === "") {
63
+ session.flashMessage("a test name is required");
64
+ return true;
65
+ }
66
+ if (!releaseQuarantinedTest(session.root, test)) {
67
+ session.flashMessage(`not in the flake quarantine: ${test}`);
68
+ return true;
69
+ }
70
+ exitReleaseMode();
71
+ session.flashMessage(`released quarantined test ${test} — the gate no longer excuses it`);
72
+ session.refreshReport();
73
+ return true;
74
+ };
75
+ const handleKey = (_str, key, ctx) => {
76
+ if (key.ctrl && key.name === "x")
77
+ return openReleaseEdit(ctx);
78
+ if (key.name === "escape" && releaseMode)
79
+ return cancelReleaseEdit();
80
+ if (key.name === "return" && releaseMode)
81
+ return submitReleaseEdit();
82
+ return false;
83
+ };
84
+ return { handleKey, exit: exitReleaseMode };
85
+ }
@@ -20,55 +20,66 @@ export function createRetireEditor(session) {
20
20
  session.held.retire = false;
21
21
  restoreDraft();
22
22
  };
23
- const handleKey = (_str, key, ctx) => {
24
- if (key.ctrl && key.name === "x" && ctx.view === ctx.roleIds.length + 7 && !retireMode) {
25
- // Ctrl+X toggles retire-edit mode on the Health view's prompt line (mnemonic: **x**
26
- // remove — the same key the Transcript notes-clear, Backlog cancel, and Inbox cancel
27
- // arms use on their own views, guarded away here by the Health index). The line edits one
28
- // role id; Enter retires that role's leftovers through the same retireRole core the CLI
29
- // and the dashboard button use. Entering saves the prompt draft and blanks the line;
30
- // leaving restores it.
31
- if (session.editorEntryDenied("retire"))
32
- return true;
33
- // Mid-recall, hand the arrows' saved draft back before the retire editor takes the line —
34
- // the same settle the role editors do — so leaving restores the draft, not a
35
- // history entry, with no stale recall state riding along.
36
- restoreDraft = session.takeDraft();
37
- retireMode = true;
38
- session.held.retire = true;
39
- session.input = "";
40
- session.cursor = 0;
41
- session.flashMessage("retire a disabled role's leftovers: type the role id, Enter to retire, Esc to cancel");
23
+ // Ctrl+X toggles retire-edit mode on the Health view's prompt line (mnemonic: **x**
24
+ // remove — the same key the Transcript notes-clear, Backlog cancel, and Inbox cancel
25
+ // arms use on their own views, guarded away here by the Health index). The line edits one
26
+ // role id; Enter retires that role's leftovers through the same retireRole core the CLI
27
+ // and the dashboard button use. Entering saves the prompt draft and blanks the line;
28
+ // leaving restores it. The guard returns false on a miss so the key falls through to the
29
+ // panes' own Ctrl+X arms on their views.
30
+ const openRetireEdit = (ctx) => {
31
+ if (ctx.view !== ctx.roleIds.length + 7)
32
+ return false;
33
+ if (retireMode)
34
+ return false;
35
+ if (session.editorEntryDenied("retire"))
42
36
  return true;
43
- }
44
- if (key.name === "escape" && retireMode) {
45
- // Esc cancels retire-edit mode, restoring the previous prompt text and removing nothing.
46
- exitRetireMode();
47
- session.flashMessage("retire cancelled");
48
- return true;
49
- }
50
- if (key.name === "return" && retireMode) {
51
- // Enter retires the typed role id through retireRole — the same core, and the same
52
- // never-force policy, the CLI's `tumwater retire --role <id>` and the dashboard's Retire
53
- // button use — then flashes the dashboard's own outcome wording. An empty id flashes and
54
- // STAYS in the editor so the operator can type one; a safety-rail refusal throws (the
55
- // role still enabled, unmerged commits, a dirty or unusable worktree, a run in progress)
56
- // and the cursor stays so the operator can fix the id or reach for the CLI's --force.
57
- const role = session.input.trim();
58
- if (role === "") {
59
- session.flashMessage("a role id is required");
60
- return true;
61
- }
62
- void retireRole(session.root, role, {}).then(({ removed }) => {
63
- exitRetireMode();
64
- session.flashMessage(removed.length === 0
65
- ? `nothing to remove for role ${role}`
66
- : removed.map((a) => `removed ${artifactPhrase(a, role)}`).join("; "));
67
- }, (err) => {
68
- session.flashError(err);
69
- });
37
+ // Mid-recall, hand the arrows' saved draft back before the retire editor takes the line —
38
+ // the same settle the role editors do — so leaving restores the draft, not a
39
+ // history entry, with no stale recall state riding along.
40
+ restoreDraft = session.takeDraft();
41
+ retireMode = true;
42
+ session.held.retire = true;
43
+ session.input = "";
44
+ session.cursor = 0;
45
+ session.flashMessage("retire a disabled role's leftovers: type the role id, Enter to retire, Esc to cancel");
46
+ return true;
47
+ };
48
+ // Esc cancels retire-edit mode, restoring the previous prompt text and removing nothing.
49
+ const cancelRetireEdit = () => {
50
+ exitRetireMode();
51
+ session.flashMessage("retire cancelled");
52
+ return true;
53
+ };
54
+ // Enter retires the typed role id through retireRole — the same core, and the same
55
+ // never-force policy, the CLI's `tumwater retire --role <id>` and the dashboard's Retire
56
+ // button use — then flashes the dashboard's own outcome wording. An empty id flashes and
57
+ // STAYS in the editor so the operator can type one; a safety-rail refusal throws (the
58
+ // role still enabled, unmerged commits, a dirty or unusable worktree, a run in progress)
59
+ // and the cursor stays so the operator can fix the id or reach for the CLI's --force.
60
+ const submitRetireEdit = () => {
61
+ const role = session.input.trim();
62
+ if (role === "") {
63
+ session.flashMessage("a role id is required");
70
64
  return true;
71
65
  }
66
+ void retireRole(session.root, role, {}).then(({ removed }) => {
67
+ exitRetireMode();
68
+ session.flashMessage(removed.length === 0
69
+ ? `nothing to remove for role ${role}`
70
+ : removed.map((a) => `removed ${artifactPhrase(a, role)}`).join("; "));
71
+ }, (err) => {
72
+ session.flashError(err);
73
+ });
74
+ return true;
75
+ };
76
+ const handleKey = (_str, key, ctx) => {
77
+ if (key.ctrl && key.name === "x")
78
+ return openRetireEdit(ctx);
79
+ if (key.name === "escape" && retireMode)
80
+ return cancelRetireEdit();
81
+ if (key.name === "return" && retireMode)
82
+ return submitRetireEdit();
72
83
  return false;
73
84
  };
74
85
  return { handleKey, exit: exitRetireMode };