tumwater 0.4.1 → 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 (186) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/backend-traits.js +14 -0
  4. package/dist/src/agent/claude/claude-stream.js +27 -31
  5. package/dist/src/agent/codex/codex-rollout.js +38 -24
  6. package/dist/src/agent/codex/codex-stream.js +71 -38
  7. package/dist/src/agent/image-input.js +18 -87
  8. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  9. package/dist/src/agent/opencode/opencode-stream.js +4 -4
  10. package/dist/src/agent/run-agent.js +11 -2
  11. package/dist/src/agent/run-settle.js +22 -13
  12. package/dist/src/agent/run-spawn.js +3 -12
  13. package/dist/src/agent/run-stream.js +2 -10
  14. package/dist/src/agent/session-ledger.js +16 -0
  15. package/dist/src/backlog/backlog-archive.js +64 -165
  16. package/dist/src/backlog/backlog-eligibility.js +50 -58
  17. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  18. package/dist/src/backlog/backlog-index-data.js +2 -2
  19. package/dist/src/backlog/backlog-md.js +38 -27
  20. package/dist/src/backlog/backlog-release.js +11 -19
  21. package/dist/src/backlog/backlog-structure.js +14 -1
  22. package/dist/src/backlog/backlog-write.js +1 -1
  23. package/dist/src/backlog/backlog.js +5 -4
  24. package/dist/src/backlog/question-answer.js +30 -14
  25. package/dist/src/baseline/main-baseline.js +30 -20
  26. package/dist/src/baseline/main-guardrails.js +54 -17
  27. package/dist/src/baseline/main-watch.js +1 -1
  28. package/dist/src/budget/budget-wiring.js +0 -5
  29. package/dist/src/budget/budget.js +2 -1
  30. package/dist/src/build/build-check-scoped.js +70 -41
  31. package/dist/src/change/change-data.js +4 -2
  32. package/dist/src/cli/cli-command-args.js +199 -98
  33. package/dist/src/cli/cli-flag-specs.js +1 -4
  34. package/dist/src/cli/cli-review-commands.js +2 -1
  35. package/dist/src/cli/cli-role-command.js +27 -50
  36. package/dist/src/cli/cli-start.js +62 -12
  37. package/dist/src/cli/follow-view.js +3 -1
  38. package/dist/src/cli/help.js +6 -7
  39. package/dist/src/cli/log-commands.js +3 -1
  40. package/dist/src/cli.js +3 -7
  41. package/dist/src/collections.js +21 -7
  42. package/dist/src/config/config-fallbacks.js +0 -4
  43. package/dist/src/config/config-live.js +179 -147
  44. package/dist/src/config/config-views.js +2 -7
  45. package/dist/src/config/config-write.js +19 -26
  46. package/dist/src/doctor/doctor-backlog-bugs.js +17 -115
  47. package/dist/src/doctor/doctor-backlog-plans.js +8 -8
  48. package/dist/src/doctor/doctor-backlog.js +22 -36
  49. package/dist/src/doctor/doctor-check-names.js +2 -2
  50. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  51. package/dist/src/doctor/doctor-contention-checks.js +1 -1
  52. package/dist/src/doctor/doctor-holds.js +13 -11
  53. package/dist/src/doctor/doctor-model-checks.js +8 -184
  54. package/dist/src/doctor/doctor-notify.js +2 -5
  55. package/dist/src/doctor/doctor-opencode-checks.js +85 -48
  56. package/dist/src/doctor/doctor-project-checks.js +66 -17
  57. package/dist/src/doctor/doctor-role-notes.js +6 -24
  58. package/dist/src/doctor/doctor.js +6 -6
  59. package/dist/src/doctor/probe.js +1 -1
  60. package/dist/src/events/event-format.js +5 -15
  61. package/dist/src/events/event-labels.js +0 -1
  62. package/dist/src/events/event-legacy.js +1 -1
  63. package/dist/src/events/event-window.js +1 -1
  64. package/dist/src/events/events.js +15 -2
  65. package/dist/src/failure/failure-state-change.js +183 -68
  66. package/dist/src/failure/time-spend.js +1 -1
  67. package/dist/src/files/json-object.js +15 -2
  68. package/dist/src/gates/startup-gate.js +28 -28
  69. package/dist/src/git/slot-salvage.js +5 -4
  70. package/dist/src/gui/gui-admin-endpoints.js +2 -3
  71. package/dist/src/gui/gui-backlog-commands.js +1 -1
  72. package/dist/src/gui/gui-backlog-endpoints.js +1 -7
  73. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  74. package/dist/src/gui/gui-operator-commands.js +0 -12
  75. package/dist/src/gui/gui-prompt-commands.js +26 -20
  76. package/dist/src/gui/gui-report-endpoints.js +2 -1
  77. package/dist/src/gui/gui-server.js +1 -2
  78. package/dist/src/history/history.js +102 -67
  79. package/dist/src/inbox/inbox-submit.js +1 -5
  80. package/dist/src/inbox/prompt-commands.js +24 -19
  81. package/dist/src/init/init.js +68 -15
  82. package/dist/src/merge/merge-core.js +1 -1
  83. package/dist/src/merge/merge-post-review-drain.js +1 -1
  84. package/dist/src/operator/operator-commands.js +68 -47
  85. package/dist/src/operator/retire.js +2 -1
  86. package/dist/src/orchestrator/retention.js +6 -5
  87. package/dist/src/paths.js +0 -6
  88. package/dist/src/pi/pi-watchdogs.js +20 -1
  89. package/dist/src/pi-extension/context-shake.js +55 -31
  90. package/dist/src/process/process.js +23 -0
  91. package/dist/src/process/supervisor.js +17 -1
  92. package/dist/src/prompt/gate-notes.js +9 -9
  93. package/dist/src/prompt/notes-backlog.js +11 -24
  94. package/dist/src/prompt/prompt-followup.js +4 -12
  95. package/dist/src/prompt/prompt.js +4 -5
  96. package/dist/src/report/report-data.js +10 -225
  97. package/dist/src/report/report-fold.js +20 -295
  98. package/dist/src/report/report-main-red.js +45 -42
  99. package/dist/src/report/report-render.js +66 -39
  100. package/dist/src/report/report-since-data.js +218 -0
  101. package/dist/src/report/report-usage-fold.js +288 -0
  102. package/dist/src/report/report.js +2 -1
  103. package/dist/src/review/known-flakes.js +3 -3
  104. package/dist/src/review/post-merge-review-accept.js +3 -2
  105. package/dist/src/review/post-merge-review.js +9 -9
  106. package/dist/src/review/quarantine-render.js +2 -1
  107. package/dist/src/roles/role-catalog.js +18 -30
  108. package/dist/src/roles/role-guidance.js +18 -43
  109. package/dist/src/roles/role-view.js +1 -2
  110. package/dist/src/run/ci-parity.js +5 -6
  111. package/dist/src/run/qa-coverage.js +4 -4
  112. package/dist/src/run/run-detail-data.js +23 -7
  113. package/dist/src/run/run-detail-render.js +6 -0
  114. package/dist/src/run/run-detail.js +44 -22
  115. package/dist/src/run/run-dismiss.js +43 -0
  116. package/dist/src/run/run-prompt.js +100 -56
  117. package/dist/src/run/run-resume.js +32 -31
  118. package/dist/src/run/run-stage.js +4 -21
  119. package/dist/src/run/run-timing.js +18 -4
  120. package/dist/src/run/run-verdict.js +8 -73
  121. package/dist/src/run/stage-backlog-checks.js +2 -29
  122. package/dist/src/run/stage-check.js +4 -12
  123. package/dist/src/runner/runner-outcome.js +0 -5
  124. package/dist/src/runner/runner-run.js +2 -79
  125. package/dist/src/runner/runner-state-load.js +0 -10
  126. package/dist/src/runner/runner.js +1 -32
  127. package/dist/src/scheduling/claims.js +3 -10
  128. package/dist/src/status/status-data.js +5 -9
  129. package/dist/src/status/status-role-notes.js +18 -14
  130. package/dist/src/status/status-roles.js +2 -35
  131. package/dist/src/text/datetime.js +8 -1
  132. package/dist/src/text/text.js +12 -0
  133. package/dist/src/ui/alert-feeds.js +7 -212
  134. package/dist/src/ui/badges.js +2 -3
  135. package/dist/src/ui/gui/gui-client-boot.js +19 -4
  136. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  137. package/dist/src/ui/gui/gui-client-report-chart.js +10 -8
  138. package/dist/src/ui/gui/gui-client-report.js +1 -2
  139. package/dist/src/ui/gui/gui-client-settings.js +0 -23
  140. package/dist/src/ui/gui/gui-client-team.js +26 -0
  141. package/dist/src/ui/gui/gui-icons.js +7 -1
  142. package/dist/src/ui/status-model.js +2 -2
  143. package/dist/src/ui/status-payload.js +3 -3
  144. package/dist/src/ui/status-render.js +3 -3
  145. package/dist/src/ui/team-alert-backlog.js +23 -54
  146. package/dist/src/ui/team-alert-shared.js +23 -10
  147. package/dist/src/ui/team-alert-status.js +8 -3
  148. package/dist/src/ui/team-alert-tail.js +1 -14
  149. package/dist/src/ui/team-alerts.js +6 -17
  150. package/dist/src/ui/transcript-tail.js +137 -94
  151. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  152. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  153. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  154. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  155. package/dist/src/ui/tui/tui-editor-budget.js +105 -93
  156. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  157. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  158. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  159. package/dist/src/ui/tui/tui-editor-pause.js +209 -183
  160. package/dist/src/ui/tui/tui-editor-prompt.js +44 -34
  161. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  162. package/dist/src/ui/tui/tui-editor-quiet.js +121 -108
  163. package/dist/src/ui/tui/tui-editor-release.js +50 -42
  164. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  165. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  166. package/dist/src/ui/tui/tui-editor-role-file.js +58 -46
  167. package/dist/src/ui/tui/tui-editor-role-key.js +49 -42
  168. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  169. package/dist/src/ui/tui/tui-frame.js +1 -1
  170. package/dist/src/ui/tui/tui-keys.js +1 -1
  171. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  172. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  173. package/dist/src/ui/tui/tui-pane-health.js +3 -24
  174. package/dist/src/ui/tui/tui-pane-history.js +20 -24
  175. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  176. package/dist/src/ui/tui/tui-pane-session.js +3 -2
  177. package/dist/src/ui/tui/tui-pane-transcript.js +151 -128
  178. package/dist/src/ui/tui/tui-pane.js +201 -174
  179. package/dist/src/ui/tui/tui-panes.js +2 -3
  180. package/dist/src/ui/tui/tui.js +3 -3
  181. package/dist/src/verdict/fix-claim.js +18 -6
  182. package/dist/src/verdict/refusal.js +30 -6
  183. package/package.json +1 -1
  184. package/dist/src/doctor/image-probe.js +0 -257
  185. package/dist/src/ui/team-alert-backlog-feed.js +0 -181
  186. package/dist/src/ui/team-alert-merge.js +0 -183
