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
@@ -68,7 +68,20 @@ function markerHasLabel(line) {
68
68
  * readCompleteLines uses. A line straddling a chunk boundary is held (tail + newline) until
69
69
  * its head arrives from an older chunk, so no line is ever decoded torn. */
70
70
  export function readTranscriptTail(file, limit, opts = {}) {
71
- const includePrompts = opts.includePrompts === true;
71
+ const opened = openTailFile(file);
72
+ if (opened === null)
73
+ return null;
74
+ try {
75
+ const state = scanTailFromEof(opened.fd, opened.size, limit, opts.includePrompts === true);
76
+ return finalizeTailWindow(file, state, limit, opts);
77
+ }
78
+ finally {
79
+ fs.closeSync(opened.fd);
80
+ }
81
+ }
82
+ /** Opens the log and stat-guards it: returns the opened `{ fd, size }`, or null when the file
83
+ * is missing, empty, or rotated away between stat and open. Owns the open/stat guard. */
84
+ function openTailFile(file) {
72
85
  const st = statOrNull(file);
73
86
  if (!st || st.size === 0)
74
87
  return null;
@@ -88,130 +101,160 @@ export function readTranscriptTail(file, limit, opts = {}) {
88
101
  fs.closeSync(fd);
89
102
  return null;
90
103
  }
91
- let pos = size; // Exclusive end of the not-yet-scanned region [pos - chunk, pos).
92
- let held = EMPTY_BUFFER; // Tail (with its newline) of a line that starts before the current chunk and ends inside it.
93
- const lines = []; // Complete lines, newest first.
94
- let candidates = 0; // Entry-candidate lines seen so far, counting from EOF.
95
- let end = 0; // Offset just past the last complete newline; 0 when the file has none.
96
- let stoppedAtBoundary = false;
97
- let armed = false; // A qualifying agent_start was recorded; walking older lines for its marker line.
98
- let boundaryIndex = -1; // Index in `lines` of the arming agent_start (valid while armed).
104
+ return { fd, size };
105
+ }
106
+ /** Finds the newest complete line's start in a chunk and sets up the straddling-line hold at
107
+ * the chunk's outer boundary; returns the walk's emitStart, or null when the whole chunk is one
108
+ * incomplete line. Owns the chunk-boundary arithmetic. */
109
+ function prepareTailChunk(fd, c, got, state) {
110
+ const lastNl = c.lastIndexOf(10);
111
+ if (lastNl < 0)
112
+ return null; // No complete line in this stretch yet.
113
+ if (state.end === 0)
114
+ state.end = state.pos - got + lastNl + 1; // The first complete line found is the newest one.
115
+ // The oldest line of c is incomplete when older bytes exist and the region does not
116
+ // start at a line boundary — hold it for the next chunk instead of decoding it torn.
117
+ let emitStart = 0;
118
+ if (state.pos - got > 0) {
119
+ const boundary = Buffer.alloc(1);
120
+ fs.readSync(fd, boundary, 0, 1, state.pos - got - 1);
121
+ if (boundary[0] !== 10) {
122
+ const firstNl = c.indexOf(10); // ≥ 0 — lastNl exists.
123
+ state.held = Buffer.from(c.subarray(0, firstNl + 1));
124
+ emitStart = firstNl + 1;
125
+ }
126
+ }
127
+ else {
128
+ state.held = EMPTY_BUFFER; // Reached file start: nothing older to hold for.
129
+ }
130
+ // A newline at c's start terminates a zero-length line (a blank line whose terminating
131
+ // \n is the region's first byte — e.g. the log starts with one). The walk below finds a
132
+ // line's start via lastIndexOf(10, lineEnd - 1), which for that line would search from
133
+ // index -1 and wrap to c's LAST newline — re-emitting every newer line until the
134
+ // candidate count reaches limit. Skip the zero-length line by starting the walk after it.
135
+ if (c[emitStart] === 10)
136
+ emitStart += 1;
137
+ return emitStart;
138
+ }
139
+ /** Advances the scan state across one pushed line: the arming walk while armed, else the
140
+ * candidate/agent_start transition; returns true when the walk must stop. Owns the arming
141
+ * state walk. */
142
+ function advanceTailScan(raw, state, includePrompts, limit) {
99
143
  // Advances the arming state across one pushed line while `armed` — walking older lines for
100
144
  // the label-bearing marker that precedes the arming agent_start. Every line pushed here
101
145
  // sits inside [marker .. EOF] when a boundary lands, so the walk keeps it. Returns true when
102
146
  // a boundary was found and the walk must stop; false to keep walking.
103
- const walkArmed = (raw) => {
147
+ const walkArmed = () => {
104
148
  if (raw.includes('"tumwater_run"')) {
105
149
  // A kind-only marker carries no label and is skipped — a full re-read lets it pass
106
150
  // through too — so the scan keeps walking to the labeled marker that actually applies,
107
151
  // or to the older agent_start when the run is unlabeled.
108
152
  if (markerHasLabel(raw)) {
109
- armed = false; // Boundary found — the post-loop EOF-while-armed fallback must not fire.
110
- stoppedAtBoundary = true; // Window [this marker .. EOF] renders identically to a full re-read.
153
+ state.armed = false; // Boundary found — the post-loop EOF-while-armed fallback must not fire.
154
+ state.stoppedAtBoundary = true; // Window [this marker .. EOF] renders identically to a full re-read.
111
155
  return true;
112
156
  }
113
157
  return false;
114
158
  }
115
159
  if (raw.includes('"agent_start"')) {
116
- lines.splice(boundaryIndex + 1); // No label before the next older run: unlabeled — stop at A.
117
- armed = false;
118
- stoppedAtBoundary = true;
160
+ state.lines.splice(state.boundaryIndex + 1); // No label before the next older run: unlabeled — stop at A.
161
+ state.armed = false;
162
+ state.stoppedAtBoundary = true;
119
163
  return true;
120
164
  }
121
165
  return false;
122
166
  };
123
- try {
124
- for (;;) {
125
- const len = Math.min(TAIL_SCAN_CHUNK, pos);
126
- if (len <= 0)
127
- break;
128
- const buf = Buffer.alloc(len);
129
- const got = fs.readSync(fd, buf, 0, len, pos - len);
130
- if (got <= 0)
131
- break; // Shrank under us — use what we have.
132
- // Covers the file region [pos - got, pos + held.length): this chunk plus the tail of
133
- // the line that straddles its newer boundary (if any).
134
- const c = held.length > 0 ? Buffer.concat([buf.subarray(0, got), held]) : buf.subarray(0, got);
135
- const lastNl = c.lastIndexOf(10);
136
- if (lastNl < 0) {
137
- held = Buffer.from(c); // No complete line in this stretch yet.
138
- pos -= got;
139
- continue;
140
- }
141
- if (end === 0)
142
- end = pos - got + lastNl + 1; // The first complete line found is the newest one.
143
- // The oldest line of c is incomplete when older bytes exist and the region does not
144
- // start at a line boundary — hold it for the next chunk instead of decoding it torn.
145
- let emitStart = 0;
146
- if (pos - got > 0) {
147
- const boundary = Buffer.alloc(1);
148
- fs.readSync(fd, boundary, 0, 1, pos - got - 1);
149
- if (boundary[0] !== 10) {
150
- const firstNl = c.indexOf(10); // ≥ 0 — lastNl exists.
151
- held = Buffer.from(c.subarray(0, firstNl + 1));
152
- emitStart = firstNl + 1;
153
- }
154
- }
155
- else {
156
- held = EMPTY_BUFFER; // Reached file start: nothing older to hold for.
157
- }
158
- // A newline at c's start terminates a zero-length line (a blank line whose terminating
159
- // \n is the region's first byte — e.g. the log starts with one). The walk below finds a
160
- // line's start via lastIndexOf(10, lineEnd - 1), which for that line would search from
161
- // index -1 and wrap to c's LAST newline — re-emitting every newer line until the
162
- // candidate count reaches limit. Skip the zero-length line by starting the walk after it.
163
- if (c[emitStart] === 10)
164
- emitStart += 1;
165
- let lineEnd = lastNl; // Byte index of the \n terminating the newest complete line in c.
166
- while (lineEnd >= emitStart) {
167
- const lineStart = c.lastIndexOf(10, lineEnd - 1) + 1;
168
- if (lineStart < emitStart)
169
- break; // Safety — cannot happen: emitStart follows a \n.
170
- const raw = c.toString("utf8", lineStart, lineEnd);
171
- if (!raw.trim()) {
172
- lineEnd = lineStart - 1; // Blank lines render nothing.
173
- continue;
174
- }
175
- lines.push(raw);
176
- if (armed) {
177
- // Walking for the label-bearing marker line preceding the arming agent_start.
178
- if (walkArmed(raw))
179
- break;
180
- }
181
- else if (isEntryCandidate(raw, includePrompts)) {
182
- candidates += 1;
183
- }
184
- else if (raw.includes('"agent_start"') && candidates >= limit) {
185
- armed = true; // Keep walking for the marker line preceding this agent_start.
186
- boundaryIndex = lines.length - 1;
187
- }
188
- lineEnd = lineStart - 1;
189
- }
190
- pos -= got;
191
- if (stoppedAtBoundary || pos <= 0)
192
- break;
167
+ if (state.armed)
168
+ return walkArmed();
169
+ if (isEntryCandidate(raw, includePrompts)) {
170
+ state.candidates += 1;
171
+ return false;
172
+ }
173
+ if (raw.includes('"agent_start"') && state.candidates >= limit) {
174
+ state.armed = true; // Keep walking for the marker line preceding this agent_start.
175
+ state.boundaryIndex = state.lines.length - 1;
176
+ }
177
+ return false;
178
+ }
179
+ /** Decodes and collects a chunk's complete lines newest-first, threading the arming state
180
+ * through advanceTailScan until the walk must stop. Owns the per-chunk line walk. */
181
+ function walkTailChunkLines(state, c, emitStart, includePrompts, limit) {
182
+ let lineEnd = c.lastIndexOf(10); // Byte index of the \n terminating the newest complete line in c.
183
+ while (lineEnd >= emitStart) {
184
+ const lineStart = c.lastIndexOf(10, lineEnd - 1) + 1;
185
+ if (lineStart < emitStart)
186
+ break; // Safety — cannot happen: emitStart follows a \n.
187
+ const raw = c.toString("utf8", lineStart, lineEnd);
188
+ if (!raw.trim()) {
189
+ lineEnd = lineStart - 1; // Blank lines render nothing.
190
+ continue;
193
191
  }
192
+ state.lines.push(raw);
193
+ if (advanceTailScan(raw, state, includePrompts, limit))
194
+ break;
195
+ lineEnd = lineStart - 1;
194
196
  }
195
- finally {
196
- fs.closeSync(fd);
197
+ }
198
+ /** The backward chunk-scan loop over the opened fd, from EOF to file start, returning the
199
+ * threaded TailScanState. Owns the chunk scan loop. */
200
+ function scanTailFromEof(fd, size, limit, includePrompts) {
201
+ const state = {
202
+ size,
203
+ pos: size, // Exclusive end of the not-yet-scanned region [pos - chunk, pos).
204
+ held: EMPTY_BUFFER, // Tail (with its newline) of a line that starts before the current chunk and ends inside it.
205
+ lines: [], // Complete lines, newest first.
206
+ candidates: 0, // Entry-candidate lines seen so far, counting from EOF.
207
+ end: 0, // Offset just past the last complete newline; 0 when the file has none.
208
+ stoppedAtBoundary: false,
209
+ armed: false, // A qualifying agent_start was recorded; walking older lines for its marker line.
210
+ boundaryIndex: -1, // Index in `lines` of the arming agent_start (valid while armed).
211
+ };
212
+ for (;;) {
213
+ const len = Math.min(TAIL_SCAN_CHUNK, state.pos);
214
+ if (len <= 0)
215
+ break;
216
+ const buf = Buffer.alloc(len);
217
+ const got = fs.readSync(fd, buf, 0, len, state.pos - len);
218
+ if (got <= 0)
219
+ break; // Shrank under us — use what we have.
220
+ // Covers the file region [pos - got, pos + held.length): this chunk plus the tail of
221
+ // the line that straddles its newer boundary (if any).
222
+ const c = state.held.length > 0
223
+ ? Buffer.concat([buf.subarray(0, got), state.held])
224
+ : buf.subarray(0, got);
225
+ const emitStart = prepareTailChunk(fd, c, got, state);
226
+ if (emitStart === null) {
227
+ state.held = Buffer.from(c); // No complete line in this stretch yet.
228
+ state.pos -= got;
229
+ continue;
230
+ }
231
+ walkTailChunkLines(state, c, emitStart, includePrompts, limit);
232
+ state.pos -= got;
233
+ if (state.stoppedAtBoundary || state.pos <= 0)
234
+ break;
197
235
  }
198
- if (armed) {
236
+ return state;
237
+ }
238
+ /** Builds the rendered window from a finished (or EOF-armed) scan: the arming splice, the
239
+ * whole-file fallback and the slice(-limit) guard. Owns the final window. */
240
+ function finalizeTailWindow(file, state, limit, opts) {
241
+ if (state.armed) {
199
242
  // Reached EOF while still arming: no marker precedes the run — unlabeled, stop at its
200
243
  // agent_start.
201
- lines.splice(boundaryIndex + 1);
202
- stoppedAtBoundary = true;
244
+ state.lines.splice(state.boundaryIndex + 1);
245
+ state.stoppedAtBoundary = true;
203
246
  }
204
- let entries = formatTranscript(lines.reverse(), opts); // Oldest first.
205
- if (entries.length < limit && stoppedAtBoundary) {
247
+ let entries = formatTranscript(state.lines.reverse(), opts); // Oldest first.
248
+ if (entries.length < limit && state.stoppedAtBoundary) {
206
249
  // Contentless assistant turns made the rendered entries fewer than the candidate count
207
250
  // promised — re-read the whole file (the pre-optimization behavior) so slice(-limit) is
208
251
  // exact.
209
- entries = formatTranscript(readCompleteLines(file, 0, size).lines, opts);
252
+ entries = formatTranscript(readCompleteLines(file, 0, state.size).lines, opts);
210
253
  }
211
254
  // limit can be 0 (or negative — nonsense a caller should never pass): entries.slice(-0) is
212
255
  // entries.slice(0) — the whole window — because -0 === 0, so a bare slice(-limit) would
213
256
  // return everything instead of nothing; guard it, the same contract readTranscript's take
214
257
  // guard pins (the last `limit` entries of a zero-sized window is the empty list).
215
258
  const kept = limit > 0 ? entries.slice(-limit) : [];
216
- return { entries: kept, end };
259
+ return { entries: kept, end: state.end };
217
260
  }
@@ -1,6 +1,6 @@
1
1
  import { answeredQuestionEntries, donePlanEntries, entryMatches, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries, } from "../../backlog/backlog.js";
2
2
  import { backlogHolds, backlogInProgress, backlogMarks, } from "../../backlog/backlog-eligibility.js";
3
- import { groupBlockedSeries, partsLabel } from "../../backlog/backlog-structure.js";
3
+ import { groupBlockedSeries, partsLabel } from "../../backlog/backlog-index-data.js";
4
4
  import { clipToWidth } from "../../text/text-width.js";
5
5
  import { DASHBOARD_ANSWERED_QUESTIONS, DASHBOARD_RECENT_FINISHED } from "../status-payload.js";
6
6
  /** Pure project-status pane logic for the TUI (src/ui/tui/tui.tsx): the three-section backlog body
@@ -16,14 +16,13 @@ export function createAnswerEditor(session) {
16
16
  session.held.answer = null;
17
17
  restoreDraft();
18
18
  };
19
- const handleKey = (_str, key, ctx) => {
20
- if (key.ctrl && key.name === "a" &&
21
- !session.anyEditorHeld() &&
19
+ // Ctrl+A opens the answer editor on the selected open question (mnemonic: **A**nswer);
20
+ // Enter records the decision through tryAnswerQuestion, the core `tumwater questions
21
+ // answer` and the dashboard's form share. The position is the question's 1-based index
22
+ // among the open questions: the browsable list puts plans, then bugs, then questions first.
23
+ const answerOpenEdit = (ctx) => {
24
+ if (!session.anyEditorHeld() &&
22
25
  ctx.view === ctx.roleIds.length + 1 && ctx.selectedEntry !== null) {
23
- // Ctrl+A opens the answer editor on the selected open question (mnemonic: **A**nswer);
24
- // Enter records the decision through tryAnswerQuestion, the core `tumwater questions
25
- // answer` and the dashboard's form share. The position is the question's 1-based index
26
- // among the open questions: the browsable list puts plans, then bugs, then questions first.
27
26
  try {
28
27
  const entries = session.flat();
29
28
  const sel = Math.min(ctx.selectedEntry, entries.length - 1);
@@ -46,47 +45,58 @@ export function createAnswerEditor(session) {
46
45
  }
47
46
  return true;
48
47
  }
49
- if (key.name === "escape" && answerFor) {
50
- // Esc cancels the answer editor, restoring the previous prompt text; nothing is written.
51
- exitAnswerMode();
52
- session.flashMessage("answer cancelled");
48
+ return false;
49
+ };
50
+ // Esc cancels the answer editor, restoring the previous prompt text; nothing is written.
51
+ const cancelAnswerEdit = () => {
52
+ exitAnswerMode();
53
+ session.flashMessage("answer cancelled");
54
+ return true;
55
+ };
56
+ // Enter records the answer through tryAnswerQuestion, titled so a question that moved
57
+ // since Ctrl+A is refused. A whitespace-only line, an unclosed fence, and a throw all
58
+ // flash and STAY in the editor so the text survives; answered and missing/moved both
59
+ // leave the editor and return the pane to list mode (the entry left the list).
60
+ const commitAnswerEdit = () => {
61
+ const target = answerFor;
62
+ if (target === null)
63
+ return false;
64
+ if (!session.input.trim()) {
65
+ session.flashMessage("questions answer needs a decision");
53
66
  return true;
54
67
  }
55
- if (key.name === "return" && answerFor) {
56
- // Enter records the answer through tryAnswerQuestion, titled so a question that moved
57
- // since Ctrl+A is refused. A whitespace-only line, an unclosed fence, and a throw all
58
- // flash and STAY in the editor so the text survives; answered and missing/moved both
59
- // leave the editor and return the pane to list mode (the entry left the list).
60
- const target = answerFor;
61
- if (!session.input.trim()) {
62
- session.flashMessage("questions answer needs a decision");
63
- return true;
64
- }
65
- let outcome;
66
- try {
67
- outcome = tryAnswerQuestion(session.root, target.position, session.input, target.title);
68
- }
69
- catch (err) {
70
- session.flashError(err);
71
- return true;
72
- }
73
- if (outcome.status === "fenced") {
74
- session.flashMessage(UNCLOSED_FENCE_MESSAGE);
75
- return true;
76
- }
77
- let woke = "";
78
- if (outcome.status === "answered") {
79
- session.commitBacklogEdit("QUESTIONS.md", `Answer question: ${outcome.title}`);
80
- if (wakeQuestionAsker(session.root, outcome.title) !== null)
81
- woke = ` · woke ${questionAsker(outcome.title)}`;
82
- }
83
- exitAnswerMode();
84
- session.clearBacklogSelection();
85
- session.flashMessage(outcome.status === "answered"
86
- ? `answered question #${target.position}: ${outcome.title}${woke}`
87
- : "that question was already answered or moved — nothing recorded");
68
+ let outcome;
69
+ try {
70
+ outcome = tryAnswerQuestion(session.root, target.position, session.input, target.title);
71
+ }
72
+ catch (err) {
73
+ session.flashError(err);
88
74
  return true;
89
75
  }
76
+ if (outcome.status === "fenced") {
77
+ session.flashMessage(UNCLOSED_FENCE_MESSAGE);
78
+ return true;
79
+ }
80
+ let woke = "";
81
+ if (outcome.status === "answered") {
82
+ session.commitBacklogEdit("QUESTIONS.md", `Answer question: ${outcome.title}`);
83
+ if (wakeQuestionAsker(session.root, outcome.title) !== null)
84
+ woke = ` · woke ${questionAsker(outcome.title)}`;
85
+ }
86
+ exitAnswerMode();
87
+ session.clearBacklogSelection();
88
+ session.flashMessage(outcome.status === "answered"
89
+ ? `answered question #${target.position}: ${outcome.title}${woke}`
90
+ : "that question was already answered or moved — nothing recorded");
91
+ return true;
92
+ };
93
+ const handleKey = (_str, key, ctx) => {
94
+ if (key.ctrl && key.name === "a")
95
+ return answerOpenEdit(ctx);
96
+ if (key.name === "escape" && answerFor)
97
+ return cancelAnswerEdit();
98
+ if (key.name === "return" && answerFor)
99
+ return commitAnswerEdit();
90
100
  return false;
91
101
  };
92
102
  return { handleKey, exit: exitAnswerMode };
@@ -23,76 +23,87 @@ export function createAttachEditor(session) {
23
23
  session.held.attach = false;
24
24
  restoreDraft();
25
25
  };
26
- const handleKey = (_str, key, ctx) => {
27
- if (key.ctrl && key.name === "v" && ctx.view === 0) {
28
- // Ctrl+V toggles image-attach mode on the Activity view's prompt line (mnemonic: attach
29
- // **v**ia the same key the reset-counters and move-to-top arms use on their own views):
30
- // the line edits one image path that rides into the next director prompt as the same
31
- // `[image attached: …]` `tumwater prompt --attach <path>` and the dashboard composer
32
- // write. Entering saves the director draft and blanks the line; leaving restores it.
33
- if (session.editorEntryDenied("attach"))
34
- return true;
35
- if (attachMode) {
36
- exitAttachMode();
37
- session.flashMessage("image attach cancelled");
38
- }
39
- else {
40
- // Mid-recall, hand the arrows' saved draft back before the attach editor takes the
41
- // line — the same settle the role editors do — so leaving restores the
42
- // draft, not a history entry, with no stale recall state riding along.
43
- restoreDraft = session.takeDraft();
44
- attachMode = true;
45
- session.held.attach = true;
46
- session.input = "";
47
- session.cursor = 0;
48
- session.flashMessage("attach an image path — Enter to attach, Esc to cancel");
49
- }
26
+ // Ctrl+V toggles image-attach mode on the Activity view's prompt line (mnemonic: attach
27
+ // **v**ia the same key the reset-counters and move-to-top arms use on their own views):
28
+ // the line edits one image path that rides into the next director prompt as the same
29
+ // `[image attached: …]` `tumwater prompt --attach <path>` and the dashboard composer
30
+ // write. Entering saves the director draft and blanks the line; leaving restores it.
31
+ // Guarded to the Activity view (`ctx.view === 0`), so the Transcript reset arm, the
32
+ // Backlog move-to-top arm, and the Queued pane's move-to-top arm keep `Ctrl+V` on their
33
+ // views: a guard miss returns false and falls through the dispatch.
34
+ const toggleAttachEdit = (ctx) => {
35
+ if (ctx.view !== 0)
36
+ return false;
37
+ if (session.editorEntryDenied("attach"))
50
38
  return true;
39
+ if (attachMode) {
40
+ exitAttachMode();
41
+ session.flashMessage("image attach cancelled");
51
42
  }
52
- if (key.name === "escape" && attachMode) {
53
- // Esc cancels the attach editor, restoring the previous prompt text and leaving any
54
- // already-saved pending images untouched.
43
+ else {
44
+ // Mid-recall, hand the arrows' saved draft back before the attach editor takes the
45
+ // line — the same settle the role editors do — so leaving restores the
46
+ // draft, not a history entry, with no stale recall state riding along.
47
+ restoreDraft = session.takeDraft();
48
+ attachMode = true;
49
+ session.held.attach = true;
50
+ session.input = "";
51
+ session.cursor = 0;
52
+ session.flashMessage("attach an image path — Enter to attach, Esc to cancel");
53
+ }
54
+ return true;
55
+ };
56
+ // Esc cancels the attach editor, restoring the previous prompt text and leaving any
57
+ // already-saved pending images untouched.
58
+ const cancelAttachEdit = () => {
59
+ exitAttachMode();
60
+ return true;
61
+ };
62
+ // Enter in attach mode reads the typed path as one image through the same rules
63
+ // `tumwater prompt --attach` uses (readPromptImageFiles), refuses a path no configured
64
+ // director seam can read up front, and stores it as a pending attachment on the session
65
+ // so the next director prompt carries it. Empty clears the pending images; a failed read
66
+ // or a refusal flashes and STAYS in the editor so the operator can fix the path. Either
67
+ // way the saved draft is restored.
68
+ const commitAttachEdit = () => {
69
+ const p = session.input.trim();
70
+ if (p === "") {
71
+ session.images = [];
55
72
  exitAttachMode();
73
+ session.flashMessage("image attach cleared");
56
74
  return true;
57
75
  }
58
- if (key.name === "return" && attachMode) {
59
- // Enter in attach mode reads the typed path as one image through the same rules
60
- // `tumwater prompt --attach` uses (readPromptImageFiles), refuses a path no configured
61
- // director seam can read up front, and stores it as a pending attachment on the session
62
- // so the next director prompt carries it. Empty clears the pending images; a failed read
63
- // or a refusal flashes and STAYS in the editor so the operator can fix the path. Either
64
- // way the saved draft is restored.
65
- const p = session.input.trim();
66
- if (p === "") {
67
- session.images = [];
68
- exitAttachMode();
69
- session.flashMessage("image attach cleared");
70
- return true;
71
- }
72
- let readImages;
73
- try {
74
- readImages = readPromptImageFiles([p]);
75
- }
76
- catch (err) {
77
- session.flashError(err);
78
- return true;
79
- }
80
- const guidance = imagePromptGuidance(session.root, "director");
81
- if (guidance.refusal) {
82
- session.flashMessage(guidance.refusal);
83
- return true;
84
- }
85
- const next = [...session.images, ...readImages];
86
- const countProblem = promptImagesCountProblem(next.length);
87
- if (countProblem !== null) {
88
- session.flashMessage(countProblem);
89
- return true;
90
- }
91
- session.images = next;
92
- exitAttachMode();
93
- session.flashMessage(guidance.notice ?? `attached ${path.basename(p)}`);
76
+ let readImages;
77
+ try {
78
+ readImages = readPromptImageFiles([p]);
79
+ }
80
+ catch (err) {
81
+ session.flashError(err);
94
82
  return true;
95
83
  }
84
+ const guidance = imagePromptGuidance(session.root, "director");
85
+ if (guidance.refusal) {
86
+ session.flashMessage(guidance.refusal);
87
+ return true;
88
+ }
89
+ const next = [...session.images, ...readImages];
90
+ const countProblem = promptImagesCountProblem(next.length);
91
+ if (countProblem !== null) {
92
+ session.flashMessage(countProblem);
93
+ return true;
94
+ }
95
+ session.images = next;
96
+ exitAttachMode();
97
+ session.flashMessage(guidance.notice ?? `attached ${path.basename(p)}`);
98
+ return true;
99
+ };
100
+ const handleKey = (_str, key, ctx) => {
101
+ if (key.ctrl && key.name === "v")
102
+ return toggleAttachEdit(ctx);
103
+ if (key.name === "escape" && attachMode)
104
+ return cancelAttachEdit();
105
+ if (key.name === "return" && attachMode)
106
+ return commitAttachEdit();
96
107
  return false;
97
108
  };
98
109
  return { handleKey, exit: exitAttachMode };