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
@@ -103,6 +103,11 @@ export function followUntilParentGone(file, offset, onLines, seams) {
103
103
  });
104
104
  });
105
105
  }
106
+ /** The `-n` ceiling for the event-log dump (`tumwater logs -n <count>`): a terminal tail has
107
+ * no use for more than this many scanned events, and bounding it keeps an accidental
108
+ * `-n 999999999` from traversing the whole event file. Sister counts are bounded the same way
109
+ * (history's HISTORY_MAX_RUNS, report's REPORT_MAX_DAYS); the GUI clamps its logs counts too. */
110
+ const LOGS_MAX_EVENTS = 100_000;
106
111
  /** `tumwater logs [-f] [-n <count>] [--since <duration>] [--until <duration>]
107
112
  * [--role <id>[,<id>...]] [--transcript <id>] [--prompt]
108
113
  * [--grep <text>]`: follow or dump the harness event log, with `--since` a bounded
@@ -126,7 +131,7 @@ export async function cmdLogs(root, args, seams = {}) {
126
131
  return;
127
132
  }
128
133
  const nRaw = flagValue(feed.rest, "-n");
129
- const limit = nRaw !== null ? parseCountFlag("-n", nRaw) : 50;
134
+ const limit = nRaw !== null ? parseCountFlag("-n", nRaw, LOGS_MAX_EVENTS) : 50;
130
135
  // The event feed (no --transcript) never needs the config at all: parseRoleScope reads it only
131
136
  // when the flag is present.
132
137
  const role = parseRoleScope(root, feed.rest, "--transcript", LOGS_TRANSCRIPT_VALUE_ERROR);
@@ -278,39 +283,54 @@ function sayLogsWindowNoMatch(feed, windowPhrase) {
278
283
  else
279
284
  say(feed.roles !== null ? `${feed.roleEmpty} ${windowPhrase}` : `no events ${windowPhrase}`);
280
285
  }
281
- /** The `-n` tail view and its follow: `readEventsTailWithEnd`, the `shown` filter, the
282
- * `--count` branch and its empty-match prose, the per-event print, the `!follow` return, and
283
- * `followUntilParentGone` seeded from `tail.coveredEnd`. It owns the follow-loop locals. */
286
+ /** The `-n` tail view and its `-f` follow: read the filtered tail, print the `--count`
287
+ * tally or the per-event rows, and seed the follow; each job lives in a named helper. */
284
288
  async function sayLogsTail(root, feed, limit, seams) {
285
- // -n bounds the scan, not the print: the matched subset of the last `limit` events is what
286
- // appears, so `-n 200 --grep merge_failed` may print 3 rows from 200 scanned. The read is
287
- // the tail-with-end shape (not the per-poll cached readEvents) so the follow below can seed
288
- // from the byte end this very read covered instead of a later stat of the file.
289
- const tail = readEventsTailWithEnd(root, limit);
290
- const shown = feed.grepLower !== null || feed.type !== null || feed.roles !== null || feed.notable ? tail.events.filter(feed.filter) : tail.events;
291
- // `--count` over the -n view answers with the tally of exactly the filtered rows the plain
292
- // view would print, and carries over the plain view's empty-match prose below (a `--type`
293
- // or `--grep` that matches nothing names its value; an empty unfiltered -n view prints
294
- // nothing, as the plain view does). In JSON mode the empty output is the answer.
289
+ const { shown, coveredEnd } = readTailShown(root, feed, limit);
295
290
  if (feed.count) {
291
+ // `--count` over the -n view answers with the tally of exactly the filtered rows the plain
292
+ // view would print, and carries over the plain view's empty-match prose (a `--type` or
293
+ // `--grep` that matches nothing names its value; an empty unfiltered -n view prints
294
+ // nothing, as the plain view does). In JSON mode the empty output is the answer.
296
295
  if (shown.length !== 0)
297
296
  sayTally(shown, feed.json);
298
- else if (!feed.json && (feed.filtersGrepOrType || feed.roles !== null || feed.notable))
299
- sayLogsTailNoMatch(feed);
297
+ else
298
+ sayTailEmptyNote(shown, feed);
300
299
  return;
301
300
  }
302
301
  for (const e of shown)
303
302
  sayEventLine(e, feed.json);
304
303
  if (!feed.follow) {
305
- // The empty-match line is prose: in JSON mode the empty output is the answer. It fires
306
- // for either filter, naming the flag's value(s) whose match came up empty so the operator
307
- // knows what to loosen.
308
- if (shown.length === 0 && !feed.json && (feed.filtersGrepOrType || feed.roles !== null || feed.notable))
309
- sayLogsTailNoMatch(feed);
304
+ sayTailEmptyNote(shown, feed);
310
305
  return;
311
306
  }
307
+ await followLogsTail(root, feed, coveredEnd, seams);
308
+ }
309
+ /** How `-n` scans versus prints: `limit` bounds the scan, not the print — the matched
310
+ * subset of the last `limit` events is what appears, so `-n 200 --grep merge_failed` may
311
+ * print 3 rows from 200 scanned. The read is the tail-with-end shape (not the per-poll
312
+ * cached readEvents) so the follow can seed from the byte end this very read covered
313
+ * instead of a later stat of the file. It also owns the share of output to print: the
314
+ * `shown` rows in read order. */
315
+ function readTailShown(root, feed, limit) {
316
+ const tail = readEventsTailWithEnd(root, limit);
317
+ const shown = feed.grepLower !== null || feed.type !== null || feed.roles !== null || feed.notable ? tail.events.filter(feed.filter) : tail.events;
318
+ return { shown, coveredEnd: tail.coveredEnd };
319
+ }
320
+ /** The empty-match guard shared by the `--count` branch and the no-follow plain branch: the
321
+ * empty-match line is prose (in JSON mode the empty output is the answer) and fires for
322
+ * either filter, naming the flag's value(s) whose match came up empty so the operator knows
323
+ * what to loosen. */
324
+ function sayTailEmptyNote(shown, feed) {
325
+ if (shown.length === 0 && !feed.json && (feed.filtersGrepOrType || feed.roles !== null || feed.notable))
326
+ sayLogsTailNoMatch(feed);
327
+ }
328
+ /** The `-f` follow: resolve the event-log path and stream every newly landed event through
329
+ * the same `feed.filter` as the seeded window, seeded from `coveredEnd` (the byte end the
330
+ * initial read covered) and holding the filter across rotation. */
331
+ async function followLogsTail(root, feed, coveredEnd, seams) {
312
332
  const file = eventsLogPath(root);
313
- // Seed the follow from the byte end the initial read covered (tail.coveredEnd), mirroring
333
+ // Seed the follow from the byte end the initial read covered (coveredEnd), mirroring
314
334
  // cmdLogsTranscript's tail.end seed: an event appended between the read and the follow's
315
335
  // start lies past the printed window but at or above this offset, so the follow's first
316
336
  // poll delivers it — seeding from a fresh statOrNull(file).size instead would start past
@@ -318,7 +338,7 @@ async function sayLogsTail(root, feed, limit, seams) {
318
338
  // log scans to coveredEnd 0, and followFile tolerates a missing file (its poll re-stats
319
339
  // every interval), so a read-only command never leaves a harness state file behind — the
320
340
  // module contract above promises writes to stdout only.
321
- await followUntilParentGone(file, tail.coveredEnd, (lines) => {
341
+ await followUntilParentGone(file, coveredEnd, (lines) => {
322
342
  for (const line of lines.filter(Boolean)) {
323
343
  const e = parseEventLine(line);
324
344
  // The filter holds across rotation: every event the follow callback sees goes through
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { fail, say } from "./cli-output.js";
3
3
  import { loadConfig } from "../config/config.js";
4
4
  import { TUMWATER_SOURCE } from "../events/events.js";
5
- import { notifyCommandEnv, notifyExitGloss } from "../events/notify.js";
5
+ import { notifyCommandEnv, notifyExitGloss } from "../events/event-notify.js";
6
6
  /** The one representative allowlisted event `tumwater notify --test` fires: a failed merge
7
7
  * with source `tumwater`. Fixed and opinionated — the goal is to prove the hook runs, not to
8
8
  * replay a specific page, so there are no per-type or per-message flags. */
@@ -16,7 +16,7 @@ const NOTIFY_TEST_MESSAGE = "tumwater notify --test";
16
16
  * Generous enough not to misclassify a hook that is merely slow. */
17
17
  export const NOTIFY_TEST_TIMEOUT_MS = 10_000;
18
18
  /** Run one notify hook to completion, returning its exit code. Unlike the notifier's detached
19
- * fire-and-forget spawn (src/events/notify.ts), this waits with inherited stdio by
19
+ * fire-and-forget spawn (src/events/event-notify.ts), this waits with inherited stdio by
20
20
  * default (the dashboard passes "ignore"), so the
21
21
  * operator sees the hook's own output and its exit. A shell that cannot start resolves 127 —
22
22
  * the conventional "command not found" code. A hook that does not finish within `timeoutMs`
@@ -1,8 +1,9 @@
1
1
  /** The CLI layer of `tumwater questions` — the `tumwater questions` command body: list the open
2
2
  * questions in QUESTIONS.md numbered, or `answer <n> <decision>` one, which moves its entry from
3
3
  * `## Open` to `## Answered` with a dated operator answer. The markdown surgery itself — the
4
- * fence-aware `### ` block cut and the `## Answered` skeleton — lives in src/backlog/
5
- * question-answer.ts (the shared QUESTIONS.md core the TUI and dashboard also call); this module
4
+ * fence-aware `### ` block cut and the `## Answered` skeleton — lives in
5
+ * src/backlog/backlog-question-answer.ts (the shared QUESTIONS.md core the TUI and dashboard also
6
+ * call); this module
6
7
  * is the command's argument scan, say/fail/editor jacket and commit-and-wake tail over that core,
7
8
  * routed to by src/cli.ts.
8
9
  */
@@ -17,8 +18,14 @@ import { typoSuffix } from "../text/suggest.js";
17
18
  import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
18
19
  import { stampedSubject } from "../git/commit-message.js";
19
20
  import { wakeQuestionAsker } from "../operator/operator-intent.js";
20
- import { answeredQuestionAt, answeredQuestionCount, answeredQuestionList, answeredQuestionPayload, openQuestionAt, openQuestionList, questionListPayload, renderShownQuestion, UNCLOSED_FENCE_MESSAGE, tryAnswerQuestion } from "../backlog/question-answer.js";
21
+ import { answeredQuestionAt, answeredQuestionCount, answeredQuestionList, answeredQuestionPayload, openQuestionAt, openQuestionList, questionListPayload, renderShownQuestion, UNCLOSED_FENCE_MESSAGE, tryAnswerQuestion } from "../backlog/backlog-question-answer.js";
21
22
  import { bodyStructureProblem, normalizeEntryBody } from "../backlog/backlog-write.js";
23
+ /** `questions --answered -n`'s ceiling: bounds how many answered questions one list shows,
24
+ * so a typo'd huge count fails fast with the shared `must be between 1 and <max>` wording
25
+ * (the logs/history/report pattern) instead of silently meaning "everything". The list data
26
+ * is small (QUESTIONS.md), so the cap never meaningfully binds — it exists for the uniform
27
+ * count-flag contract and the clear over-range rebuke, not for memory. */
28
+ const QUESTIONS_MAX_LISTED = 100_000;
22
29
  /** The `tumwater questions answer` entry point: `tryAnswerQuestion` without a title check, its
23
30
  * refusals mapped to `fail` (exit 1) with the CLI's wording. Returns the answered question's
24
31
  * title plus the collapsed decision that landed in the file. */
@@ -84,6 +91,72 @@ function sayQuestionList(root, json, pattern) {
84
91
  }
85
92
  /** `questions answer`'s `--file <path>` flag: the decision comes from the file, lines kept. */
86
93
  const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
94
+ /** The `--show`/`--answered`/`-n`/`--grep` arms — the flags only valid before the question's
95
+ * number positional. Each performs its read and writes its `scan.<field>` exactly as the
96
+ * inline scan did; returns `i` (the index of the last token consumed, after any `args[++i]`);
97
+ * every other token returns `undefined`. */
98
+ function readPreNumberFlag(scan, args, i) {
99
+ if (scan.numbered)
100
+ return undefined;
101
+ const arg = args[i];
102
+ if (arg === "--show") {
103
+ rejectRepeatFlag(scan.show !== undefined, "--show");
104
+ scan.show = parsePositionFlag("--show", args[++i], "--show 2");
105
+ return i;
106
+ }
107
+ if (arg === "--answered") {
108
+ rejectRepeatFlag(scan.answeredFlag, "--answered");
109
+ scan.answeredFlag = true;
110
+ return i;
111
+ }
112
+ if (arg === "-n") {
113
+ rejectRepeatFlag(scan.limit !== undefined, "-n");
114
+ scan.limit = parseCountFlag("-n", args[++i], QUESTIONS_MAX_LISTED);
115
+ return i;
116
+ }
117
+ if (arg === "--grep") {
118
+ rejectRepeatFlag(scan.grep !== undefined, "--grep");
119
+ const text = args[++i];
120
+ if (text === undefined || text === "")
121
+ fail("--grep needs text (usage: tumwater questions [--answered] --grep <text>)");
122
+ scan.grep = text;
123
+ return i;
124
+ }
125
+ return undefined;
126
+ }
127
+ /** The `--file` arm (valid anywhere), the `!numbered && --json` arm, and the unknown
128
+ * `--`-flag gate — in that order. Returns `i` when `--file`/`--json` consumed the token; the
129
+ * unknown-flag arm never returns (it `fail`s). */
130
+ function readAnywhereFlag(scan, args, i) {
131
+ const arg = args[i];
132
+ if (arg === "--file") {
133
+ rejectRepeatFlag(scan.file !== undefined, "--file");
134
+ scan.file = promptFileValue(args[++i]);
135
+ return i;
136
+ }
137
+ if (!scan.numbered && arg === "--json") {
138
+ rejectRepeatFlag(scan.jsonFlag, "--json");
139
+ scan.jsonFlag = true;
140
+ return i;
141
+ }
142
+ if (arg.startsWith("--") && arg !== "--json") {
143
+ rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
144
+ // The complete flag set (this gate only sees `--`-flags, but `-n` is a real flag the
145
+ // list should not omit) plus the same did-you-mean hint the sibling commands'
146
+ // unknown-argument gate (cli-flag-specs's failUnknownArgument) attaches, so a mistyped
147
+ // `--ansered` names `--answered` instead of only printing the valid list — the missing
148
+ // treatment the questions gate was the one unknown-argument caller without.
149
+ const spellings = ["--json", "--file", "--answered", "--show", "--grep", "-n"];
150
+ fail(`unknown argument: ${arg} (valid flags for tumwater questions: ${spellings.join(", ")})${typoSuffix(arg, spellings)}`);
151
+ }
152
+ return undefined;
153
+ }
154
+ /** The trailing `-n needs --answered` guard: `-n` limits the answered list, so without
155
+ * `--answered` it means nothing. */
156
+ function assertQuestionsFlags(scan) {
157
+ if (scan.limit !== undefined && !scan.answeredFlag)
158
+ fail("-n needs --answered (it limits the answered questions listed)");
159
+ }
87
160
  /** No requireReadyRepo gate, like backlog: the reader degrades to an empty list on a
88
161
  * missing QUESTIONS.md, so the command inspects any directory instead of refusing (a team
89
162
  * workspace still refuses).
@@ -102,72 +175,25 @@ const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
102
175
  * into JSON mode. Unknown `--`-prefixed tokens stay refused even there, so a misspelled
103
176
  * flag is still an error rather than silent decision text. */
104
177
  function parseQuestionsInvocation(args) {
105
- const words = [];
106
- let jsonFlag = false;
107
- let numbered = false;
108
- let file;
109
- let answeredFlag = false;
110
- let limit;
111
- let grep;
112
- let show;
178
+ const scan = { words: [], jsonFlag: false, numbered: false, file: undefined, answeredFlag: false, limit: undefined, grep: undefined, show: undefined };
113
179
  for (let i = 0; i < args.length; i++) {
114
- const arg = args[i];
115
- if (!numbered && arg === "--show") {
116
- rejectRepeatFlag(show !== undefined, "--show");
117
- show = parsePositionFlag("--show", args[++i], "--show 2");
180
+ const consumed = readPreNumberFlag(scan, args, i) ?? readAnywhereFlag(scan, args, i);
181
+ if (consumed !== undefined) {
182
+ i = consumed;
118
183
  continue;
119
184
  }
120
- if (!numbered && arg === "--answered") {
121
- rejectRepeatFlag(answeredFlag, "--answered");
122
- answeredFlag = true;
123
- continue;
124
- }
125
- if (!numbered && arg === "-n") {
126
- rejectRepeatFlag(limit !== undefined, "-n");
127
- limit = parseCountFlag("-n", args[++i]);
128
- continue;
129
- }
130
- if (!numbered && arg === "--grep") {
131
- rejectRepeatFlag(grep !== undefined, "--grep");
132
- const text = args[++i];
133
- if (text === undefined || text === "")
134
- fail("--grep needs text (usage: tumwater questions [--answered] --grep <text>)");
135
- grep = text;
136
- continue;
137
- }
138
- if (arg === "--file") {
139
- rejectRepeatFlag(file !== undefined, "--file");
140
- file = promptFileValue(args[++i]);
141
- continue;
142
- }
143
- if (!numbered && arg === "--json") {
144
- rejectRepeatFlag(jsonFlag, "--json");
145
- jsonFlag = true;
146
- continue;
147
- }
148
- if (arg.startsWith("--") && arg !== "--json") {
149
- rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
150
- // The complete flag set (this gate only sees `--`-flags, but `-n` is a real flag the
151
- // list should not omit) plus the same did-you-mean hint the sibling commands'
152
- // unknown-argument gate (cli-flag-specs's failUnknownArgument) attaches, so a mistyped
153
- // `--ansered` names `--answered` instead of only printing the valid list — the missing
154
- // treatment the questions gate was the one unknown-argument caller without.
155
- const spellings = ["--json", "--file", "--answered", "--show", "--grep", "-n"];
156
- fail(`unknown argument: ${arg} (valid flags for tumwater questions: ${spellings.join(", ")})${typoSuffix(arg, spellings)}`);
157
- }
158
- if (!numbered) {
185
+ if (!scan.numbered) {
159
186
  // The position is a plain-decimal positive integer (parsePositiveInt's rule, the same
160
187
  // one --cancel's position honors): bare Number() admitted hex ("0x2"), exponent
161
188
  // ("1e2"), and whitespace-padded spellings as a valid question number, answering a
162
189
  // question the operator never named.
163
- if (parsePositiveInt(arg) !== null)
164
- numbered = true;
190
+ if (parsePositiveInt(args[i]) !== null)
191
+ scan.numbered = true;
165
192
  }
166
- words.push(arg);
193
+ scan.words.push(args[i]);
167
194
  }
168
- if (limit !== undefined && !answeredFlag)
169
- fail("-n needs --answered (it limits the answered questions listed)");
170
- return { words, jsonFlag, file, answeredFlag, limit, grep, show };
195
+ assertQuestionsFlags(scan);
196
+ return { words: scan.words, jsonFlag: scan.jsonFlag, file: scan.file, answeredFlag: scan.answeredFlag, limit: scan.limit, grep: scan.grep, show: scan.show };
171
197
  }
172
198
  /** The `--show` output branch: one open (or, with `--answered`, answered) question in full. */
173
199
  function sayShownQuestion(root, inv) {