@@ -31,174 +31,192 @@ export function createEntryEditor(session) {
31
31
  session.held.entry = null;
32
32
  restoreDraft();
33
33
  };
34
- const handleKey = (_str, key, ctx) => {
35
- if (key.ctrl && key.name === "n") {
36
- // On the Backlog view Ctrl+N files a new entry: it opens the title editor with a null
37
- // position (new plan), and pressed again inside that editor flips plan <-> bug.
38
- if (entryEditFor !== null && entryEditFor.position === null) {
39
- const other = entryEditFor.kind === "plan" ? "bug" : "plan";
40
- entryEditFor = { ...entryEditFor, kind: other };
41
- session.held.entry = entryEditFor;
42
- session.flashMessage(`new ${other} — Enter to file, Ctrl+N switches to ${other === "plan" ? "bug" : "plan"}, Esc cancels`);
43
- return true;
44
- }
45
- // A Ctrl+N on any other state (not the Backlog view's new-entry slot, or an editor
46
- // already holding the line) is the notes/change/trail cycle, a pane arm the outer
47
- // dispatch handles — this factory returns false so it runs.
48
- if (ctx.view === ctx.roleIds.length + 1 &&
49
- !session.anyEditorHeld()) {
34
+ // On the Backlog view Ctrl+N files a new entry: it opens the title editor with a null
35
+ // position (new plan), and pressed again inside that editor flips plan <-> bug.
36
+ const flipNewEntryKind = () => {
37
+ const other = entryEditFor.kind === "plan" ? "bug" : "plan";
38
+ entryEditFor = { ...entryEditFor, kind: other };
39
+ session.held.entry = entryEditFor;
40
+ session.flashMessage(`new ${other} — Enter to file, Ctrl+N switches to ${other === "plan" ? "bug" : "plan"}, Esc cancels`);
41
+ return true;
42
+ };
43
+ // A Ctrl+N on any other state (not the Backlog view's new-entry slot, or an editor
44
+ // already holding the line) is the notes/change/trail cycle, a pane arm the outer
45
+ // dispatch handles — this factory returns false so it runs.
46
+ const openNewEntry = () => {
47
+ restoreDraft = session.takeDraft();
48
+ entryEditFor = { kind: "plan", position: null, title: "" };
49
+ session.held.entry = entryEditFor;
50
+ session.input = "";
51
+ session.cursor = 0;
52
+ session.flashMessage("new plan — Enter to file, Ctrl+N switches to bug, Esc cancels");
53
+ return true;
54
+ };
55
+ // A second Ctrl+E inside the entry editor toggles the same prompt line into the file
56
+ // field, whose path's contents replace the whole entry (splitEntryFile's rule) through
57
+ // tryEditEntry — the terminal twin of `tumwater plan|bug --edit <n> --file <path>`. A
58
+ // third Ctrl+E returns to the title draft, parked in the saved pair; Esc cancels all.
59
+ const toggleEntryFileField = () => {
60
+ entryBodyFile = !entryBodyFile;
61
+ if (entryBodyFile) {
62
+ entryBodySavedInput = session.input;
63
+ entryBodySavedCursor = session.cursor;
64
+ session.input = "";
65
+ session.cursor = 0;
66
+ session.flashMessage(`replace ${entryEditFor.kind} #${entryEditFor.position} from a file — Enter to save, Ctrl+E back to the title, Esc cancels`);
67
+ }
68
+ else {
69
+ session.input = entryBodySavedInput;
70
+ session.cursor = entryBodySavedCursor;
71
+ session.flashMessage(`editing ${entryEditFor.kind} #${entryEditFor.position} title — Enter to save, Ctrl+E to replace from a file, Esc cancels`);
72
+ }
73
+ return true;
74
+ };
75
+ // Ctrl+E opens the title editor on the selected plan or bug (mnemonic: **E**dit); Enter
76
+ // saves through tryEditEntry, the non-exiting core the dashboard drawer shares, keeping
77
+ // the body and stamp. The prompt line is single-line, so only the title is editable here.
78
+ const openSelectedEntry = (ctx) => {
79
+ try {
80
+ const selected = selectedEditableEntry(session, ctx.selectedEntry, "edited");
81
+ if (selected !== null) {
82
+ const kind = selected.kind;
83
+ const position = selected.position;
50
84
  restoreDraft = session.takeDraft();
51
- entryEditFor = { kind: "plan", position: null, title: "" };
85
+ entryEditFor = { kind, position, title: selected.picked.title };
52
86
  session.held.entry = entryEditFor;
53
- session.input = "";
54
- session.cursor = 0;
55
- session.flashMessage("new plan — Enter to file, Ctrl+N switches to bug, Esc cancels");
56
- return true;
87
+ session.input = stripEntryStamp(selected.picked.title);
88
+ session.cursor = session.input.length;
89
+ session.flashMessage(`editing ${kind} #${position} — Enter to save, Esc to cancel`);
57
90
  }
58
- return false;
59
91
  }
60
- if (key.ctrl && key.name === "e" && entryEditFor !== null && entryEditFor.position !== null) {
61
- // A second Ctrl+E inside the entry editor toggles the same prompt line into the file
62
- // field, whose path's contents replace the whole entry (splitEntryFile's rule) through
63
- // tryEditEntry — the terminal twin of `tumwater plan|bug --edit <n> --file <path>`. A
64
- // third Ctrl+E returns to the title draft, parked in the saved pair; Esc cancels all.
65
- entryBodyFile = !entryBodyFile;
66
- if (entryBodyFile) {
67
- entryBodySavedInput = session.input;
68
- entryBodySavedCursor = session.cursor;
69
- session.input = "";
70
- session.cursor = 0;
71
- session.flashMessage(`replace ${entryEditFor.kind} #${entryEditFor.position} from a file — Enter to save, Ctrl+E back to the title, Esc cancels`);
92
+ catch (err) {
93
+ session.flashError(err);
94
+ }
95
+ return true;
96
+ };
97
+ // Esc cancels the title editor, restoring the previous prompt text; nothing is written.
98
+ const cancelEntryEdit = () => {
99
+ const filing = entryEditFor.position === null;
100
+ exitEntryEdit();
101
+ session.flashMessage(filing ? "new entry cancelled" : "edit cancelled");
102
+ return true;
103
+ };
104
+ // Shared tail of the title and file edit arms: re-read the entry by flat index and
105
+ // title, guard against a concurrent move (so a mismatch is treated as moved, not
106
+ // written), then settle the editor and commit through tryEditEntry. The only thing
107
+ // the two arms pass differently is the (title, body) pair taking the entry's place;
108
+ // an omitted body preserves the matched entry's own body (the title edit arm).
109
+ const applyEntryEdit = (target, newTitle, newBody) => {
110
+ try {
111
+ // Both call sites (the file arm's and the title arm's Entry-save paths) run only
112
+ // when `target.position` is non-null, so the position math never sees a null.
113
+ const position = target.position;
114
+ const entries = session.flat();
115
+ const plans = entries.filter((e) => e.label === "plan").length;
116
+ const entry = entries[target.kind === "plan" ? position - 1 : plans + position - 1];
117
+ const outcome = entry?.label === target.kind && entry.title === target.title
118
+ ? tryEditEntry(session.root, target.kind, position, target.title, newTitle, newBody ?? entry.body)
119
+ : { status: "moved", title: "" };
120
+ exitEntryEdit();
121
+ if (outcome.status === "edited") {
122
+ session.commitBacklogEdit(outcome.file, `Edit ${target.kind}: ${outcome.title}`);
123
+ const holds = backlogHolds(session.root);
124
+ const hold = (target.kind === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
125
+ session.flashMessage(`edited ${target.kind} #${target.position}: ${outcome.title}${hold === null ? "" : ` — ${stillHeldNote(hold)}`}`);
126
+ }
127
+ else if (outcome.status === "unchanged") {
128
+ session.flashMessage(`unchanged ${target.kind} #${target.position}: ${outcome.title}`);
72
129
  }
73
130
  else {
74
- session.input = entryBodySavedInput;
75
- session.cursor = entryBodySavedCursor;
76
- session.flashMessage(`editing ${entryEditFor.kind} #${entryEditFor.position} title — Enter to save, Ctrl+E to replace from a file, Esc cancels`);
131
+ session.clearBacklogSelection();
132
+ session.flashMessage(`that ${target.kind} was already removed or moved — nothing edited`);
77
133
  }
78
- return true;
79
134
  }
80
- if (key.ctrl && key.name === "e" &&
81
- !session.anyEditorHeld() &&
82
- ctx.view === ctx.roleIds.length + 1 && ctx.selectedEntry !== null) {
83
- // Ctrl+E opens the title editor on the selected plan or bug (mnemonic: **E**dit); Enter
84
- // saves through tryEditEntry, the non-exiting core the dashboard drawer shares, keeping
85
- // the body and stamp. The prompt line is single-line, so only the title is editable here.
86
- try {
87
- const selected = selectedEditableEntry(session, ctx.selectedEntry, "edited");
88
- if (selected !== null) {
89
- const kind = selected.kind;
90
- const position = selected.position;
91
- restoreDraft = session.takeDraft();
92
- entryEditFor = { kind, position, title: selected.picked.title };
93
- session.held.entry = entryEditFor;
94
- session.input = stripEntryStamp(selected.picked.title);
95
- session.cursor = session.input.length;
96
- session.flashMessage(`editing ${kind} #${position} — Enter to save, Esc to cancel`);
97
- }
98
- }
99
- catch (err) {
100
- session.flashError(err);
135
+ catch (err) {
136
+ session.flashError(err);
137
+ }
138
+ };
139
+ // File mode: the line holds a path whose contents replace title and body through the
140
+ // same title-checked tryEditEntry core the title edit uses, keeping the entry's
141
+ // position and its (planned …)/(found by …) stamp. A missing file, an empty path, a
142
+ // file with no title line, and a structurally unsound body all flash and stay here.
143
+ const commitEntryFromFile = (target) => {
144
+ const text = readEditorPath(session);
145
+ if (text === null)
146
+ return;
147
+ const { title: rawTitle, body } = splitEntryFile(text);
148
+ const newTitle = stripEntryStamp(rawTitle);
149
+ if (!newTitle) {
150
+ session.flashMessage("the file needs a title line");
151
+ return;
152
+ }
153
+ const problem = bodyStructureProblem(body);
154
+ if (problem !== null) {
155
+ session.flashMessage(problem);
156
+ return;
157
+ }
158
+ applyEntryEdit(target, newTitle, body);
159
+ };
160
+ // New entry (Ctrl+N): file by title with an empty body and wake its role, the same
161
+ // steps handleBacklogFile takes; a length problem or a throw stays in the editor.
162
+ const fileNewEntry = (target) => {
163
+ try {
164
+ const { role, wakeText } = OPERATOR_FILE_KINDS[target.kind];
165
+ const problem = promptLengthProblem(wakeText(collapseWhitespace(session.input)), role);
166
+ if (problem) {
167
+ session.flashMessage(problem);
168
+ return;
101
169
  }
170
+ const filed = fileEntryWithBody(session.root, target.kind, session.input, "");
171
+ session.commitBacklogEdit(filed.file, `File ${target.kind}: ${filed.title}`);
172
+ submitRolePromptAndWake(session.root, role, wakeText(filed.title));
173
+ exitEntryEdit();
174
+ session.flashMessage(`filed ${target.kind}: ${filed.title} — woke ${role}`);
175
+ }
176
+ catch (err) {
177
+ session.flashError(err);
178
+ }
179
+ };
180
+ const commitEntryTitle = (target) => {
181
+ applyEntryEdit(target, session.input);
182
+ };
183
+ // Enter saves the new title through tryEditEntry with the body re-read now, so a
184
+ // concurrent body edit is not clobbered by a stale copy. The entry is matched by flat
185
+ // index and title; a mismatch is treated as moved without writing. A blank title and a
186
+ // throw flash and STAY in the editor so the text survives.
187
+ const commitEntryEdit = () => {
188
+ const target = entryEditFor;
189
+ if (entryBodyFile && target.position !== null) {
190
+ commitEntryFromFile(target);
102
191
  return true;
103
192
  }
104
- if (key.name === "escape" && entryEditFor) {
105
- // Esc cancels the title editor, restoring the previous prompt text; nothing is written.
106
- const filing = entryEditFor.position === null;
107
- exitEntryEdit();
108
- session.flashMessage(filing ? "new entry cancelled" : "edit cancelled");
193
+ if (!session.input.trim()) {
194
+ session.flashMessage("an entry needs a title");
109
195
  return true;
110
196
  }
111
- if (key.name === "return" && entryEditFor) {
112
- // Enter saves the new title through tryEditEntry with the body re-read now, so a
113
- // concurrent body edit is not clobbered by a stale copy. The entry is matched by flat
114
- // index and title; a mismatch is treated as moved without writing. A blank title and a
115
- // throw flash and STAY in the editor so the text survives.
116
- const target = entryEditFor;
117
- // Shared tail of the title and file edit arms: re-read the entry by flat index and
118
- // title, guard against a concurrent move (so a mismatch is treated as moved, not
119
- // written), then settle the editor and commit through tryEditEntry. The only thing
120
- // the two arms pass differently is the (title, body) pair taking the entry's place;
121
- // an omitted body preserves the matched entry's own body (the title edit arm).
122
- const applyEntryEdit = (newTitle, newBody) => {
123
- try {
124
- // Both call sites (the file arm's and the title arm's Entry-save paths) run only
125
- // when `target.position` is non-null, so the position math never sees a null.
126
- const position = target.position;
127
- const entries = session.flat();
128
- const plans = entries.filter((e) => e.label === "plan").length;
129
- const entry = entries[target.kind === "plan" ? position - 1 : plans + position - 1];
130
- const outcome = entry?.label === target.kind && entry.title === target.title
131
- ? tryEditEntry(session.root, target.kind, position, target.title, newTitle, newBody ?? entry.body)
132
- : { status: "moved", title: "" };
133
- exitEntryEdit();
134
- if (outcome.status === "edited") {
135
- session.commitBacklogEdit(outcome.file, `Edit ${target.kind}: ${outcome.title}`);
136
- const holds = backlogHolds(session.root);
137
- const hold = (target.kind === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
138
- session.flashMessage(`edited ${target.kind} #${target.position}: ${outcome.title}${hold === null ? "" : ` — ${stillHeldNote(hold)}`}`);
139
- }
140
- else if (outcome.status === "unchanged") {
141
- session.flashMessage(`unchanged ${target.kind} #${target.position}: ${outcome.title}`);
142
- }
143
- else {
144
- session.clearBacklogSelection();
145
- session.flashMessage(`that ${target.kind} was already removed or moved — nothing edited`);
146
- }
147
- }
148
- catch (err) {
149
- session.flashError(err);
150
- }
151
- };
152
- // File mode: the line holds a path whose contents replace title and body through the
153
- // same title-checked tryEditEntry core the title edit uses, keeping the entry's
154
- // position and its (planned …)/(found by …) stamp. A missing file, an empty path, a
155
- // file with no title line, and a structurally unsound body all flash and stay here.
156
- if (entryBodyFile && target.position !== null) {
157
- const text = readEditorPath(session);
158
- if (text === null)
159
- return true;
160
- const { title: rawTitle, body } = splitEntryFile(text);
161
- const newTitle = stripEntryStamp(rawTitle);
162
- if (!newTitle) {
163
- session.flashMessage("the file needs a title line");
164
- return true;
165
- }
166
- const problem = bodyStructureProblem(body);
167
- if (problem !== null) {
168
- session.flashMessage(problem);
169
- return true;
170
- }
171
- applyEntryEdit(newTitle, body);
172
- return true;
173
- }
174
- if (!session.input.trim()) {
175
- session.flashMessage("an entry needs a title");
176
- return true;
177
- }
178
- if (target.position === null) {
179
- // New entry (Ctrl+N): file by title with an empty body and wake its role, the same
180
- // steps handleBacklogFile takes; a length problem or a throw stays in the editor.
181
- try {
182
- const { role, wakeText } = OPERATOR_FILE_KINDS[target.kind];
183
- const problem = promptLengthProblem(wakeText(collapseWhitespace(session.input)), role);
184
- if (problem) {
185
- session.flashMessage(problem);
186
- return true;
187
- }
188
- const filed = fileEntryWithBody(session.root, target.kind, session.input, "");
189
- session.commitBacklogEdit(filed.file, `File ${target.kind}: ${filed.title}`);
190
- submitRolePromptAndWake(session.root, role, wakeText(filed.title));
191
- exitEntryEdit();
192
- session.flashMessage(`filed ${target.kind}: ${filed.title} — woke ${role}`);
193
- }
194
- catch (err) {
195
- session.flashError(err);
196
- }
197
- return true;
198
- }
199
- applyEntryEdit(session.input);
197
+ if (target.position === null) {
198
+ fileNewEntry(target);
200
199
  return true;
201
200
  }
201
+ commitEntryTitle(target);
202
+ return true;
203
+ };
204
+ const handleKey = (_str, key, ctx) => {
205
+ if (key.ctrl && key.name === "n") {
206
+ if (entryEditFor !== null && entryEditFor.position === null)
207
+ return flipNewEntryKind();
208
+ if (ctx.view === ctx.roleIds.length + 1 && !session.anyEditorHeld())
209
+ return openNewEntry();
210
+ return false;
211
+ }
212
+ if (key.ctrl && key.name === "e" && entryEditFor !== null && entryEditFor.position !== null)
213
+ return toggleEntryFileField();
214
+ if (key.ctrl && key.name === "e" && !session.anyEditorHeld() && ctx.view === ctx.roleIds.length + 1 && ctx.selectedEntry !== null)
215
+ return openSelectedEntry(ctx);
216
+ if (key.name === "escape" && entryEditFor)
217
+ return cancelEntryEdit();
218
+ if (key.name === "return" && entryEditFor)
219
+ return commitEntryEdit();
202
220
  return false;
203
221
  };
204
222
  return { handleKey, exit: exitEntryEdit };
@@ -1,5 +1,6 @@
1
1
  import { setConfigKey, unsetConfigKey } from "../../config/config-write.js";
2
2
  import { rawRoleField } from "../../roles/role-view.js";
3
+ import { isExponentSpelling } from "../../text/text.js";
3
4
  import { createRoleKeyEditor } from "./tui-editor-role-key.js";
4
5
  /** The Ctrl+G parallel-runner-count editor on the shared prompt line, the terminal twin of
5
6
  * `tumwater role <id> --instances` and the dashboard role drawer's instance-count editor. Enter
@@ -32,7 +33,7 @@ export function createInstancesEditor(session) {
32
33
  // refuses (and the dashboard drawer's instance editor), so this refuses it too, naming
33
34
  // the key and staying in edit mode. A non-exponent spelling falls through to
34
35
  // validateConfig, whose message names the key.
35
- if (!/^\d*\.?\d+$/.test(text) && /^\d+(\.\d+)?[eE][+-]?\d+$/.test(text)) {
36
+ if (isExponentSpelling(text)) {
36
37
  return { ok: false, error: `${cfgKey} must be a plain integer from 1 to 8 — no exponent notation` };
37
38
  }
38
39
  const r = setConfigKey(s.root, cfgKey, text);