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
@@ -3,6 +3,7 @@ import { isTeamPaused, pausedRoles, pauseTeam, pauseRole, resumeTeam, resumeRole
3
3
  import { durationLabel } from "../../cli/cli-args.js";
4
4
  import { teamPausePhrase, teamResumePhrase } from "../../text/phrases.js";
5
5
  import { parsePauseInput, parseRolePauseInput, parseRoleWakeInput, parseStopInInput, } from "./tui-input.js";
6
+ import { viewedRole } from "./tui-editor-ctx.js";
6
7
  /** The pause/stop/wake editor on the shared prompt line: the whole-team Ctrl+F pause and Ctrl+Q
7
8
  * stop editors, the per-role Ctrl+P pause / Ctrl+A abort / Ctrl+W wake controls, and the
8
9
  * parse/dispatch leaves those write. It owns its mode flags (pauseEditMode/`pauseEditRole`/
@@ -42,213 +43,239 @@ export function createPauseEditor(session) {
42
43
  session.cursor = 0;
43
44
  session.flashMessage(hint);
44
45
  };
45
- const handleKey = (_str, key, ctx) => {
46
- if (key.ctrl && key.name === "f" &&
47
- !session.anyEditorHeld("pause")) {
48
- // Ctrl+F toggles the whole-team pause (mnemonic: **F**leet) through the same
49
- // pauseTeam/resumeTeam marker writers `tumwater pause`/`resume` and the dashboard's
50
- // /api/pause call use, so the surfaces cannot drift on marker format or idempotence.
51
- // The shared teamPausePhrase/teamResumePhrase base lines keep the flash wording the
52
- // event feed and failure digest already read; a no-op flashes the same
53
- // already-paused/was-not-paused contract the CLI prints. Guarded out of the budget and
54
- // role-prompt editors, whose prompt line the team toggle must not disturb — the same
55
- // inertness the per-role keys honor below. The write can fail (a lock timeout, a torn
56
- // fs), so flash the reason instead of letting the throw kill the TUI.
57
- if (pauseEditMode) {
58
- exitPauseEditMode();
59
- return true;
60
- }
61
- try {
62
- if (isTeamPaused(session.root)) {
63
- session.flashMessage(resumeTeam(session.root) ? teamResumePhrase() : "team was not paused");
64
- }
65
- else {
66
- // An unpaused team opens the one-line editor: "[duration] [reason]", empty pauses
67
- // until resumed. A paused team resumes at once.
68
- openPauseEditor(null, false, false, 'pause the team: [duration] [reason], e.g. "2h deploy freeze" (empty pauses until resumed) — Enter to pause, Esc to cancel');
69
- }
70
- }
71
- catch (err) {
72
- session.flashError(err);
73
- }
46
+ // Ctrl+F toggles the whole-team pause (mnemonic: **F**leet) through the same
47
+ // pauseTeam/resumeTeam marker writers `tumwater pause`/`resume` and the dashboard's
48
+ // /api/pause call use, so the surfaces cannot drift on marker format or idempotence.
49
+ // The shared teamPausePhrase/teamResumePhrase base lines keep the flash wording the
50
+ // event feed and failure digest already read; a no-op flashes the same
51
+ // already-paused/was-not-paused contract the CLI prints. Guarded out of the budget and
52
+ // role-prompt editors, whose prompt line the team toggle must not disturb — the same
53
+ // inertness the per-role keys honor below. The write can fail (a lock timeout, a torn
54
+ // fs), so flash the reason instead of letting the throw kill the TUI.
55
+ const teamPauseToggle = (_ctx) => {
56
+ if (pauseEditMode) {
57
+ exitPauseEditMode();
74
58
  return true;
75
59
  }
76
- if (key.ctrl && key.name === "q" &&
77
- !session.anyEditorHeld("pause")) {
78
- // Ctrl+Q (mnemonic: **q**uit later) schedules or cancels a graceful team stop through
79
- // the same requestScheduledStop/cancelScheduledStopMessage cores `tumwater stop --in` /
80
- // `--cancel` and the dashboard use. A pending stop cancels at once; otherwise it opens
81
- // the duration editor. A second press closes any open pause/wake/stop editor unwritten.
82
- if (pauseEditMode) {
83
- exitPauseEditMode();
84
- return true;
85
- }
86
- try {
87
- if (scheduledStopAt(session.root) !== undefined) {
88
- session.flashMessage(cancelScheduledStopMessage(session.root));
89
- }
90
- else {
91
- // The same one-line entry state as the Ctrl+F and per-role editors, scoped to the
92
- // stop intent (no role pinned, stop mode on): nothing is written until Enter.
93
- 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');
94
- }
60
+ try {
61
+ if (isTeamPaused(session.root)) {
62
+ session.flashMessage(resumeTeam(session.root) ? teamResumePhrase() : "team was not paused");
95
63
  }
96
- catch (err) {
97
- session.flashError(err);
64
+ else {
65
+ // An unpaused team opens the one-line editor: "[duration] [reason]", empty pauses
66
+ // until resumed. A paused team resumes at once.
67
+ openPauseEditor(null, false, false, 'pause the team: [duration] [reason], e.g. "2h deploy freeze" (empty pauses until resumed) — Enter to pause, Esc to cancel');
98
68
  }
69
+ }
70
+ catch (err) {
71
+ session.flashError(err);
72
+ }
73
+ return true;
74
+ };
75
+ // Ctrl+Q (mnemonic: **q**uit later) schedules or cancels a graceful team stop through
76
+ // the same requestScheduledStop/cancelScheduledStopMessage cores `tumwater stop --in` /
77
+ // `--cancel` and the dashboard use. A pending stop cancels at once; otherwise it opens
78
+ // the duration editor. A second press closes any open pause/wake/stop editor unwritten.
79
+ const stopLaterToggle = (_ctx) => {
80
+ if (pauseEditMode) {
81
+ exitPauseEditMode();
99
82
  return true;
100
83
  }
101
- if (key.ctrl && key.name === "r" && ctx.view === 0 && !pauseEditMode &&
102
- !session.anyEditorHeld("pause")) {
103
- // Ctrl+R on the Activity view applies the pending self-redeploy now — the terminal twin
104
- // of `tumwater restart` and the dashboard update alert's restart button, all three
105
- // through the same requestRestart marker writer. A refusal carries the CLI's wording
106
- // (a red main, failed compile, swap error, or boot refusal still blocks; a fresh build
107
- // refuses as "current with main"), so the flash matches what a terminal press would
108
- // print. Inert on every other view (the transcript's Ctrl+R still opens the per-role
109
- // prompt editor, Backlog's still releases an entry, Pending/Health's still refresh) and
110
- // out of pause-edit mode, whose prompt line the restart must not disturb. The write can
111
- // fail like any marker write, so flash the reason instead of letting the throw escape.
112
- try {
113
- const result = requestRestart(session.root);
114
- session.flashMessage(result.ok ? result.message : result.error);
84
+ try {
85
+ if (scheduledStopAt(session.root) !== undefined) {
86
+ session.flashMessage(cancelScheduledStopMessage(session.root));
115
87
  }
116
- catch (err) {
117
- session.flashError(err);
88
+ else {
89
+ // The same one-line entry state as the Ctrl+F and per-role editors, scoped to the
90
+ // stop intent (no role pinned, stop mode on): nothing is written until Enter.
91
+ 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');
118
92
  }
119
- return true;
120
93
  }
121
- // Per-role controls on the transcript pane: Ctrl+P toggles the viewed role's pause,
122
- // Ctrl+A aborts its in-flight run, Ctrl+V zeroes its counters, Ctrl+W opens the wake
123
- // editor (Ctrl+R opens the role-prompt editor for it — the prompt editor's arms handle
124
- // that key). They call the
125
- // same marker-writing/submit cores the CLI's --role flags do, so the surfaces cannot
126
- // drift on marker format, idempotence, or wording. Guarded to the transcript views and
127
- // out of budget-edit mode; everywhere else the keys fall through to applyKey. Every
128
- // branch is a disk write that can fail (a lock timeout, a torn fs), and an unguarded
129
- // throw would escape this keypress handler and kill the TUI — flash the reason instead.
130
- if (key.ctrl && (key.name === "p" || key.name === "w") && pauseEditRole !== null) {
131
- // A second Ctrl+P or Ctrl+W closes the per-role pause/wake editor, mirroring Ctrl+F's
132
- // second press.
133
- exitPauseEditMode();
134
- return true;
94
+ catch (err) {
95
+ session.flashError(err);
135
96
  }
136
- if (!session.anyEditorHeld("rolePromptFor", "search") &&
137
- ctx.roleIds.length > 0 &&
138
- ctx.view >= 1 && ctx.view <= ctx.roleIds.length &&
139
- key.ctrl && (key.name === "p" || key.name === "a" || key.name === "w" || key.name === "v")) {
140
- const role = ctx.roleIds[ctx.view - 1]; // defined: view is clamped inside the transcript range
141
- try {
142
- if (key.name === "p") {
143
- // Toggle by the marker's current state, read fresh: pauseRole/resumeRole's false
144
- // return means another window raced us to the same state, and the wording
145
- // helpers render that honestly — the changed-state contract the CLI prints.
146
- if (pausedRoles(session.root).includes(role)) {
147
- session.flashMessage(roleResumeMessage(session.root, role, resumeRole(session.root, role)));
148
- }
149
- else if (session.held.rolePromptFor || session.held.search) {
150
- session.flashMessage(session.held.rolePromptFor ? session.busyFlash("role prompt") : session.busyFlash("search"));
151
- }
152
- else {
153
- // An unpaused role opens the one-line duration editor; nothing is written until
154
- // Enter. Empty pauses until resumed.
155
- openPauseEditor(role, false, false, `pause ${role}: [duration], e.g. "2h" (empty pauses until resumed) — Enter to pause, Esc to cancel`);
156
- }
157
- }
158
- else if (key.name === "a") {
159
- const result = requestAbort(session.root, role);
160
- session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
161
- }
162
- else if (key.name === "v") {
163
- session.flashMessage(requestResetCounters(session.root, [role]));
97
+ return true;
98
+ };
99
+ // Ctrl+R on the Activity view applies the pending self-redeploy now — the terminal twin
100
+ // of `tumwater restart` and the dashboard update alert's restart button, all three
101
+ // through the same requestRestart marker writer. A refusal carries the CLI's wording
102
+ // (a red main, failed compile, swap error, or boot refusal still blocks; a fresh build
103
+ // refuses as "current with main"), so the flash matches what a terminal press would
104
+ // print. Inert on every other view (the transcript's Ctrl+R still opens the per-role
105
+ // prompt editor, Backlog's still releases an entry, Pending/Health's still refresh) and
106
+ // out of pause-edit mode, whose prompt line the restart must not disturb. The write can
107
+ // fail like any marker write, so flash the reason instead of letting the throw escape.
108
+ const restartNow = (_ctx) => {
109
+ try {
110
+ const result = requestRestart(session.root);
111
+ session.flashMessage(result.ok ? result.message : result.error);
112
+ }
113
+ catch (err) {
114
+ session.flashError(err);
115
+ }
116
+ return true;
117
+ };
118
+ // A second Ctrl+P or Ctrl+W closes the per-role pause/wake editor, mirroring Ctrl+F's
119
+ // second press.
120
+ const closeRolePauseEdit = () => {
121
+ exitPauseEditMode();
122
+ return true;
123
+ };
124
+ // Per-role controls on the transcript pane: Ctrl+P toggles the viewed role's pause,
125
+ // Ctrl+A aborts its in-flight run, Ctrl+V zeroes its counters, Ctrl+W opens the wake
126
+ // editor (Ctrl+R opens the role-prompt editor for it — the prompt editor's arms handle
127
+ // that key). They call the
128
+ // same marker-writing/submit cores the CLI's --role flags do, so the surfaces cannot
129
+ // drift on marker format, idempotence, or wording. Guarded to the transcript views and
130
+ // out of budget-edit mode; everywhere else the keys fall through to applyKey. Every
131
+ // branch is a disk write that can fail (a lock timeout, a torn fs), and an unguarded
132
+ // throw would escape this keypress handler and kill the TUI — flash the reason instead.
133
+ const roleControlArm = (name, ctx) => {
134
+ const role = viewedRole(ctx); // defined: view is clamped inside the transcript range
135
+ try {
136
+ if (name === "p") {
137
+ // Toggle by the marker's current state, read fresh: pauseRole/resumeRole's false
138
+ // return means another window raced us to the same state, and the wording
139
+ // helpers render that honestly — the changed-state contract the CLI prints.
140
+ if (pausedRoles(session.root).includes(role)) {
141
+ session.flashMessage(roleResumeMessage(session.root, role, resumeRole(session.root, role)));
164
142
  }
165
143
  else if (session.held.rolePromptFor || session.held.search) {
166
144
  session.flashMessage(session.held.rolePromptFor ? session.busyFlash("role prompt") : session.busyFlash("search"));
167
145
  }
168
146
  else {
169
- // Ctrl+W opens the one-line wake editor; nothing is written until Enter. Empty wakes now.
170
- openPauseEditor(role, true, false, `wake ${role}: [duration], e.g. "2h", or "cancel" (empty wakes now) — Enter to apply, Esc to cancel`);
147
+ // An unpaused role opens the one-line duration editor; nothing is written until
148
+ // Enter. Empty pauses until resumed.
149
+ openPauseEditor(role, false, false, `pause ${role}: [duration], e.g. "2h" (empty pauses until resumed) — Enter to pause, Esc to cancel`);
171
150
  }
172
151
  }
173
- catch (err) {
174
- session.flashError(err);
152
+ else if (name === "a") {
153
+ const result = requestAbort(session.root, role);
154
+ session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
155
+ }
156
+ else if (name === "v") {
157
+ session.flashMessage(requestResetCounters(session.root, [role]));
175
158
  }
159
+ else if (session.held.rolePromptFor || session.held.search) {
160
+ session.flashMessage(session.held.rolePromptFor ? session.busyFlash("role prompt") : session.busyFlash("search"));
161
+ }
162
+ else {
163
+ // Ctrl+W opens the one-line wake editor; nothing is written until Enter. Empty wakes now.
164
+ openPauseEditor(role, true, false, `wake ${role}: [duration], e.g. "2h", or "cancel" (empty wakes now) — Enter to apply, Esc to cancel`);
165
+ }
166
+ }
167
+ catch (err) {
168
+ session.flashError(err);
169
+ }
170
+ return true;
171
+ };
172
+ // Esc cancels the pause editor without writing, restoring the previous prompt text.
173
+ const cancelPauseEdit = () => {
174
+ exitPauseEditMode();
175
+ return true;
176
+ };
177
+ // Enter in pause-edit mode parses "[duration] [reason]" and writes the same marker
178
+ // `tumwater pause --for/--reason` does. An invalid entry flashes and STAYS in the editor.
179
+ // It reads the current mode/draft and dispatches to the single named writer for the
180
+ // intent it resolves: a scheduled stop, a per-role wake, a per-role pause, or the team
181
+ // pause.
182
+ const commitPauseEntry = () => {
183
+ const target = pauseEditRole;
184
+ const stopParsed = pauseEditStop ? parseStopInInput(session.input) : null;
185
+ const wakeParsed = pauseEditWake && target !== null ? parseRoleWakeInput(session.input) : null;
186
+ const parsed = target !== null ? parseRolePauseInput(session.input) : parsePauseInput(session.input);
187
+ if (stopParsed) {
188
+ applyStopEntry(stopParsed);
176
189
  return true;
177
190
  }
178
- if (key.name === "escape" && pauseEditMode) {
179
- // Esc cancels the pause editor without writing, restoring the previous prompt text.
180
- exitPauseEditMode();
191
+ if (wakeParsed) {
192
+ applyWakeEntry(wakeParsed, target);
181
193
  return true;
182
194
  }
183
- if (key.name === "return" && pauseEditMode) {
184
- // Enter in pause-edit mode parses "[duration] [reason]" and writes the same marker
185
- // `tumwater pause --for/--reason` does. An invalid entry flashes and STAYS in the editor.
186
- const target = pauseEditRole;
187
- const stopParsed = pauseEditStop ? parseStopInInput(session.input) : null;
188
- const wakeParsed = pauseEditWake && target !== null ? parseRoleWakeInput(session.input) : null;
189
- const parsed = target !== null ? parseRolePauseInput(session.input) : parsePauseInput(session.input);
190
- if (stopParsed) {
191
- if (!stopParsed.ok) {
192
- session.flashMessage(stopParsed.error);
193
- return true;
194
- }
195
- try {
196
- const result = requestScheduledStop(session.root, stopParsed.inMs);
197
- exitPauseEditMode();
198
- session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
199
- }
200
- catch (err) {
201
- session.flashError(err);
202
- }
203
- return true;
204
- }
205
- if (wakeParsed) {
206
- if (!wakeParsed.ok) {
207
- session.flashMessage(wakeParsed.error);
208
- return true;
209
- }
210
- try {
211
- const message = wakeParsed.cancel ? cancelWakes(session.root, [target]) : requestWake(session.root, [target], wakeParsed.inMs);
212
- exitPauseEditMode();
213
- session.flashMessage(message);
214
- }
215
- catch (err) {
216
- session.flashError(err);
217
- }
218
- return true;
219
- }
220
- if (!parsed.ok) {
221
- session.flashMessage(parsed.error);
222
- return true;
223
- }
224
- if (target !== null) {
225
- try {
226
- const at = session.now();
227
- const until = parsed.forMs === undefined ? undefined : at + parsed.forMs;
228
- const changed = pauseRole(session.root, target, until);
229
- exitPauseEditMode();
230
- session.flashMessage(rolePauseMessage(session.root, target, changed, until === undefined ? undefined : { ms: parsed.forMs, untilMs: until }, at));
231
- }
232
- catch (err) {
233
- session.flashError(err);
234
- }
235
- return true;
236
- }
237
- const team = parsed;
238
- try {
239
- const until = team.forMs === undefined ? undefined : session.now() + team.forMs;
240
- const changed = pauseTeam(session.root, until, team.reason);
241
- exitPauseEditMode();
242
- const forSuffix = team.forMs === undefined ? "" : ` for ${durationLabel(team.forMs)}`;
243
- session.flashMessage(changed || until !== undefined
244
- ? `${teamPausePhrase()}${forSuffix}${changed && team.reason ? `: ${team.reason}` : ""}`
245
- : "team is already paused");
246
- }
247
- catch (err) {
248
- session.flashError(err);
249
- }
195
+ if (!parsed.ok) {
196
+ session.flashMessage(parsed.error);
250
197
  return true;
251
198
  }
199
+ if (target !== null) {
200
+ applyRolePauseEntry(parsed, target);
201
+ return true;
202
+ }
203
+ applyTeamPauseEntry(parsed);
204
+ return true;
205
+ };
206
+ const applyStopEntry = (stopParsed) => {
207
+ if (!stopParsed.ok) {
208
+ session.flashMessage(stopParsed.error);
209
+ return;
210
+ }
211
+ try {
212
+ const result = requestScheduledStop(session.root, stopParsed.inMs);
213
+ exitPauseEditMode();
214
+ session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
215
+ }
216
+ catch (err) {
217
+ session.flashError(err);
218
+ }
219
+ };
220
+ const applyWakeEntry = (wakeParsed, target) => {
221
+ if (!wakeParsed.ok) {
222
+ session.flashMessage(wakeParsed.error);
223
+ return;
224
+ }
225
+ try {
226
+ const message = wakeParsed.cancel ? cancelWakes(session.root, [target]) : requestWake(session.root, [target], wakeParsed.inMs);
227
+ exitPauseEditMode();
228
+ session.flashMessage(message);
229
+ }
230
+ catch (err) {
231
+ session.flashError(err);
232
+ }
233
+ };
234
+ const applyRolePauseEntry = (parsed, target) => {
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
+ const applyTeamPauseEntry = (parsed) => {
247
+ try {
248
+ const until = parsed.forMs === undefined ? undefined : session.now() + parsed.forMs;
249
+ const changed = pauseTeam(session.root, until, parsed.reason);
250
+ exitPauseEditMode();
251
+ const forSuffix = parsed.forMs === undefined ? "" : ` for ${durationLabel(parsed.forMs)}`;
252
+ session.flashMessage(changed || until !== undefined
253
+ ? `${teamPausePhrase()}${forSuffix}${changed && parsed.reason ? `: ${parsed.reason}` : ""}`
254
+ : "team is already paused");
255
+ }
256
+ catch (err) {
257
+ session.flashError(err);
258
+ }
259
+ };
260
+ const handleKey = (_str, key, ctx) => {
261
+ // Ordered dispatch: a guard miss falls through to the next arm, then to applyKey.
262
+ if (key.ctrl && key.name === "f" && !session.anyEditorHeld("pause"))
263
+ return teamPauseToggle(ctx);
264
+ if (key.ctrl && key.name === "q" && !session.anyEditorHeld("pause"))
265
+ return stopLaterToggle(ctx);
266
+ if (key.ctrl && key.name === "r" && ctx.view === 0 && !pauseEditMode && !session.anyEditorHeld("pause"))
267
+ return restartNow(ctx);
268
+ if (key.ctrl && (key.name === "p" || key.name === "w") && pauseEditRole !== null)
269
+ return closeRolePauseEdit();
270
+ if (!session.anyEditorHeld("rolePromptFor", "search") &&
271
+ ctx.roleIds.length > 0 &&
272
+ ctx.view >= 1 && ctx.view <= ctx.roleIds.length &&
273
+ key.ctrl && (key.name === "p" || key.name === "a" || key.name === "w" || key.name === "v"))
274
+ return roleControlArm(key.name, ctx);
275
+ if (key.name === "escape" && pauseEditMode)
276
+ return cancelPauseEdit();
277
+ if (key.name === "return" && pauseEditMode)
278
+ return commitPauseEntry();
252
279
  return false;
253
280
  };
254
281
  return { handleKey, exit: exitPauseEditMode };
@@ -1,6 +1,7 @@
1
1
  import { submitRolePromptAndWake } from "../../operator/operator-intent.js";
2
2
  import { pushPromptHistory } from "./tui-prompt-history.js";
3
3
  import { parseRolePromptInput } from "./tui-input.js";
4
+ import { viewedRole } from "./tui-editor-ctx.js";
4
5
  /** The per-role prompt Ctrl+R editor on the shared prompt line. It owns its mode flag (the
5
6
  * viewed role's role id), its saved-draft pair, its exit helper, and the handleKey arms for its
6
7
  * key: the per-role toggle (guarded out of the other editors' prompt line) and its Esc/Enter
@@ -19,17 +20,16 @@ export function createPromptEditor(session) {
19
20
  session.held.rolePromptFor = null;
20
21
  restoreDraft();
21
22
  };
22
- const handleKey = (_str, key, ctx) => {
23
- // Ctrl+R toggles role-prompt mode for the viewed role (mnemonic: p**R**ompt — Ctrl+P is
24
- // taken by pause): entering saves the director draft in this mode's own pair and blanks
25
- // the line; entering again (or Esc) restores it. Mutually exclusive with budget mode by
26
- // the outer guard and the mirrored refusal in the Ctrl+B branch. Rides the same per-role
27
- // guard as the Ctrl+P/A/W pause controls so an editor's prompt line is never disturbed.
23
+ // Ctrl+R toggles role-prompt mode for the viewed role (mnemonic: p**R**ompt — Ctrl+P is
24
+ // taken by pause): entering saves the director draft in this mode's own pair and blanks
25
+ // the line; entering again (or Esc) restores it. Mutually exclusive with budget mode by
26
+ // the outer guard and the mirrored refusal in the Ctrl+B branch. Rides the same per-role
27
+ // guard as the Ctrl+P/A/W pause controls so an editor's prompt line is never disturbed.
28
+ const toggleRolePromptEdit = (ctx) => {
28
29
  if (!session.anyEditorHeld("rolePromptFor") &&
29
30
  ctx.roleIds.length > 0 &&
30
- ctx.view >= 1 && ctx.view <= ctx.roleIds.length &&
31
- key.ctrl && key.name === "r") {
32
- const role = ctx.roleIds[ctx.view - 1]; // defined: view is clamped inside the transcript range
31
+ ctx.view >= 1 && ctx.view <= ctx.roleIds.length) {
32
+ const role = viewedRole(ctx); // defined: view is clamped inside the transcript range
33
33
  try {
34
34
  if (rolePromptFor) {
35
35
  exitRolePromptMode();
@@ -53,37 +53,48 @@ export function createPromptEditor(session) {
53
53
  }
54
54
  return true;
55
55
  }
56
- if (key.name === "escape" && rolePromptFor) {
57
- // Esc cancels role-prompt mode the same way, restoring the previous prompt text.
58
- exitRolePromptMode();
56
+ return false;
57
+ };
58
+ // Esc cancels role-prompt mode the same way, restoring the previous prompt text.
59
+ const cancelRolePromptEdit = () => {
60
+ exitRolePromptMode();
61
+ return true;
62
+ };
63
+ // Enter in role-prompt mode submits for the viewed role through the same path
64
+ // `tumwater prompt --role` uses: the shared submitRolePromptAndWake enqueues into
65
+ // that role's own queue (length-capped by the shared rule), logs under the role,
66
+ // and wakes it so a live team's role comes in within one poll. An invalid
67
+ // (whitespace-only) line flashes and STAYS in edit mode; a failed queue write
68
+ // flashes the reason and keeps the text, the same contract the director path
69
+ // honors; success restores the saved draft.
70
+ const submitRolePromptEdit = () => {
71
+ const role = rolePromptFor; // dispatch guarantees it is set; kept for TS narrowing
72
+ if (role === null)
59
73
  return true;
74
+ const parsed = parseRolePromptInput(session.input);
75
+ if (!parsed.ok) {
76
+ session.flashMessage(parsed.error);
60
77
  }
61
- if (key.name === "return" && rolePromptFor) {
62
- // Enter in role-prompt mode submits for the viewed role through the same path
63
- // `tumwater prompt --role` uses: the shared submitRolePromptAndWake enqueues into
64
- // that role's own queue (length-capped by the shared rule), logs under the role,
65
- // and wakes it so a live team's role comes in within one poll. An invalid
66
- // (whitespace-only) line flashes and STAYS in edit mode; a failed queue write
67
- // flashes the reason and keeps the text, the same contract the director path
68
- // honors; success restores the saved draft.
69
- const parsed = parseRolePromptInput(session.input);
70
- if (!parsed.ok) {
71
- session.flashMessage(parsed.error);
78
+ else {
79
+ try {
80
+ submitRolePromptAndWake(session.root, role, parsed.value, undefined);
81
+ session.promptHistory = pushPromptHistory(session.promptHistory, parsed.value);
82
+ exitRolePromptMode();
83
+ session.flashMessage(`added to ${role}'s inbox`);
72
84
  }
73
- else {
74
- const role = rolePromptFor;
75
- try {
76
- submitRolePromptAndWake(session.root, role, parsed.value, undefined);
77
- session.promptHistory = pushPromptHistory(session.promptHistory, parsed.value);
78
- exitRolePromptMode();
79
- session.flashMessage(`added to ${role}'s inbox`);
80
- }
81
- catch (err) {
82
- session.flashError(err);
83
- }
85
+ catch (err) {
86
+ session.flashError(err);
84
87
  }
85
- return true;
86
88
  }
89
+ return true;
90
+ };
91
+ const handleKey = (_str, key, ctx) => {
92
+ if (key.ctrl && key.name === "r")
93
+ return toggleRolePromptEdit(ctx);
94
+ if (key.name === "escape" && rolePromptFor)
95
+ return cancelRolePromptEdit();
96
+ if (key.name === "return" && rolePromptFor)
97
+ return submitRolePromptEdit();
87
98
  return false;
88
99
  };
89
100
  return { handleKey, exit: exitRolePromptMode };