tumwater 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -0,0 +1,266 @@
1
+ // vocabulary: plain
2
+ import { cancelScheduledStopMessage, requestAbort, requestResetCounters, requestScheduledStop, requestRestart, requestWake, cancelWakes, rolePauseMessage, roleResumeMessage, } from "../../operator/operator-intent.js";
3
+ import { isTeamPaused, pausedRoles, pauseTeam, pauseRole, resumeTeam, resumeRole, scheduledStopAt, } from "../../team/team-state.js";
4
+ import { durationLabel } from "../../cli/cli-args.js";
5
+ import { teamPausePhrase, teamResumePhrase } from "../../text/phrases.js";
6
+ import { parsePauseInput, parseRolePauseInput, parseRoleWakeInput, parseStopInInput, } from "./tui-input.js";
7
+ /** The pause/stop/wake editor on the shared prompt line: the whole-team Ctrl+F pause and Ctrl+Q
8
+ * stop editors, the per-loop Ctrl+P pause / Ctrl+A abort / Ctrl+W wake controls, and the
9
+ * parse/dispatch leaves those write. It owns its mode flags (pauseEditMode/`pauseEditRole`/
10
+ * `pauseEditWake`/`pauseEditStop`), its saved-draft pair, its exit helper, and the handleKey
11
+ * arms for its keys. Mutually exclusive with the other editors via the session's held registry. */
12
+ export function createPauseEditor(session) {
13
+ let pauseEditMode = false;
14
+ let pauseEditRole = null;
15
+ let pauseEditWake = false;
16
+ let pauseEditStop = false;
17
+ let pauseSavedInput = "";
18
+ let pauseSavedCursor = 0;
19
+ // Leave pause-edit mode (Esc, Ctrl+F again, a written pause, or Ctrl+T): restore the draft.
20
+ const exitPauseEditMode = () => {
21
+ if (!pauseEditMode)
22
+ return;
23
+ pauseEditMode = false;
24
+ pauseEditRole = null;
25
+ pauseEditWake = false;
26
+ pauseEditStop = false;
27
+ session.held.pause = false;
28
+ session.held.pauseEditRole = null;
29
+ session.held.pauseEditWake = false;
30
+ session.held.pauseEditStop = false;
31
+ session.restorePromptFrom(pauseSavedInput, pauseSavedCursor);
32
+ };
33
+ const openPauseEditor = (role, wake, stop, hint) => {
34
+ session.settleDraft((t) => (pauseSavedInput = t), (n) => (pauseSavedCursor = n));
35
+ pauseEditMode = true;
36
+ pauseEditRole = role;
37
+ pauseEditWake = wake;
38
+ pauseEditStop = stop;
39
+ session.held.pause = true;
40
+ session.held.pauseEditRole = role;
41
+ session.held.pauseEditWake = wake;
42
+ session.held.pauseEditStop = stop;
43
+ session.input = "";
44
+ session.cursor = 0;
45
+ session.flashMessage(hint);
46
+ };
47
+ const handleKey = (_str, key, ctx) => {
48
+ if (key.ctrl && key.name === "f" &&
49
+ !session.held.budget && !session.held.rolePromptFor && !session.held.search &&
50
+ !session.held.queued && !session.held.answer && !session.held.entry &&
51
+ !session.held.attach && !session.held.notes && !session.held.retire && !session.held.quiet && !session.held.interval) {
52
+ // Ctrl+F toggles the whole-team pause (mnemonic: **F**leet) through the same
53
+ // pauseTeam/resumeTeam marker writers `tumwater pause`/`resume` and the dashboard's
54
+ // /api/pause call use, so the surfaces cannot drift on marker format or idempotence.
55
+ // The shared teamPausePhrase/teamResumePhrase base lines keep the flash wording the
56
+ // event feed and failure digest already read; a no-op flashes the same
57
+ // already-paused/was-not-paused contract the CLI prints. Guarded out of the budget and
58
+ // role-prompt editors, whose prompt line the team toggle must not disturb — the same
59
+ // inertness the per-loop keys honor below. The write can fail (a lock timeout, a torn
60
+ // fs), so flash the reason instead of letting the throw kill the TUI.
61
+ if (pauseEditMode) {
62
+ exitPauseEditMode();
63
+ return true;
64
+ }
65
+ try {
66
+ if (isTeamPaused(session.root)) {
67
+ session.flashMessage(resumeTeam(session.root) ? teamResumePhrase() : "team was not paused");
68
+ }
69
+ else {
70
+ // An unpaused team opens the one-line editor: "[duration] [reason]", empty pauses
71
+ // until resumed. A paused team resumes at once.
72
+ openPauseEditor(null, false, false, 'pause the team: [duration] [reason], e.g. "2h deploy freeze" (empty pauses until resumed) — Enter to pause, Esc to cancel');
73
+ }
74
+ }
75
+ catch (err) {
76
+ session.flashError(err);
77
+ }
78
+ return true;
79
+ }
80
+ if (key.ctrl && key.name === "q" &&
81
+ !session.held.budget && !session.held.rolePromptFor && !session.held.search &&
82
+ !session.held.queued && !session.held.answer && !session.held.entry &&
83
+ !session.held.attach && !session.held.notes && !session.held.retire && !session.held.quiet && !session.held.interval) {
84
+ // Ctrl+Q (mnemonic: **q**uit later) schedules or cancels a graceful team stop through
85
+ // the same requestScheduledStop/cancelScheduledStopMessage cores `tumwater stop --in` /
86
+ // `--cancel` and the dashboard use. A pending stop cancels at once; otherwise it opens
87
+ // the duration editor. A second press closes any open pause/wake/stop editor unwritten.
88
+ if (pauseEditMode) {
89
+ exitPauseEditMode();
90
+ return true;
91
+ }
92
+ try {
93
+ if (scheduledStopAt(session.root) !== undefined) {
94
+ session.flashMessage(cancelScheduledStopMessage(session.root));
95
+ }
96
+ else {
97
+ // The same one-line entry state as the Ctrl+F and per-role editors, scoped to the
98
+ // stop intent (no role pinned, stop mode on): nothing is written until Enter.
99
+ openPauseEditor(null, false, true, 'stop the team later: a duration, e.g. "2h" — current runs finish, then the team stops — Enter to schedule, Esc to cancel');
100
+ }
101
+ }
102
+ catch (err) {
103
+ session.flashError(err);
104
+ }
105
+ return true;
106
+ }
107
+ if (key.ctrl && key.name === "r" && ctx.view === 0 && !pauseEditMode &&
108
+ !session.held.budget && !session.held.rolePromptFor && !session.held.search &&
109
+ !session.held.queued && !session.held.answer && !session.held.entry &&
110
+ !session.held.attach && !session.held.retire) {
111
+ // Ctrl+R on the Activity view applies the pending self-redeploy now — the terminal twin
112
+ // of `tumwater restart` and the dashboard update alert's restart button, all three
113
+ // through the same requestRestart marker writer. A refusal carries the CLI's wording
114
+ // (a red main, failed compile, swap error, or boot refusal still blocks; a fresh build
115
+ // refuses as "current with main"), so the flash matches what a terminal press would
116
+ // print. Inert on every other view (the transcript's Ctrl+R still opens the per-role
117
+ // prompt editor, Backlog's still releases an entry, Pending/Health's still refresh) and
118
+ // out of pause-edit mode, whose prompt line the restart must not disturb. The write can
119
+ // fail like any marker write, so flash the reason instead of letting the throw escape.
120
+ try {
121
+ const result = requestRestart(session.root);
122
+ session.flashMessage(result.ok ? result.message : result.error);
123
+ }
124
+ catch (err) {
125
+ session.flashError(err);
126
+ }
127
+ return true;
128
+ }
129
+ // Per-loop controls on the transcript pane: Ctrl+P toggles the viewed loop's pause,
130
+ // Ctrl+A aborts its in-flight tick, Ctrl+V zeroes its counters, Ctrl+W opens the wake
131
+ // editor (Ctrl+R opens the role-prompt editor for it — the prompt editor's arms handle
132
+ // that key). They call the
133
+ // same marker-writing/submit cores the CLI's --role flags do, so the surfaces cannot
134
+ // drift on marker format, idempotence, or wording. Guarded to the transcript views and
135
+ // out of budget-edit mode; everywhere else the keys fall through to applyKey. Every
136
+ // branch is a disk write that can fail (a lock timeout, a torn fs), and an unguarded
137
+ // throw would escape this keypress handler and kill the TUI — flash the reason instead.
138
+ if (key.ctrl && (key.name === "p" || key.name === "w") && pauseEditRole !== null) {
139
+ // A second Ctrl+P or Ctrl+W closes the per-loop pause/wake editor, mirroring Ctrl+F's
140
+ // second press.
141
+ exitPauseEditMode();
142
+ return true;
143
+ }
144
+ if (!session.held.budget &&
145
+ !session.held.pause &&
146
+ !session.held.queued && !session.held.answer && !session.held.entry &&
147
+ !session.held.notes && !session.held.retire && !session.held.quiet && !session.held.interval &&
148
+ ctx.roleIds.length > 0 &&
149
+ ctx.view >= 1 && ctx.view <= ctx.roleIds.length &&
150
+ key.ctrl && (key.name === "p" || key.name === "a" || key.name === "w" || key.name === "v")) {
151
+ const role = ctx.roleIds[ctx.view - 1]; // defined: view is clamped inside the transcript range
152
+ try {
153
+ if (key.name === "p") {
154
+ // Toggle by the marker's current state, read fresh: pauseRole/resumeRole's false
155
+ // return means another window raced us to the same state, and the wording
156
+ // helpers render that honestly — the changed-state contract the CLI prints.
157
+ if (pausedRoles(session.root).includes(role)) {
158
+ session.flashMessage(roleResumeMessage(session.root, role, resumeRole(session.root, role)));
159
+ }
160
+ else if (session.held.rolePromptFor || session.held.search) {
161
+ session.flashMessage(session.held.rolePromptFor ? session.busyFlash("role prompt") : session.busyFlash("search"));
162
+ }
163
+ else {
164
+ // An unpaused loop opens the one-line duration editor; nothing is written until
165
+ // Enter. Empty pauses until resumed.
166
+ openPauseEditor(role, false, false, `pause ${role}: [duration], e.g. "2h" (empty pauses until resumed) — Enter to pause, Esc to cancel`);
167
+ }
168
+ }
169
+ else if (key.name === "a") {
170
+ const result = requestAbort(session.root, role);
171
+ session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
172
+ }
173
+ else if (key.name === "v") {
174
+ session.flashMessage(requestResetCounters(session.root, [role]));
175
+ }
176
+ else if (session.held.rolePromptFor || session.held.search) {
177
+ session.flashMessage(session.held.rolePromptFor ? session.busyFlash("role prompt") : session.busyFlash("search"));
178
+ }
179
+ else {
180
+ // Ctrl+W opens the one-line wake editor; nothing is written until Enter. Empty wakes now.
181
+ openPauseEditor(role, true, false, `wake ${role}: [duration], e.g. "2h", or "cancel" (empty wakes now) — Enter to apply, Esc to cancel`);
182
+ }
183
+ }
184
+ catch (err) {
185
+ session.flashError(err);
186
+ }
187
+ return true;
188
+ }
189
+ if (key.name === "escape" && pauseEditMode) {
190
+ // Esc cancels the pause editor without writing, restoring the previous prompt text.
191
+ exitPauseEditMode();
192
+ return true;
193
+ }
194
+ if (key.name === "return" && pauseEditMode) {
195
+ // Enter in pause-edit mode parses "[duration] [reason]" and writes the same marker
196
+ // `tumwater pause --for/--reason` does. An invalid entry flashes and STAYS in the editor.
197
+ const target = pauseEditRole;
198
+ const stopParsed = pauseEditStop ? parseStopInInput(session.input) : null;
199
+ const wakeParsed = pauseEditWake && target !== null ? parseRoleWakeInput(session.input) : null;
200
+ const parsed = target !== null ? parseRolePauseInput(session.input) : parsePauseInput(session.input);
201
+ if (stopParsed) {
202
+ if (!stopParsed.ok) {
203
+ session.flashMessage(stopParsed.error);
204
+ }
205
+ else {
206
+ try {
207
+ const result = requestScheduledStop(session.root, stopParsed.inMs);
208
+ exitPauseEditMode();
209
+ session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
210
+ }
211
+ catch (err) {
212
+ session.flashError(err);
213
+ }
214
+ }
215
+ }
216
+ else if (wakeParsed) {
217
+ if (!wakeParsed.ok) {
218
+ session.flashMessage(wakeParsed.error);
219
+ }
220
+ else {
221
+ try {
222
+ const message = wakeParsed.cancel ? cancelWakes(session.root, target) : requestWake(session.root, [target], wakeParsed.inMs);
223
+ exitPauseEditMode();
224
+ session.flashMessage(message);
225
+ }
226
+ catch (err) {
227
+ session.flashError(err);
228
+ }
229
+ }
230
+ }
231
+ else if (!parsed.ok) {
232
+ session.flashMessage(parsed.error);
233
+ }
234
+ else if (target !== null) {
235
+ try {
236
+ const at = session.now();
237
+ const until = parsed.forMs === undefined ? undefined : at + parsed.forMs;
238
+ const changed = pauseRole(session.root, target, until);
239
+ exitPauseEditMode();
240
+ session.flashMessage(rolePauseMessage(session.root, target, changed, until === undefined ? undefined : { ms: parsed.forMs, untilMs: until }, at));
241
+ }
242
+ catch (err) {
243
+ session.flashError(err);
244
+ }
245
+ }
246
+ else {
247
+ const team = parsed;
248
+ try {
249
+ const until = team.forMs === undefined ? undefined : session.now() + team.forMs;
250
+ const changed = pauseTeam(session.root, until, team.reason);
251
+ exitPauseEditMode();
252
+ const forSuffix = team.forMs === undefined ? "" : ` for ${durationLabel(team.forMs)}`;
253
+ session.flashMessage(changed || until !== undefined
254
+ ? `${teamPausePhrase()}${forSuffix}${changed && team.reason ? `: ${team.reason}` : ""}`
255
+ : "team is already paused");
256
+ }
257
+ catch (err) {
258
+ session.flashError(err);
259
+ }
260
+ }
261
+ return true;
262
+ }
263
+ return false;
264
+ };
265
+ return { handleKey, exit: exitPauseEditMode };
266
+ }
@@ -0,0 +1,95 @@
1
+ // vocabulary: plain
2
+ import { submitRolePromptAndWake } from "../../operator/operator-intent.js";
3
+ import { pushPromptHistory } from "./tui-prompt-history.js";
4
+ import { parseRolePromptInput } from "./tui-input.js";
5
+ /** The per-role prompt Ctrl+R editor on the shared prompt line. It owns its mode flag (the
6
+ * viewed loop's role id), its saved-draft pair, its exit helper, and the handleKey arms for its
7
+ * key: the per-loop toggle (guarded out of the other editors' prompt line) and its Esc/Enter
8
+ * leaves. Mutually exclusive with the other editors via the session's held registry. */
9
+ export function createPromptEditor(session) {
10
+ let rolePromptFor = null;
11
+ let roleSavedInput = "";
12
+ let roleSavedCursor = 0;
13
+ // Leave role-prompt mode (Esc, Ctrl+R again, or Ctrl+T): restore the saved prompt text,
14
+ // and reset the recall state with it — the saved draft was saved settled (see the Ctrl+R
15
+ // handler), so resuming a browse entered in role mode would restore role-mode text onto
16
+ // the director line and lose the director draft the arrows had saved.
17
+ const exitRolePromptMode = () => {
18
+ if (!rolePromptFor)
19
+ return;
20
+ rolePromptFor = null;
21
+ session.held.rolePromptFor = null;
22
+ session.restorePromptFrom(roleSavedInput, roleSavedCursor);
23
+ };
24
+ const handleKey = (_str, key, ctx) => {
25
+ // Ctrl+R toggles role-prompt mode for the viewed loop (mnemonic: p**R**ompt — Ctrl+P is
26
+ // taken by pause): entering saves the director draft in this mode's own pair and blanks
27
+ // the line; entering again (or Esc) restores it. Mutually exclusive with budget mode by
28
+ // the outer guard and the mirrored refusal in the Ctrl+B branch. Rides the same per-loop
29
+ // guard as the Ctrl+P/A/W pause controls so an editor's prompt line is never disturbed.
30
+ if (!session.held.budget &&
31
+ !session.held.pause &&
32
+ !session.held.queued && !session.held.answer && !session.held.entry &&
33
+ !session.held.notes && !session.held.quiet && !session.held.interval &&
34
+ ctx.roleIds.length > 0 &&
35
+ ctx.view >= 1 && ctx.view <= ctx.roleIds.length &&
36
+ key.ctrl && key.name === "r") {
37
+ const role = ctx.roleIds[ctx.view - 1]; // defined: view is clamped inside the transcript range
38
+ try {
39
+ if (rolePromptFor) {
40
+ exitRolePromptMode();
41
+ session.flashMessage("role prompt cancelled");
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 role editor saves (and later restores) the
46
+ // draft the operator was writing, and no stale index/draft crosses modes —
47
+ // otherwise Esc here would surface role-mode recalls on the director line.
48
+ session.settleDraft((t) => (roleSavedInput = t), (n) => (roleSavedCursor = n));
49
+ rolePromptFor = role;
50
+ session.held.rolePromptFor = role;
51
+ session.input = "";
52
+ session.cursor = 0;
53
+ session.flashMessage(`prompt for ${role}: Enter to send, Esc to cancel`);
54
+ }
55
+ }
56
+ catch (err) {
57
+ session.flashError(err);
58
+ }
59
+ return true;
60
+ }
61
+ if (key.name === "escape" && rolePromptFor) {
62
+ // Esc cancels role-prompt mode the same way, restoring the previous prompt text.
63
+ exitRolePromptMode();
64
+ return true;
65
+ }
66
+ if (key.name === "return" && rolePromptFor) {
67
+ // Enter in role-prompt mode submits for the viewed loop through the same path
68
+ // `tumwater prompt --role` uses: the shared submitRolePromptAndWake enqueues into
69
+ // that loop's own queue (length-capped by the shared rule), logs under the loop,
70
+ // and wakes it so a live team's loop comes in within one poll. An invalid
71
+ // (whitespace-only) line flashes and STAYS in edit mode; a failed queue write
72
+ // flashes the reason and keeps the text, the same contract the director path
73
+ // honors; success restores the saved draft.
74
+ const parsed = parseRolePromptInput(session.input);
75
+ if (!parsed.ok) {
76
+ session.flashMessage(parsed.error);
77
+ }
78
+ else {
79
+ const role = rolePromptFor;
80
+ try {
81
+ submitRolePromptAndWake(session.root, role, parsed.value, undefined);
82
+ session.promptHistory = pushPromptHistory(session.promptHistory, parsed.value);
83
+ exitRolePromptMode();
84
+ session.flashMessage(`added to ${role}'s inbox`);
85
+ }
86
+ catch (err) {
87
+ session.flashError(err);
88
+ }
89
+ }
90
+ return true;
91
+ }
92
+ return false;
93
+ };
94
+ return { handleKey, exit: exitRolePromptMode };
95
+ }
@@ -0,0 +1,98 @@
1
+ // vocabulary: plain
2
+ import { editQueuedFile, readQueuedPromptBody } from "../../inbox/inbox-edit.js";
3
+ import { parseRolePromptInput } from "./tui-input.js";
4
+ /** The Queued pane's in-place row editor (Ctrl+E) on the shared prompt line. It owns its target
5
+ * (`queuedEditFor`), its saved-draft pair, its exit helper, and the readQueuedPromptBody /
6
+ * editQueuedFile leaves that pre-fill the line and write it back. Mutually exclusive with the
7
+ * other editors via the session's held registry. */
8
+ export function createQueuedEditor(session) {
9
+ let queuedEditFor = null;
10
+ let queuedSavedInput = "";
11
+ let queuedSavedCursor = 0;
12
+ // Leave the Queued pane's in-place editor (Esc, a saved edit, or Ctrl+T): restore the saved
13
+ // prompt text, and reset the recall state with it — the same settle contract the other
14
+ // editors honor.
15
+ const exitQueuedEdit = () => {
16
+ if (!queuedEditFor)
17
+ return;
18
+ queuedEditFor = null;
19
+ session.held.queued = null;
20
+ session.restorePromptFrom(queuedSavedInput, queuedSavedCursor);
21
+ };
22
+ const handleKey = (_str, key, ctx) => {
23
+ if (key.ctrl && key.name === "e" &&
24
+ !session.held.budget && !session.held.rolePromptFor && !session.held.search &&
25
+ !session.held.pause && queuedEditFor === null &&
26
+ !session.held.answer && !session.held.entry &&
27
+ ctx.view === ctx.roleIds.length + 6 && ctx.selectedQueued !== null) {
28
+ // Ctrl+E opens the Queued pane's in-place editor on the selected row (mnemonic:
29
+ // **E**dit) through the same readQueuedPromptBody/editQueuedFile pair the dashboard's
30
+ // Edit uses: the body is pre-filled marker- and attachment-stripped, and Enter writes
31
+ // it back to the same queue file, so its position, enqueue stamp, deferral, and
32
+ // attached images survive. A row that vanished between selection and this press (the
33
+ // loop dequeued it, or another operator cancelled it) flashes the gone notice and
34
+ // refreshes the list. A no-selection press (list mode) falls through to the report
35
+ // window below, exactly as before.
36
+ try {
37
+ const position = Math.min(ctx.selectedQueued, ctx.queuedRows.length - 1);
38
+ const row = ctx.queuedRows[position];
39
+ if (row !== undefined) {
40
+ const body = readQueuedPromptBody(session.root, row.role, row.file);
41
+ if (body === null) {
42
+ session.flashMessage(`inbox message #${row.position} for ${row.role} is gone`);
43
+ session.refreshQueued();
44
+ }
45
+ else {
46
+ // Mid-recall, the line holds a history entry while the real draft sits in the
47
+ // recall state: settle first, so the queued editor saves (and later restores)
48
+ // the draft the operator was writing.
49
+ session.settleDraft((t) => (queuedSavedInput = t), (n) => (queuedSavedCursor = n));
50
+ queuedEditFor = { role: row.role, file: row.file };
51
+ session.held.queued = queuedEditFor;
52
+ session.input = body;
53
+ session.cursor = body.length;
54
+ session.flashMessage(`editing inbox message #${row.position} for ${row.role} — Enter to save, Esc to cancel`);
55
+ }
56
+ }
57
+ }
58
+ catch (err) {
59
+ session.flashError(err);
60
+ }
61
+ return true;
62
+ }
63
+ if (key.name === "escape" && queuedEditFor) {
64
+ // Esc cancels the Queued pane's in-place editor, restoring the previous prompt text.
65
+ exitQueuedEdit();
66
+ return true;
67
+ }
68
+ if (key.name === "return" && queuedEditFor) {
69
+ // Enter saves the queued editor's line through the same file-addressed
70
+ // editQueuedFile pair the dashboard's Edit uses: the write targets the queue file
71
+ // the row named, so a concurrent dequeue cannot land it on the wrong prompt. An
72
+ // invalid (whitespace-only) line flashes and STAYS in the editor; a gone outcome
73
+ // is the concurrent-dequeue race, flashed honestly and returned to the list; a
74
+ // throw keeps the text in the editor for a retry.
75
+ const parsed = parseRolePromptInput(session.input);
76
+ if (!parsed.ok) {
77
+ session.flashMessage(parsed.error);
78
+ }
79
+ else {
80
+ const target = queuedEditFor;
81
+ try {
82
+ const outcome = editQueuedFile(session.root, target.role, target.file, parsed.value);
83
+ exitQueuedEdit();
84
+ session.refreshQueued();
85
+ session.flashMessage(outcome.status === "edited"
86
+ ? `edited inbox message for ${target.role}`
87
+ : "the inbox message was already gone");
88
+ }
89
+ catch (err) {
90
+ session.flashError(err);
91
+ }
92
+ }
93
+ return true;
94
+ }
95
+ return false;
96
+ };
97
+ return { handleKey, exit: exitQueuedEdit };
98
+ }
@@ -0,0 +1,106 @@
1
+ // vocabulary: plain
2
+ import { setConfigKey, unsetConfigKey } from "../../config/config-write.js";
3
+ import { loadConfigSafe } from "../../config/config.js";
4
+ import { DIRECTOR_ROLE } from "../../roles/roles.js";
5
+ import { baseRoleOf } from "../../roles/loop-ids.js";
6
+ /** The Ctrl+O quiet-hours editor on the shared prompt line, the terminal twin of the dashboard
7
+ * role drawer's `Quiet hours` row and `tumwater role <id> --quiet-hours`. It owns its mode
8
+ * flag, its saved-draft pair, and the role pinned when it opened, so a roster shift cannot
9
+ * redirect the write. Enter writes through the shared setConfigKey/unsetConfigKey family the
10
+ * CLI and the drawer use — so checkQuietHours (naming `quietHoursPerRole.<id>`) and
11
+ * validateConfig own the window's truth — and Esc, Ctrl+O again, and Ctrl+T cancel by
12
+ * restoring the prompt draft. Mutually exclusive with the other editors on the one prompt
13
+ * line via the session's held registry. */
14
+ export function createQuietEditor(session) {
15
+ let quietMode = false;
16
+ let savedInput = "";
17
+ let savedCursor = 0;
18
+ // The role scope pinned when the editor opened, so Enter writes the same role's window even
19
+ // if syncRoster shrinks or reorders the roster while the editor is open.
20
+ let lockedRole = null;
21
+ // Leave quiet-edit mode (Esc, Ctrl+O again, or Ctrl+T): restore the saved prompt text.
22
+ const exitQuietMode = () => {
23
+ if (!quietMode)
24
+ return;
25
+ quietMode = false;
26
+ session.held.quiet = false;
27
+ session.restorePromptFrom(savedInput, savedCursor);
28
+ lockedRole = null;
29
+ };
30
+ const handleKey = (_str, key, ctx) => {
31
+ if (key.ctrl && key.name === "o") {
32
+ // Ctrl+O edits the viewed role's own quiet-hours window (mnemonic: the silent *o*ur
33
+ // hours) on its Transcript view. Every non-transcript view keeps Ctrl+O — the guard
34
+ // lets the key fall through to the pane arms (Activity's event-type cycle, History's
35
+ // outcome cycle, Health's problems-only toggle) unless the quiet editor already holds
36
+ // the line, in which case Ctrl+O is the toggle-out arm below. Entering pre-fills the
37
+ // window from `quietHoursPerRole.<base>` (empty when unset — empty means "off" on
38
+ // save) and saves the prompt text; leaving restores it. The director is exempt — its
39
+ // quietHoursPerRole.director entry is inert (the gate and doctor both exempt it).
40
+ if (!quietMode && !(ctx.view >= 1 && ctx.view <= ctx.roleIds.length))
41
+ return false;
42
+ const role = ctx.roleIds[ctx.view - 1];
43
+ const held = session.heldEditorFlash("quiet");
44
+ if (held) {
45
+ // Mutually exclusive with the other editors: all share one prompt line, so a mode may
46
+ // not enter while another holds it. Flash the way out first.
47
+ session.flashMessage(held);
48
+ return true;
49
+ }
50
+ if (role === DIRECTOR_ROLE) {
51
+ session.flashMessage("the director has no quiet hours");
52
+ return true;
53
+ }
54
+ if (quietMode) {
55
+ exitQuietMode();
56
+ return true;
57
+ }
58
+ // Pin the role first so the write below is safe against a mid-edit roster shift, then
59
+ // pre-fill from the file's own entry — one fresh read per open is the smallest honest
60
+ // source; a broken config degrades to no pre-fill and the write reports the error.
61
+ lockedRole = role;
62
+ const existing = loadConfigSafe(session.root).config?.quietHoursPerRole?.[baseRoleOf(role)] ?? "";
63
+ session.settleDraft((t) => (savedInput = t), (n) => (savedCursor = n));
64
+ quietMode = true;
65
+ session.held.quiet = true;
66
+ session.input = existing;
67
+ session.cursor = session.input.length;
68
+ session.flashMessage(`edit ${role}'s quiet hours (e.g. 23:00-07:00, empty clears): Enter to save, Esc to cancel`);
69
+ return true;
70
+ }
71
+ if (key.name === "escape" && quietMode) {
72
+ // Esc cancels quiet-edit mode, restoring the previous prompt text.
73
+ exitQuietMode();
74
+ return true;
75
+ }
76
+ if (key.name === "return" && quietMode) {
77
+ // Enter commits the window through the shared setter family the CLI's `--quiet-hours`
78
+ // and the dashboard drawer use: an empty or whitespace-only line clears the key
79
+ // (`unsetConfigKey`), otherwise `setConfigKey` writes the dotted `quietHoursPerRole
80
+ // .<base>` entry as a JSON string. checkQuietHours (naming the dotted key) rejects a
81
+ // malformed window and validateConfig guards the write; both write tumwater.json
82
+ // atomically and the running orchestrator picks the change up on its next poll. An
83
+ // invalid window flashes and STAYS in edit mode so the operator can fix it; success
84
+ // restores the saved prompt text.
85
+ const role = lockedRole;
86
+ const cfgKey = `quietHoursPerRole.${baseRoleOf(role)}`;
87
+ const text = session.input.trim();
88
+ const result = text === ""
89
+ ? unsetConfigKey(session.root, cfgKey)
90
+ : setConfigKey(session.root, cfgKey, JSON.stringify(text));
91
+ if (result.ok) {
92
+ exitQuietMode();
93
+ session.flashMessage(text === "" ? `${role}: quiet hours cleared` : `${role}: quiet hours set to ${text}`);
94
+ }
95
+ else {
96
+ session.flashMessage(result.error); // broken config or write failure — stay in edit mode
97
+ }
98
+ return true;
99
+ }
100
+ return false;
101
+ };
102
+ return {
103
+ handleKey,
104
+ exit: exitQuietMode,
105
+ };
106
+ }
@@ -0,0 +1,83 @@
1
+ // vocabulary: plain
2
+ import { artifactPhrase, retireRole } from "../../operator/retire.js";
3
+ /** The Health view's Ctrl+X retire editor on the shared prompt line, the terminal twin of
4
+ * `tumwater retire --role <id>` and the dashboard Settings card's Retire button. It owns its
5
+ * `retireMode` flag, its saved-draft pair, and the handleKey arms for its key (the toggle with
6
+ * the `heldEditorFlash(entering)` guard clause — the shared guard priority lives in the session
7
+ * module — plus Esc and Enter). Guarded to the Health view (`ctx.view === roleIds.length + 7`),
8
+ * so every other view's Ctrl+X arm (Transcript clears notes, Backlog cancels an entry, Inbox
9
+ * cancels a queued prompt) keeps the key. Enter runs the same `retireRole(session.root, role,
10
+ * {})` core the CLI and dashboard call, with the same never-force policy, and flashes the
11
+ * dashboard's own outcome wording; a safety-rail refusal or an empty id flashes and stays open
12
+ * so the operator can fix the id, Esc cancels and restores the draft. */
13
+ export function createRetireEditor(session) {
14
+ let retireMode = false;
15
+ let retireSavedInput = "";
16
+ let retireSavedCursor = 0;
17
+ // Leave retire-edit mode (Esc, Enter's success path, or Ctrl+T): restore the saved prompt text.
18
+ const exitRetireMode = () => {
19
+ if (!retireMode)
20
+ return;
21
+ retireMode = false;
22
+ session.held.retire = false;
23
+ session.restorePromptFrom(retireSavedInput, retireSavedCursor);
24
+ };
25
+ const handleKey = (_str, key, ctx) => {
26
+ if (key.ctrl && key.name === "x" && ctx.view === ctx.roleIds.length + 7 && !retireMode) {
27
+ // Ctrl+X toggles retire-edit mode on the Health view's prompt line (mnemonic: **x**
28
+ // remove — the same key the Transcript notebook-clear, Backlog cancel, and Inbox cancel
29
+ // arms use on their own views, guarded away here by the Health index). The line edits one
30
+ // role id; Enter retires that role's leftovers through the same retireRole core the CLI
31
+ // and the dashboard button use. Entering saves the prompt draft and blanks the line;
32
+ // leaving restores it.
33
+ const held = session.heldEditorFlash("retire");
34
+ if (held) {
35
+ // Mutually exclusive with the other editors: all share one prompt line, so a mode may
36
+ // not enter while another holds it. Flash the way out instead of silently dropping
37
+ // the held mode's saved draft.
38
+ session.flashMessage(held);
39
+ return true;
40
+ }
41
+ // Mid-recall, hand the arrows' saved draft back before the retire editor takes the line —
42
+ // the same settle the role editors do — so leaving restores the draft, not a
43
+ // history entry, with no stale recall state riding along.
44
+ session.settleDraft((t) => (retireSavedInput = t), (n) => (retireSavedCursor = n));
45
+ retireMode = true;
46
+ session.held.retire = true;
47
+ session.input = "";
48
+ session.cursor = 0;
49
+ session.flashMessage("retire a disabled role's leftovers: type the role id, Enter to retire, Esc to cancel");
50
+ return true;
51
+ }
52
+ if (key.name === "escape" && retireMode) {
53
+ // Esc cancels retire-edit mode, restoring the previous prompt text and removing nothing.
54
+ exitRetireMode();
55
+ session.flashMessage("retire cancelled");
56
+ return true;
57
+ }
58
+ if (key.name === "return" && retireMode) {
59
+ // Enter retires the typed role id through retireRole — the same core, and the same
60
+ // never-force policy, the CLI's `tumwater retire --role <id>` and the dashboard's Retire
61
+ // button use — then flashes the dashboard's own outcome wording. An empty id flashes and
62
+ // STAYS in the editor so the operator can type one; a safety-rail refusal throws (the
63
+ // role still enabled, unmerged commits, a dirty or unusable worktree, a run in progress)
64
+ // and the cursor stays so the operator can fix the id or reach for the CLI's --force.
65
+ const role = session.input.trim();
66
+ if (role === "") {
67
+ session.flashMessage("a role id is required");
68
+ return true;
69
+ }
70
+ void retireRole(session.root, role, {}).then(({ removed }) => {
71
+ exitRetireMode();
72
+ session.flashMessage(removed.length === 0
73
+ ? `nothing to remove for role ${role}`
74
+ : removed.map((a) => `removed ${artifactPhrase(a, role)}`).join("; "));
75
+ }, (err) => {
76
+ session.flashError(err);
77
+ });
78
+ return true;
79
+ }
80
+ return false;
81
+ };
82
+ return { handleKey, exit: exitRetireMode };
83
+ }