tumwater 0.4.2 → 0.4.3

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 (305) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +1 -8
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -1,173 +1,6 @@
1
- /** The tail team alerts, in emission order: a run of quiet hours holding the team, a maintenance
2
- * allowance held at its rolling cap, a standing provider failure hold, an operator pause with
3
- * its countdown, the last day's capacity contention, a stale workspace pin, a fixed test that
4
- * regressed, a stopped tumwater, and roles that cannot or may not read images. One of the
5
- * per-domain builders split out of src/ui/team-alerts.ts; the shared types and wording helpers
6
- * live in team-alert-shared.ts. */
7
- import { listRoleIds, listRoles, openRoleActions, seeFailures } from "./team-alert-shared.js";
8
- import { stalePinPhrase } from "../doctor/doctor-contention-checks.js";
9
- import { quietWindowEnd } from "../scheduling/quiet-hours.js";
10
- import { pauseCountdown } from "./badges.js";
11
- import { agree, pauseReasonSuffix, plural } from "../text/phrases.js";
12
- import { humanUntil } from "../text/datetime.js";
13
- import { squash } from "../text/text.js";
14
- function pushQuietAlert(out, snap) {
15
- // Quiet hours (plans: "Quiet hours … part 2/2, observability") informs rather than asks:
16
- // the team holding to a schedule is not a problem, but an operator watching idle roles
17
- // inside the window needs the same explanation the pause alert gives a paused team —
18
- // why nothing runs, and when it starts again. The window end comes from quiet-hours.ts's
19
- // quietWindowEnd, the same helper quietBadge uses for the header.
20
- if (snap.inQuietHours && snap.quietHours) {
21
- const end = quietWindowEnd(snap.quietHours);
22
- out.push({
23
- key: "quiet",
24
- tone: "blue",
25
- title: `Quiet hours — roles start no new runs until ${end}`,
26
- detail: `The schedule (${snap.quietHours} local time, quietHours in tumwater.json) holds them. Runs in progress finish, and the director still runs your prompts.`,
27
- actions: [],
28
- });
29
- }
30
- }
31
- function pushMaintenanceAlert(out, snap) {
32
- // The maintenance allowance (plans/work-ratio.md, part 1b/4): by design and self-clearing —
33
- // the scheduler holds maintenance roles at the rolling 24 h allowance until merges free room,
34
- // then they run again on their own — so it informs (gray, no actions), the quiet alert's
35
- // tone, rather than amber like the failed/refused asks. An operator watching idle maintenance
36
- // roles gets the same explanation the hold's own row carries, team-wide.
37
- if (snap.maintenanceQuota.held.length > 0) {
38
- const mq = snap.maintenanceQuota;
39
- out.push({
40
- key: "maintenance",
41
- tone: "gray",
42
- title: "Maintenance is held at its rolling allowance",
43
- detail: `${mq.used} of ${mq.allowance} maintenance allowance used over the last 24 h; ${listRoleIds(mq.held)} ${agree(mq.held.length, "resumes", "resume")} as work merges.`,
44
- actions: [],
45
- });
46
- }
47
- }
48
- function pushProviderHoldAlert(out, snap, now) {
49
- // A standing provider failure hold (src/team/team-hold.ts `standingHolds`, published through
50
- // the orchestrator info file): a provider storm holds roles on that provider, so an operator
51
- // watching idle roles with no log open sees why nothing new starts there. Self-clearing by
52
- // design — the hold re-opens at its own deadline — so it informs (blue, no actions), the
53
- // quiet and maintenance alerts' tone, rather than asking. The snapshot carries the holds only
54
- // while a live orchestrator publishes one.
55
- if (snap.providerHolds && snap.providerHolds.length > 0) {
56
- const phrased = snap.providerHolds
57
- .map((h) => `${h.provider ?? "pi's default backend"} (${h.kind}) until it re-opens in ${humanUntil(h.until, now)}`)
58
- .join("; ");
59
- out.push({
60
- key: "provider-hold",
61
- tone: "blue",
62
- title: agree(snap.providerHolds.length, "Provider limit is holding new runs", "Provider limits are holding new runs"),
63
- detail: `${phrased}. Runs in progress finish; roles on a held provider start no new runs until it re-opens.`,
64
- actions: [],
65
- });
66
- }
67
- }
68
- function pushProviderCapsAlert(out, providerCaps) {
69
- // A `maxConcurrentPerProvider` map that cannot do its job — the always-on twin of the doctor's
70
- // `provider concurrency` row (doctor-contention-checks.ts checkProviderConcurrency): a cap key
71
- // no seam resolves to, a cap at or above maxConcurrent, or a keyless seam in a capped team.
72
- // The provider-hold and contention alerts cover a cap that is merely too small; this one
73
- // names the static misconfiguration itself, which the doctor row is the only other surface
74
- // that exposes. alert-feeds.ts's providerCapProblems feeds it the row's own warning strings,
75
- // so the alert and the row cannot drift on the trigger or the wording.
76
- if (providerCaps.length > 0) {
77
- out.push({
78
- key: "provider-caps",
79
- tone: "amber",
80
- title: "maxConcurrentPerProvider is misconfigured",
81
- detail: `${providerCaps.join("; ")} — fix the map in tumwater.json (each key must name a provider the team runs on, and each cap must sit below maxConcurrent); the provider-hold and contention alerts cover a cap that is merely too small.`,
82
- actions: [],
83
- });
84
- }
85
- }
86
- function pushPausedAlert(out, snap, now) {
87
- if (snap.paused) {
88
- const left = pauseCountdown(snap.pausedUntil, now);
89
- out.push({
90
- key: "paused",
91
- tone: "amber",
92
- // The operator's pause reason (`pause --reason <text>`) states verbatim, so an
93
- // operator returning to a paused team knows why without digging through history.
94
- title: left
95
- ? `The team is paused and resumes in ${left}${pauseReasonSuffix(snap.pauseReason)}`
96
- : `The team is paused${pauseReasonSuffix(snap.pauseReason)}`,
97
- detail: "Roles start no new runs. Runs in progress finish, and the director still runs your prompts.",
98
- actions: [{ label: "Resume now", act: "resume" }],
99
- });
100
- }
101
- }
102
- function pushContentionAlert(out, contention) {
103
- // Last day's capacity contention — the always-on twin of the doctor's `workspaces`,
104
- // `provider contention` and `check contention` rows (doctor-contention-checks.ts). The
105
- // instantaneous badges show only current waits, and the 24 h total is the tuning signal that
106
- // says a cap is too small; alert-feeds.ts's capacityContention feeds it from the same
107
- // `*_wait` events and ten-minute thresholds the doctor sums, so the alert and the doctor
108
- // cannot drift on the trigger or the knob-raising advice.
109
- if (contention.length > 0) {
110
- out.push({
111
- key: "contention",
112
- tone: "amber",
113
- title: plural(contention.length, "capacity bottleneck in the last day", "capacity bottlenecks in the last day"),
114
- detail: contention.map((c) => `${c.name}: ${squash(c.detail, 200)}`).join(" · "),
115
- actions: [],
116
- });
117
- }
118
- }
119
- function pushWorkspacePinsAlert(out, workspacePins) {
120
- // A stale workspace pin — the always-on twin of the doctor's `worktree pool` row's stuck-pin
121
- // clause (doctor-contention-checks.ts checkWorktreePool): a slot pinned past a day by a run
122
- // that never resumed silently shrinks the pool, and no other observer surface names it until
123
- // an operator runs `tumwater doctor`. alert-feeds.ts feeds it the same staleWorkspacePins
124
- // predicate the doctor's row uses, so the alert and the row cannot drift on what is stale;
125
- // the "Open <role>" buttons reach the role drawer like the other role alerts.
126
- if (workspacePins.length > 0) {
127
- out.push({
128
- key: "workspace-pins",
129
- tone: "amber",
130
- title: plural(workspacePins.length, "workspace pinned past a day", "workspaces pinned past a day"),
131
- detail: `${workspacePins
132
- .map(stalePinPhrase)
133
- .join("; ")} — a pinned workspace is held out of the pool; resume the role, or retire its leftovers and run \`tumwater doctor\``,
134
- actions: openRoleActions(workspacePins),
135
- });
136
- }
137
- }
138
- function pushInstancePoolAlert(out, instancePool) {
139
- // An enabled multi-instance role sharing a workspace pool smaller than the run cap — the
140
- // always-on twin of the doctor `parallel instances` row's pool-mismatch clause
141
- // (doctor-contention-checks.ts smallPoolProblems): instances then contend for the same
142
- // pooled checkouts, unseen until an operator runs `tumwater doctor`. alert-feeds.ts feeds
143
- // it the same smallPoolProblems selector the row uses, so the alert and the row cannot
144
- // drift on the trigger, the role list, or the wording.
145
- if (instancePool.length > 0) {
146
- out.push({
147
- key: "instance-pool",
148
- tone: "amber",
149
- title: "Parallel instances share a workspace pool smaller than the run cap",
150
- detail: `${instancePool.join("; ")} — runs wait for a pooled checkout; raise worktreeSlots to at least maxConcurrent, or lower maxConcurrent.`,
151
- actions: [],
152
- });
153
- }
154
- }
155
- function pushRegressedFixesAlert(out, regressedFixes) {
156
- // A Fixed record whose quoted test failed again in the trailing day — the always-on twin of
157
- // the doctor's `regressed fixes` row (doctor-backlog-bugs.ts checkRegressedFixes).
158
- // alert-feeds.ts's regressedFixesAlert feeds it the same candidate selection, failureMentions
159
- // match and verbatim sentence, so the alert and the doctor cannot drift on the trigger or the
160
- // wording; the Failures view shows the naming events.
161
- if (regressedFixes) {
162
- out.push({
163
- key: "regressed-fixes",
164
- tone: "amber",
165
- title: plural(regressedFixes.count, "Fixed test has regressed", "Fixed tests have regressed"),
166
- detail: regressedFixes.detail,
167
- actions: seeFailures(),
168
- });
169
- }
170
- }
1
+ /** The tail team alerts: a stopped tumwater. One of the per-domain builders split out of
2
+ * src/ui/team-alerts.ts; the shared types and wording helpers live in team-alert-shared.ts. */
3
+ import { plural } from "../text/phrases.js";
171
4
  function pushStoppedAlert(out, snap) {
172
5
  if (!snap.running) {
173
6
  out.push({
@@ -179,60 +12,31 @@ function pushStoppedAlert(out, snap) {
179
12
  });
180
13
  }
181
14
  }
182
- function pushImagesAlert(out, roles) {
183
- // Image input (plans/image-input.md "Surfaces", part 5/5): a role whose model cannot read
184
- // images. Blue when every text-only role has a reroute target (a run that reads an image
185
- // stops and reruns — nothing fails); amber when some text-only role has none (a blind read
186
- // fails and image submissions are refused). No alert
187
- // when every enabled role reads images (imageInput true or unknown — an unknown seam is not
188
- // counted as text-only, mirroring the submit gate and the doctor's `tier models` stance).
189
- const textOnly = roles.filter((l) => l.imageInput === false);
190
- const noTarget = textOnly.filter((l) => l.imageReroute == null || l.imageReroute === "");
191
- if (noTarget.length > 0) {
15
+ function pushOrphansAlert(out, orphans) {
16
+ // Orphaned worktree processes — the always-on twin of the doctor's `orphans` row
17
+ // (doctor-orphans.ts checkOrphans): a detected orphan holds no lock, writes no event and
18
+ // touches no state file, so nothing else on the always-on surfaces names it until an operator
19
+ // runs `tumwater doctor`, and it then burns CPU for days unnoticed. alert-feeds.ts's
20
+ // getOrphanAlert feeds it the doctor's own orphanFailDetail sentence and count, so the alert
21
+ // and the row cannot drift on the trigger, the cpu, or the remediation.
22
+ if (orphans) {
192
23
  out.push({
193
- key: "images",
24
+ key: "orphans",
194
25
  tone: "amber",
195
- title: `No configured model can read images for ${listRoles(noTarget)}`,
196
- detail: "A run that reads an image fails, and image attachments to these roles are refused. Declare image input on a vision model in pi's models.json, or point a tier at one.",
197
- actions: openRoleActions(noTarget),
198
- });
199
- }
200
- else if (textOnly.length > 0) {
201
- const targets = [...new Set(textOnly.map((l) => l.imageReroute ?? ""))].filter(Boolean);
202
- out.push({
203
- key: "images",
204
- tone: "blue",
205
- title: `${listRoles(textOnly)} ${agree(textOnly.length, "runs", "run")} on models that can't read images`,
206
- detail: `A run that reads an image stops and reruns on ${targets.join(", ")}.`,
26
+ title: `${plural(orphans.count, "orphaned process", "orphaned processes")} are burning CPU (PPID 1)`,
27
+ detail: orphans.detail,
207
28
  actions: [],
208
29
  });
209
30
  }
210
31
  }
211
- /** The quiet-hours, maintenance, provider-hold, pause, capacity-contention, provider-cap,
212
- * instance-pool, stale-workspace-pin, regressed-fix, stopped and image-input alerts, in
213
- * emission order: a run of quiet hours
214
- * holding the team, a
215
- * maintenance allowance held at its rolling cap, a standing provider failure hold, an operator
216
- * pause with its countdown, the last day's capacity contention, a stale workspace pin, a
217
- * fixed test that regressed, a
218
- * stopped tumwater, and roles that cannot or may not read images. Extracted from teamAlerts
32
+ /** The stopped-tumwater alert, in emission order: a stopped team. Extracted from teamAlerts
219
33
  * (part 5/5 of the teamAlerts split) so the shared module stays small; the input names exactly
220
- * the fields this run reads, including `now`, which the pause countdown and provider-hold
221
- * re-open time need. The caller pushes the returned alerts at the position this run used to
222
- * occupy, so the emitted key order is unchanged. */
34
+ * the fields this run reads. The caller pushes the returned alerts at the position this run
35
+ * used to occupy, so the emitted key order is unchanged. */
223
36
  export function buildTailAlerts(input) {
224
37
  const out = [];
225
- const { snap, now, contention, regressedFixes, roles, workspacePins, instancePool, providerCaps } = input;
226
- pushQuietAlert(out, snap);
227
- pushMaintenanceAlert(out, snap);
228
- pushProviderHoldAlert(out, snap, now);
229
- pushProviderCapsAlert(out, providerCaps);
230
- pushPausedAlert(out, snap, now);
231
- pushContentionAlert(out, contention);
232
- pushWorkspacePinsAlert(out, workspacePins);
233
- pushInstancePoolAlert(out, instancePool);
234
- pushRegressedFixesAlert(out, regressedFixes);
38
+ const { snap, orphans } = input;
39
+ pushOrphansAlert(out, orphans);
235
40
  pushStoppedAlert(out, snap);
236
- pushImagesAlert(out, roles);
237
41
  return out;
238
42
  }
@@ -15,22 +15,21 @@ import { buildStatusAlerts } from "./team-alert-status.js";
15
15
  import { buildBacklogAlerts } from "./team-alert-backlog.js";
16
16
  import { buildTailAlerts } from "./team-alert-tail.js";
17
17
  /** Everything about the team that needs the operator, most urgent first: a spent budget, a red
18
- * main, roles failing run after run, roles that look stuck, a stale running build, open
18
+ * main, roles failing run after run, roles that look stuck, disk and install drift, open
19
19
  * questions, refused entries, a plan stalled by a Needs
20
20
  * review note, the team
21
21
  * waiting on capacity slots (workspace, provider or
22
- * check), parallel instances sharing a workspace pool smaller than the run cap, a plan
22
+ * check), a plan
23
23
  * filed under the wrong backlog section, a duplicated
24
24
  * backlog section heading, finished history still under a live backlog file's `## Done` or
25
- * `## Fixed`, a Fixed record with a bad Validation gap trace, a pause, a
25
+ * `## Fixed`, a Fixed record with a bad Validation gap trace, and a
26
26
  * stopped team.
27
- * `now` is passed so a timed
28
- * pause's countdown agrees with the rest of the frame. */
29
- export function teamAlerts(snap, questions, roles, now, refused = [], planDependency = null, stalledEntries = [], strandedPlans = [], duplicatedHeadings = [], validationGaps = [], contention = [], regressedFixes = null, staleClaims = [], staleQuestions = [], fixClaims = [], workspacePins = [], liveFinished = [], instancePool = [], providerCaps = []) {
27
+ */
28
+ export function teamAlerts(snap, questions, roles, _now, refused = [], planDependency = null, stalledEntries = [], strandedPlans = [], duplicatedHeadings = [], validationGaps = [], staleQuestions = [], fixClaims = [], liveFinished = [], orphans = null) {
30
29
  const out = [];
31
- out.push(...buildStatusAlerts({ snap, roles, staleClaims }));
30
+ out.push(...buildStatusAlerts({ snap, roles }));
32
31
  out.push(...buildBacklogAlerts({ questions, staleQuestions, refused, stalledEntries, planDependency, strandedPlans, duplicatedHeadings, liveFinished, validationGaps, fixClaims }));
33
- out.push(...buildTailAlerts({ snap, now, contention, regressedFixes, roles, workspacePins, instancePool, providerCaps }));
32
+ out.push(...buildTailAlerts({ snap, orphans }));
34
33
  return out;
35
34
  }
36
35
  /** Does this alert ask something of the operator (red, amber, indigo), rather than inform? */
@@ -8,6 +8,31 @@ import { parsePiEventLine } from "../pi/pi-event-line.js";
8
8
  * renderer stamps its separator from). A segment with neither is skipped. */
9
9
  const MARKER_ONLY = new Set(["tumwater_run"]);
10
10
  const MESSAGE_ONLY = new Set(["message_end"]);
11
+ function segmentsInText(text) {
12
+ const segments = [];
13
+ for (const line of text.split("\n")) {
14
+ if (line === "")
15
+ continue;
16
+ if (line.includes('"tumwater_run"') && parsePiEventLine(line, MARKER_ONLY))
17
+ segments.push([line]);
18
+ else
19
+ segments.at(-1)?.push(line);
20
+ }
21
+ return segments;
22
+ }
23
+ function keepInWindow(segments, fromTs, toTs) {
24
+ const kept = [];
25
+ for (const seg of segments) {
26
+ const start = segmentStart(seg);
27
+ if (start !== null && start >= fromTs && start <= (toTs ?? Infinity))
28
+ kept.push(...seg);
29
+ }
30
+ return kept;
31
+ }
32
+ function applyTranscriptGrep(entries, grep) {
33
+ const keep = createTranscriptFilter(grep);
34
+ return entries.flatMap((e) => keep(e) ?? []);
35
+ }
11
36
  function segmentStart(lines) {
12
37
  const marker = parsePiEventLine(lines[0], MARKER_ONLY);
13
38
  if (typeof marker?.ts === "number")
@@ -35,24 +60,10 @@ export function readRunTranscript(file, fromTs, toTs, opts = {}) {
35
60
  catch {
36
61
  continue;
37
62
  }
38
- const segments = [];
39
- for (const line of text.split("\n")) {
40
- if (line === "")
41
- continue;
42
- if (line.includes('"tumwater_run"') && parsePiEventLine(line, MARKER_ONLY))
43
- segments.push([line]);
44
- else
45
- segments.at(-1)?.push(line);
46
- }
47
- for (const seg of segments) {
48
- const start = segmentStart(seg);
49
- if (start !== null && start >= fromTs && start <= (toTs ?? Infinity))
50
- kept.push(...seg);
51
- }
63
+ kept.push(...keepInWindow(segmentsInText(text), fromTs, toTs));
52
64
  }
53
65
  const entries = formatTranscript(kept, opts);
54
66
  if (opts.grep === undefined)
55
67
  return entries.flat();
56
- const keep = createTranscriptFilter(opts.grep);
57
- return entries.flatMap((e) => keep(e) ?? []);
68
+ return applyTranscriptGrep(entries, opts.grep);
58
69
  }
@@ -1,12 +1,14 @@
1
- import { questionAsker, tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE, } from "../../backlog/question-answer.js";
1
+ import { questionAsker, tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE, } from "../../backlog/backlog-question-answer.js";
2
2
  import { wakeQuestionAsker } from "../../operator/operator-intent.js";
3
+ import { savedPromptDraft } from "./tui-editor-session.js";
4
+ import { guardError } from "./tui-guard.js";
3
5
  /** The backlog pane's answer editor (Ctrl+A on a selected open question) on the shared prompt
4
6
  * line. It owns its target (`answerFor`), its saved-draft pair, its exit helper, and the
5
7
  * tryAnswerQuestion / wake arms that record the decision. Mutually exclusive with the other
6
8
  * editors via the session's held registry. */
7
9
  export function createAnswerEditor(session) {
8
10
  let answerFor = null;
9
- let restoreDraft = () => { };
11
+ const draft = savedPromptDraft(session);
10
12
  // Leave the backlog pane's answer editor (Esc, a recorded answer, or Ctrl+T): restore the
11
13
  // saved prompt text and reset the recall state, mirroring exitQueuedEdit.
12
14
  const exitAnswerMode = () => {
@@ -14,7 +16,7 @@ export function createAnswerEditor(session) {
14
16
  return;
15
17
  answerFor = null;
16
18
  session.held.answer = null;
17
- restoreDraft();
19
+ draft.restore();
18
20
  };
19
21
  // Ctrl+A opens the answer editor on the selected open question (mnemonic: **A**nswer);
20
22
  // Enter records the decision through tryAnswerQuestion, the core `tumwater questions
@@ -23,26 +25,26 @@ export function createAnswerEditor(session) {
23
25
  const answerOpenEdit = (ctx) => {
24
26
  if (!session.anyEditorHeld() &&
25
27
  ctx.view === ctx.roleIds.length + 1 && ctx.selectedEntry !== null) {
26
- try {
28
+ const selected = ctx.selectedEntry;
29
+ if (selected === null)
30
+ return false;
31
+ guardError(session, () => {
27
32
  const entries = session.flat();
28
- const sel = Math.min(ctx.selectedEntry, entries.length - 1);
33
+ const sel = Math.min(selected, entries.length - 1);
29
34
  const entry = entries[sel];
30
35
  if (entry?.label !== "question") {
31
36
  session.flashMessage("only an open question can be answered here");
32
37
  }
33
38
  else {
34
39
  const position = sel - entries.filter((e) => e.label === "plan" || e.label === "bug").length + 1;
35
- restoreDraft = session.takeDraft();
40
+ draft.save();
36
41
  answerFor = { position, title: entry.title };
37
42
  session.held.answer = answerFor;
38
43
  session.input = "";
39
44
  session.cursor = 0;
40
45
  session.flashMessage(`answering question #${position} — Enter to record, Esc to cancel`);
41
46
  }
42
- }
43
- catch (err) {
44
- session.flashError(err);
45
- }
47
+ });
46
48
  return true;
47
49
  }
48
50
  return false;
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { imagePromptGuidance } from "../../inbox/inbox-submit.js";
3
3
  import { promptImagesCountProblem, readPromptImageFiles } from "../../inbox/inbox-attachments.js";
4
+ import { savedPromptDraft } from "./tui-editor-session.js";
4
5
  /** The Activity view's Ctrl+V image-attach editor on the shared prompt line, the terminal
5
6
  * twin of `tumwater prompt --attach <path>` and the dashboard composer's drop/paste. It owns
6
7
  * its `attachMode` flag, its saved-draft pair, and the handleKey arms for its key (the toggle
@@ -12,7 +13,7 @@ import { promptImagesCountProblem, readPromptImageFiles } from "../../inbox/inbo
12
13
  * their views. */
13
14
  export function createAttachEditor(session) {
14
15
  let attachMode = false;
15
- let restoreDraft = () => { };
16
+ const draft = savedPromptDraft(session);
16
17
  // Leave attach mode (Esc, Ctrl+V again, or Ctrl+T): restore the saved prompt text. The
17
18
  // pending session.images are untouched — Esc cancels the path edit, not an attachment
18
19
  // already saved.
@@ -21,7 +22,7 @@ export function createAttachEditor(session) {
21
22
  return;
22
23
  attachMode = false;
23
24
  session.held.attach = false;
24
- restoreDraft();
25
+ draft.restore();
25
26
  };
26
27
  // Ctrl+V toggles image-attach mode on the Activity view's prompt line (mnemonic: attach
27
28
  // **v**ia the same key the reset-counters and move-to-top arms use on their own views):
@@ -44,7 +45,7 @@ export function createAttachEditor(session) {
44
45
  // Mid-recall, hand the arrows' saved draft back before the attach editor takes the
45
46
  // line — the same settle the role editors do — so leaving restores the
46
47
  // draft, not a history entry, with no stale recall state riding along.
47
- restoreDraft = session.takeDraft();
48
+ draft.save();
48
49
  attachMode = true;
49
50
  session.held.attach = true;
50
51
  session.input = "";
@@ -3,6 +3,7 @@ import { setConfigKey, unsetConfigKey } from "../../config/config-write.js";
3
3
  import { loadConfigSafe, parseJsonConfig } from "../../config/config.js";
4
4
  import { isJsonObject } from "../../files/json-object.js";
5
5
  import { CONFIG_BASENAME, configPath } from "../../paths.js";
6
+ import { savedPromptDraft } from "./tui-editor-session.js";
6
7
  /** The Ctrl+X agent-backends editor on the Activity view's shared prompt line — the terminal
7
8
  * twin of the dashboard Settings `Agent backends (JSON)` field and `tumwater config set
8
9
  * backends`, completing the run-concurrency knob set `tumwater doctor` and the dashboard
@@ -19,14 +20,14 @@ import { CONFIG_BASENAME, configPath } from "../../paths.js";
19
20
  * arm. */
20
21
  export function createBackendsEditor(session) {
21
22
  let backendsMode = false;
22
- let restoreDraft = () => { };
23
+ const draft = savedPromptDraft(session);
23
24
  // Leave backends mode (Esc, Ctrl+X again, or Ctrl+T): restore the saved prompt text.
24
25
  const exitBackendsMode = () => {
25
26
  if (!backendsMode)
26
27
  return;
27
28
  backendsMode = false;
28
29
  session.held.backends = false;
29
- restoreDraft();
30
+ draft.restore();
30
31
  };
31
32
  // Ctrl+X edits the team's agent backends on the Activity view (view 0) — the terminal
32
33
  // twin of the dashboard Settings `Agent backends (JSON)` field and `tumwater config set
@@ -48,7 +49,7 @@ export function createBackendsEditor(session) {
48
49
  if (session.editorEntryDenied("backends"))
49
50
  return true;
50
51
  const existing = rawBackends(session.root);
51
- restoreDraft = session.takeDraft();
52
+ draft.save();
52
53
  backendsMode = true;
53
54
  session.held.backends = true;
54
55
  session.input = existing !== undefined ? JSON.stringify(existing) : "";
@@ -1,5 +1,6 @@
1
1
  import { editBriefText } from "../../brief/brief-edit.js";
2
- import { readEditorPath } from "./tui-editor-session.js";
2
+ import { readEditorPath, savedPromptDraft } from "./tui-editor-session.js";
3
+ import { flashReject } from "./tui-guard.js";
3
4
  /** The Brief view's Ctrl+E replace-the-brief-from-a-file editor on the shared prompt line, the
4
5
  * terminal twin of `tumwater brief --file <path>` and the dashboard's Brief card. It owns its
5
6
  * `briefEdit` flag, its saved-draft pair, and the handleKey arms for its key (the toggle with
@@ -14,14 +15,14 @@ import { readEditorPath } from "./tui-editor-session.js";
14
15
  * re-entering. */
15
16
  export function createBriefEditor(session) {
16
17
  let briefEdit = false;
17
- let restoreDraft = () => { };
18
+ const draft = savedPromptDraft(session);
18
19
  // Leave the brief editor (Esc or a successful Enter): restore the saved prompt text.
19
20
  const exitBriefMode = () => {
20
21
  if (!briefEdit)
21
22
  return;
22
23
  briefEdit = false;
23
24
  session.held.brief = false;
24
- restoreDraft();
25
+ draft.restore();
25
26
  };
26
27
  // Ctrl+E opens the replace-the-brief editor on the Brief view's prompt line (mnemonic:
27
28
  // **e**dit brief — the same key the Health reclaim, Report window-cycle, Queued row-edit
@@ -39,7 +40,7 @@ export function createBriefEditor(session) {
39
40
  // Mid-recall, hand the arrows' saved draft back before the brief editor takes the line —
40
41
  // the same settle the other editors do — so leaving restores the draft, not a history
41
42
  // entry, with no stale recall state riding along.
42
- restoreDraft = session.takeDraft();
43
+ draft.save();
43
44
  briefEdit = true;
44
45
  session.held.brief = true;
45
46
  session.input = "";
@@ -75,11 +76,7 @@ export function createBriefEditor(session) {
75
76
  session.flashMessage(outcome.changed
76
77
  ? `brief updated in ${outcome.file}${outcome.committed ? " (committed to main)" : ""}`
77
78
  : "brief unchanged");
78
- }, (err) => {
79
- // The read/replace/commit threw: flash the reason and stay in the editor so the
80
- // operator can fix things, as the other editor Enter arms do.
81
- session.flashError(err);
82
- });
79
+ }, flashReject(session));
83
80
  return true;
84
81
  };
85
82
  const handleKey = (_str, key, ctx) => {
@@ -5,6 +5,7 @@ import { DIRECTOR_ROLE } from "../../roles/roles.js";
5
5
  import { baseRoleOf } from "../../roles/runner-ids.js";
6
6
  import { parseBudgetInput } from "./tui-input.js";
7
7
  import { viewedRoleOrNull } from "./tui-editor-ctx.js";
8
+ import { savedPromptDraft } from "./tui-editor-session.js";
8
9
  /** The Ctrl+B budget editor on the shared prompt line. It owns its mode flag, its saved-draft
9
10
  * pair, the snapshot-fed cap/free flags `sync` drives, and the handleKey arms for its key: the
10
11
  * toggle (with the `heldEditorFlash(entering)` guard clause, which keeps the shared guard
@@ -12,7 +13,7 @@ import { viewedRoleOrNull } from "./tui-editor-ctx.js";
12
13
  * the one prompt line via the session's held registry. */
13
14
  export function createBudgetEditor(session) {
14
15
  let budgetMode = false;
15
- let restoreDraft = () => { };
16
+ const draft = savedPromptDraft(session);
16
17
  // The last snapshot's cap, synced by render so the Ctrl+B handler can pre-fill without
17
18
  // re-reading config itself (render already polls snapshot every second).
18
19
  let currentCapUsd = 0;
@@ -29,7 +30,7 @@ export function createBudgetEditor(session) {
29
30
  return;
30
31
  budgetMode = false;
31
32
  session.held.budget = false;
32
- restoreDraft();
33
+ draft.restore();
33
34
  lockedRole = null;
34
35
  };
35
36
  // Enter budget-edit mode (the shared entry, whether the team cap or a role's own cap is
@@ -37,7 +38,7 @@ export function createBudgetEditor(session) {
37
38
  // pre-fill the line from the cap (empty string when disabled — empty means "no cap"), and
38
39
  // announce the mode. Only the cap source and the announcement differ between the two scopes.
39
40
  const enterBudgetMode = (cap, message) => {
40
- restoreDraft = session.takeDraft();
41
+ draft.save();
41
42
  budgetMode = true;
42
43
  session.held.budget = true;
43
44
  session.input = cap > 0 ? String(cap) : "";