tumwater 0.1.0 → 0.1.1

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 (298) hide show
  1. package/README.md +57 -16
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/backlog/backlog-eligibility.js +141 -0
  4. package/dist/src/{backlog.js → backlog/backlog-md.js} +56 -71
  5. package/dist/src/{ui/backlog-report.js → backlog/backlog-render.js} +3 -2
  6. package/dist/src/backlog/backlog-structure.js +350 -0
  7. package/dist/src/backlog/backlog-write.js +204 -0
  8. package/dist/src/backlog/backlog.js +99 -0
  9. package/dist/src/{main-baseline.js → baseline/main-baseline.js} +53 -28
  10. package/dist/src/{main-red.js → baseline/main-red.js} +50 -39
  11. package/dist/src/{readme.js → brief.js} +16 -6
  12. package/dist/src/{budget.js → budget/budget.js} +62 -28
  13. package/dist/src/{fallback-breaker.js → budget/fallback-breaker.js} +60 -10
  14. package/dist/src/{build-check-counts.js → build/build-check-counts.js} +16 -6
  15. package/dist/src/{build-check-detect.js → build/build-check-detect.js} +28 -44
  16. package/dist/src/{build-check-events.js → build/build-check-events.js} +23 -9
  17. package/dist/src/{build-check-report.js → build/build-check-report.js} +9 -9
  18. package/dist/src/{build-check-scoped.js → build/build-check-scoped.js} +15 -14
  19. package/dist/src/{build-check.js → build/build-check.js} +7 -5
  20. package/dist/src/{build-info.js → build/build-info.js} +12 -6
  21. package/dist/src/{build-stage.js → build/build-stage.js} +65 -29
  22. package/dist/src/{dep-install.js → build/dep-install.js} +9 -6
  23. package/dist/src/{host-sleep.js → build/host-sleep.js} +4 -4
  24. package/dist/src/{change-data.js → change/change-data.js} +21 -18
  25. package/dist/src/{ui/change-preview.js → change/change-render.js} +14 -7
  26. package/dist/src/{cli-args.js → cli/cli-args.js} +85 -37
  27. package/dist/src/{cli-command-args.js → cli/cli-command-args.js} +148 -58
  28. package/dist/src/cli/cli-flag-specs.js +309 -0
  29. package/dist/src/cli/cli-marker-commands.js +43 -0
  30. package/dist/src/cli/cli-output.js +63 -0
  31. package/dist/src/cli/cli-query-commands.js +186 -0
  32. package/dist/src/{cli-run.js → cli/cli-run.js} +156 -44
  33. package/dist/src/cli/config-commands.js +97 -0
  34. package/dist/src/{help.js → cli/help.js} +89 -13
  35. package/dist/src/{ui → cli}/log-commands.js +21 -16
  36. package/dist/src/{question-commands.js → cli/question-commands.js} +41 -41
  37. package/dist/src/cli.js +80 -262
  38. package/dist/src/collections.js +71 -0
  39. package/dist/src/{check-permit.js → concurrency/check-permit.js} +12 -13
  40. package/dist/src/concurrency/lock.js +236 -0
  41. package/dist/src/{semaphore.js → concurrency/semaphore.js} +21 -17
  42. package/dist/src/config/config-editable-keys.js +15 -0
  43. package/dist/src/{config-example.js → config/config-example.js} +32 -16
  44. package/dist/src/{config-field-checks.js → config/config-field-checks.js} +105 -38
  45. package/dist/src/{config-live.js → config/config-live.js} +60 -17
  46. package/dist/src/{config-schema.js → config/config-schema.js} +33 -1
  47. package/dist/src/config/config-validation.js +427 -0
  48. package/dist/src/config/config-views.js +457 -0
  49. package/dist/src/config/config-write.js +383 -0
  50. package/dist/src/{config.js → config/config.js} +74 -23
  51. package/dist/src/config/model-selector.js +43 -0
  52. package/dist/src/{doctor-backlog.js → doctor/doctor-backlog.js} +15 -13
  53. package/dist/src/{doctor-checks.js → doctor/doctor-checks.js} +150 -58
  54. package/dist/src/doctor/doctor-launch-services.js +24 -0
  55. package/dist/src/doctor/doctor-model-checks.js +254 -0
  56. package/dist/src/{doctor-orphans.js → doctor/doctor-orphans.js} +5 -5
  57. package/dist/src/doctor/doctor-render.js +13 -0
  58. package/dist/src/{doctor.js → doctor/doctor.js} +20 -9
  59. package/dist/src/errno.js +1 -1
  60. package/dist/src/events/event-format.js +392 -0
  61. package/dist/src/{event-read.js → events/event-read.js} +55 -23
  62. package/dist/src/{event-window.js → events/event-window.js} +27 -18
  63. package/dist/src/{events.js → events/events.js} +45 -4
  64. package/dist/src/{notify.js → events/notify.js} +13 -11
  65. package/dist/src/{failure-cluster.js → failure/failure-cluster.js} +29 -10
  66. package/dist/src/{failure-data.js → failure/failure-data.js} +59 -25
  67. package/dist/src/{failure-report.js → failure/failure-render.js} +83 -57
  68. package/dist/src/{failure-state-change.js → failure/failure-state-change.js} +36 -24
  69. package/dist/src/{time-spend.js → failure/time-spend.js} +63 -38
  70. package/dist/src/{file-queue.js → files/file-queue.js} +1 -1
  71. package/dist/src/{files.js → files/files.js} +122 -20
  72. package/dist/src/{json-files.js → files/json-files.js} +13 -11
  73. package/dist/src/files/json-object.js +77 -0
  74. package/dist/src/{stat-cache.js → files/stat-cache.js} +1 -1
  75. package/dist/src/{tail.js → files/tail.js} +12 -11
  76. package/dist/src/{error-storm.js → fleet/error-storm.js} +9 -10
  77. package/dist/src/{failure-spread.js → fleet/failure-spread.js} +7 -8
  78. package/dist/src/{fleet-hold.js → fleet/fleet-hold.js} +59 -21
  79. package/dist/src/{fleet-polls.js → fleet/fleet-polls.js} +55 -35
  80. package/dist/src/{fleet-state.js → fleet/fleet-state.js} +71 -53
  81. package/dist/src/fleet/orchestrator-info.js +132 -0
  82. package/dist/src/fleet/reclaim.js +299 -0
  83. package/dist/src/gates/bootstrap-gates.js +99 -0
  84. package/dist/src/gates/budget-gates.js +205 -0
  85. package/dist/src/gates/disk-gate.js +78 -0
  86. package/dist/src/gates/gate-polls.js +276 -0
  87. package/dist/src/gates/gate-prompts.js +364 -0
  88. package/dist/src/gates/maintenance-quota.js +198 -0
  89. package/dist/src/{pause-gates.js → gates/pause-gates.js} +10 -10
  90. package/dist/src/gates/readiness.js +11 -0
  91. package/dist/src/{role-cap-gates.js → gates/role-cap-gates.js} +44 -28
  92. package/dist/src/{startup-gate.js → gates/startup-gate.js} +19 -16
  93. package/dist/src/{streak-gate.js → gates/streak-gate.js} +31 -33
  94. package/dist/src/{commit-message.js → git/commit-message.js} +45 -11
  95. package/dist/src/{git-diff.js → git/git-diff.js} +40 -2
  96. package/dist/src/git/git-run.js +235 -0
  97. package/dist/src/{git.js → git/git.js} +45 -92
  98. package/dist/src/git/slots-state.js +106 -0
  99. package/dist/src/git/worktree-pool.js +395 -0
  100. package/dist/src/git/worktree-use.js +156 -0
  101. package/dist/src/git/worktree.js +263 -0
  102. package/dist/src/git/xcrun-git.js +24 -0
  103. package/dist/src/{ui → gui}/gui-args.js +19 -18
  104. package/dist/src/{ui/gui.js → gui/gui-command.js} +5 -8
  105. package/dist/src/{ui → gui}/gui-endpoint-commands.js +57 -44
  106. package/dist/src/{ui → gui}/gui-endpoints.js +53 -28
  107. package/dist/src/gui/gui-server.js +256 -0
  108. package/dist/src/{ui → gui}/http-body.js +35 -16
  109. package/dist/src/{history-data.js → history/history-data.js} +45 -56
  110. package/dist/src/{ui → history}/history.js +12 -17
  111. package/dist/src/inbox/inbox-attachments.js +167 -0
  112. package/dist/src/inbox/inbox-cancel.js +107 -0
  113. package/dist/src/inbox/inbox-edit.js +64 -0
  114. package/dist/src/{inbox-submit.js → inbox/inbox-submit.js} +34 -14
  115. package/dist/src/inbox/inbox.js +288 -0
  116. package/dist/src/{pending-prompt.js → inbox/pending-prompt.js} +4 -4
  117. package/dist/src/{prompt-commands.js → inbox/prompt-commands.js} +135 -45
  118. package/dist/src/inbox/prompt-not-before.js +61 -0
  119. package/dist/src/{init-templates.js → init/init-templates.js} +78 -4
  120. package/dist/src/{init.js → init/init.js} +84 -89
  121. package/dist/src/landing/backlog-conflicts.js +116 -0
  122. package/dist/src/{landing-batch.js → landing/landing-batch.js} +91 -68
  123. package/dist/src/{landing-check-failures.js → landing/landing-check-failures.js} +65 -39
  124. package/dist/src/{landing-core.js → landing/landing-core.js} +105 -23
  125. package/dist/src/landing/landing-diff.js +57 -0
  126. package/dist/src/{landing-drain.js → landing/landing-drain.js} +14 -10
  127. package/dist/src/{landing-git.js → landing/landing-git.js} +78 -18
  128. package/dist/src/{landing-merge.js → landing/landing-merge.js} +72 -102
  129. package/dist/src/{landing-pipeline.js → landing/landing-pipeline.js} +19 -9
  130. package/dist/src/landing/landing-questions.js +16 -0
  131. package/dist/src/{landing-queue.js → landing/landing-queue.js} +31 -17
  132. package/dist/src/{landing-slot.js → landing/landing-slot.js} +57 -34
  133. package/dist/src/{landing-stack.js → landing/landing-stack.js} +35 -21
  134. package/dist/src/{landing-vetting.js → landing/landing-vetting.js} +22 -18
  135. package/dist/src/{leftover.js → loop/leftover.js} +50 -18
  136. package/dist/src/{loop-pi.js → loop/loop-pi.js} +125 -61
  137. package/dist/src/loop/loop-state.js +171 -0
  138. package/dist/src/loop/loop.js +893 -0
  139. package/dist/src/loop/model-fallback.js +75 -0
  140. package/dist/src/loop/revision.js +61 -0
  141. package/dist/src/{operator-commands.js → operator/operator-commands.js} +130 -43
  142. package/dist/src/{operator-intent.js → operator/operator-intent.js} +88 -42
  143. package/dist/src/operator/operator-requests.js +226 -0
  144. package/dist/src/operator/retire.js +145 -0
  145. package/dist/src/orchestrator/orchestrator-launch.js +159 -0
  146. package/dist/src/orchestrator/orchestrator-scheduling.js +270 -0
  147. package/dist/src/{orchestrator.js → orchestrator/orchestrator.js} +178 -184
  148. package/dist/src/{retention.js → orchestrator/retention.js} +5 -5
  149. package/dist/src/paths.js +104 -20
  150. package/dist/src/{command-shape.js → pi/command-shape.js} +15 -3
  151. package/dist/src/{pi-args.js → pi/pi-args.js} +5 -3
  152. package/dist/src/{readiness.js → pi/pi-bin.js} +11 -11
  153. package/dist/src/{pi-event-line.js → pi/pi-event-line.js} +18 -19
  154. package/dist/src/{pi-models.js → pi/pi-models.js} +59 -20
  155. package/dist/src/pi/pi-run-result.js +1 -0
  156. package/dist/src/{pi-stream.js → pi/pi-stream.js} +99 -21
  157. package/dist/src/pi/pi-watchdogs.js +176 -0
  158. package/dist/src/{pi.js → pi/pi.js} +108 -137
  159. package/dist/src/pi-extension/bounded-output.js +5 -51
  160. package/dist/src/pi-extension/context-budget.js +44 -37
  161. package/dist/src/pi-extension/context-shake.js +235 -0
  162. package/dist/src/pi-extension/context-usage.js +20 -0
  163. package/dist/src/pi-extension/full-output.js +62 -0
  164. package/dist/src/pi-extension/role-notes.js +90 -0
  165. package/dist/src/pi-extension/tool-result-content.js +15 -0
  166. package/dist/src/{launch-services.js → process/launch-services.js} +16 -30
  167. package/dist/src/{process-group.js → process/process-group.js} +79 -51
  168. package/dist/src/{process-table.js → process/process-table.js} +23 -13
  169. package/dist/src/{process.js → process/process.js} +62 -20
  170. package/dist/src/{supervisor.js → process/supervisor.js} +31 -10
  171. package/dist/src/prompt/principles.js +14 -0
  172. package/dist/src/{prompt-followup.js → prompt/prompt-followup.js} +1 -1
  173. package/dist/src/{prompt.js → prompt/prompt.js} +87 -44
  174. package/dist/src/{redeploy-policy.js → redeploy/redeploy-policy.js} +15 -11
  175. package/dist/src/{redeploy-probes.js → redeploy/redeploy-probes.js} +5 -2
  176. package/dist/src/{redeploy.js → redeploy/redeploy.js} +23 -17
  177. package/dist/src/{redeployer.js → redeploy/redeployer.js} +43 -17
  178. package/dist/src/{self-reload.js → redeploy/self-reload.js} +30 -3
  179. package/dist/src/report/report-data.js +325 -0
  180. package/dist/src/{ui → report}/report-render.js +22 -20
  181. package/dist/src/{ui → report}/report.js +12 -12
  182. package/dist/src/request-timeouts.js +6 -0
  183. package/dist/src/review/known-flakes.js +45 -0
  184. package/dist/src/{review-followup.js → review/review-followup.js} +31 -40
  185. package/dist/src/{review-precheck.js → review/review-precheck.js} +37 -19
  186. package/dist/src/{review-verdict.js → review/review-verdict.js} +2 -2
  187. package/dist/src/{review.js → review/review.js} +88 -51
  188. package/dist/src/{suite-rerun.js → review/suite-rerun.js} +6 -5
  189. package/dist/src/roles/loop-ids.js +61 -0
  190. package/dist/src/{role-catalog.js → roles/role-catalog.js} +175 -29
  191. package/dist/src/{role-guidance.js → roles/role-guidance.js} +23 -10
  192. package/dist/src/{ui/role-report.js → roles/role-render.js} +12 -3
  193. package/dist/src/{role-view.js → roles/role-view.js} +28 -15
  194. package/dist/src/{roles.js → roles/roles.js} +47 -20
  195. package/dist/src/{backoff.js → scheduling/backoff.js} +12 -2
  196. package/dist/src/scheduling/claims.js +104 -0
  197. package/dist/src/{once-round.js → scheduling/once-round.js} +3 -6
  198. package/dist/src/{quiet-hours.js → scheduling/quiet-hours.js} +30 -11
  199. package/dist/src/{scheduling.js → scheduling/scheduling.js} +31 -41
  200. package/dist/src/{work-landed-cache.js → scheduling/work-landed-cache.js} +3 -3
  201. package/dist/src/status/status-data.js +225 -0
  202. package/dist/src/status/status-polls.js +202 -0
  203. package/dist/src/{datetime.js → text/datetime.js} +61 -16
  204. package/dist/src/text/format.js +58 -0
  205. package/dist/src/text/markdown.js +30 -0
  206. package/dist/src/text/phrases.js +228 -0
  207. package/dist/src/text/suggest.js +66 -0
  208. package/dist/src/{text-width.js → text/text-width.js} +1 -1
  209. package/dist/src/text/text.js +199 -0
  210. package/dist/src/{qa-coverage.js → tick/qa-coverage.js} +3 -3
  211. package/dist/src/tick/stage-check.js +276 -0
  212. package/dist/src/{telemetry-digest.js → tick/telemetry-digest.js} +6 -6
  213. package/dist/src/{tick-apply.js → tick/tick-apply.js} +51 -9
  214. package/dist/src/{tick-detail-data.js → tick/tick-detail-data.js} +9 -8
  215. package/dist/src/tick/tick-detail.js +122 -0
  216. package/dist/src/{tick-finalize.js → tick/tick-finalize.js} +41 -15
  217. package/dist/src/{tick-prompt.js → tick/tick-prompt.js} +89 -20
  218. package/dist/src/{tick-resume.js → tick/tick-resume.js} +5 -5
  219. package/dist/src/tick/tick-stage.js +181 -0
  220. package/dist/src/{tick-timing.js → tick/tick-timing.js} +44 -50
  221. package/dist/src/tick/tick-usage.js +112 -0
  222. package/dist/src/{tick-verdict.js → tick/tick-verdict.js} +55 -14
  223. package/dist/src/ui/badges.js +74 -19
  224. package/dist/src/ui/fleet-alerts.js +35 -16
  225. package/dist/src/ui/{gui-client-boot.js → gui/gui-client-boot.js} +8 -2
  226. package/dist/src/ui/{gui-client-composer.js → gui/gui-client-composer.js} +38 -6
  227. package/dist/src/ui/{gui-client-drawer.js → gui/gui-client-drawer.js} +56 -4
  228. package/dist/src/ui/{gui-client-fleet.js → gui/gui-client-fleet.js} +38 -23
  229. package/dist/src/ui/{gui-client-loops.js → gui/gui-client-loops.js} +15 -3
  230. package/dist/src/ui/{gui-client-model.js → gui/gui-client-model.js} +47 -20
  231. package/dist/src/ui/{gui-client-operator.js → gui/gui-client-operator.js} +5 -15
  232. package/dist/src/ui/gui/gui-client-pending.js +77 -0
  233. package/dist/src/ui/{gui-client-report.js → gui/gui-client-report.js} +9 -5
  234. package/dist/src/ui/{gui-client-settings.js → gui/gui-client-settings.js} +4 -2
  235. package/dist/src/ui/{gui-client.js → gui/gui-client.js} +79 -15
  236. package/dist/src/ui/{gui-page.js → gui/gui-page.js} +8 -6
  237. package/dist/src/ui/{gui-styles.js → gui/gui-styles.js} +10 -5
  238. package/dist/src/{progress-data.js → ui/progress-data.js} +68 -42
  239. package/dist/src/ui/status-model.js +122 -143
  240. package/dist/src/ui/status-payload.js +64 -20
  241. package/dist/src/ui/status-render.js +45 -15
  242. package/dist/src/ui/tick-progress-model.js +117 -0
  243. package/dist/src/ui/tone.js +28 -9
  244. package/dist/src/ui/transcript-tail.js +38 -14
  245. package/dist/src/ui/transcript.js +45 -36
  246. package/dist/src/ui/{tui-app.js → tui/tui-app.js} +2 -4
  247. package/dist/src/ui/{tui-backlog.js → tui/tui-backlog.js} +5 -4
  248. package/dist/src/ui/{tui-frame.js → tui/tui-frame.js} +15 -4
  249. package/dist/src/ui/{tui-input.js → tui/tui-input.js} +24 -84
  250. package/dist/src/ui/{tui-keys.js → tui/tui-keys.js} +26 -14
  251. package/dist/src/ui/tui/tui-pane.js +66 -0
  252. package/dist/src/ui/tui/tui-prompt-history.js +80 -0
  253. package/dist/src/ui/{tui.js → tui/tui.js} +45 -68
  254. package/dist/src/{fix-claim.js → verdict/fix-claim.js} +49 -21
  255. package/dist/src/{refusal.js → verdict/refusal.js} +11 -5
  256. package/dist/src/{reply-contract.js → verdict/reply-contract.js} +38 -27
  257. package/dist/src/version.js +3 -3
  258. package/package.json +1 -1
  259. package/dist/src/backlog-structure.js +0 -166
  260. package/dist/src/budget-gates.js +0 -128
  261. package/dist/src/cli-flag-specs.js +0 -185
  262. package/dist/src/cli-output.js +0 -44
  263. package/dist/src/config-commands.js +0 -58
  264. package/dist/src/config-validation.js +0 -307
  265. package/dist/src/config-views.js +0 -106
  266. package/dist/src/config-write.js +0 -217
  267. package/dist/src/event-format.js +0 -261
  268. package/dist/src/gate-polls.js +0 -150
  269. package/dist/src/gate-prompts.js +0 -210
  270. package/dist/src/inbox-attachments.js +0 -114
  271. package/dist/src/inbox.js +0 -392
  272. package/dist/src/json-object.js +0 -31
  273. package/dist/src/lock.js +0 -165
  274. package/dist/src/loop-state.js +0 -44
  275. package/dist/src/loop.js +0 -541
  276. package/dist/src/operator-requests.js +0 -114
  277. package/dist/src/orchestrator-launch.js +0 -119
  278. package/dist/src/phrases.js +0 -116
  279. package/dist/src/rank.js +0 -18
  280. package/dist/src/report-data.js +0 -186
  281. package/dist/src/status-data.js +0 -133
  282. package/dist/src/status-polls.js +0 -133
  283. package/dist/src/text.js +0 -183
  284. package/dist/src/tick-stage.js +0 -95
  285. package/dist/src/tick-usage.js +0 -67
  286. package/dist/src/ui/doctor-report.js +0 -9
  287. package/dist/src/ui/gui-server.js +0 -198
  288. package/dist/src/ui/tick-detail.js +0 -87
  289. package/dist/src/worktree.js +0 -154
  290. /package/dist/src/{run-marker.js → process/run-marker.js} +0 -0
  291. /package/dist/src/{exemptions.js → review/exemptions.js} +0 -0
  292. /package/dist/src/{tick-outcome.js → tick/tick-outcome.js} +0 -0
  293. /package/dist/src/ui/{gui-client-history.js → gui/gui-client-history.js} +0 -0
  294. /package/dist/src/ui/{gui-client-markdown.js → gui/gui-client-markdown.js} +0 -0
  295. /package/dist/src/ui/{gui-client-sound.js → gui/gui-client-sound.js} +0 -0
  296. /package/dist/src/ui/{gui-icons.js → gui/gui-icons.js} +0 -0
  297. /package/dist/src/ui/{tui-keymap.js → tui/tui-keymap.js} +0 -0
  298. /package/dist/src/{no-change.js → verdict/no-change.js} +0 -0
package/README.md CHANGED
@@ -10,10 +10,22 @@ local git repo, and no remote is ever touched.
10
10
 
11
11
  ![The tumwater web dashboard running tumwater's own fleet: a sidebar with the project, its fleet status, the Fleet, History, Usage, Failures, and Settings views, today's spend against the cap, and the pause control; an alert that the running build is behind main; the director prompt box; today's progress (loops in flight, commits landed, ticks, open backlog); and the loops grouped by what they are doing, each with its status, current work or last result, spend, and controls](docs/gui.png)
12
12
 
13
+ ## Install
14
+
15
+ Requires Node 20.3 or later on macOS or Linux.
16
+
17
+ ```bash
18
+ npm install -g tumwater
19
+ ```
20
+
21
+ Or run any command without installing: `npx tumwater`. To run from a checkout of this repo
22
+ instead: `npm install && npm run build && npm link`.
23
+
13
24
  ## Status
14
25
 
15
26
  <!-- tumwater:status:start -->
16
- **v0.1**: working harness. All 13 roles and the director are enabled by default.
27
+ **v0.1.1**: working harness. The director and 14 of the 15 roles are enabled by default; `telemetry`,
28
+ which files harness bugs from tumwater's own event log, is opt-in (`roles.telemetry.enabled`).
17
29
 
18
30
  Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
19
31
  [QUESTIONS.md](QUESTIONS.md) (open questions).
@@ -22,8 +34,6 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
22
34
  ## Usage
23
35
 
24
36
  ```bash
25
- npm install -g tumwater # or run any command with npx tumwater
26
- # from a checkout: npm install && npm run build && npm link
27
37
  cd your-project # a new or existing directory
28
38
  tumwater init "Build a tiny markdown-to-html converter CLI in Python."
29
39
  # add --template <id> to seed from a bundled starting point
@@ -31,30 +41,54 @@ tumwater init "Build a tiny markdown-to-html converter CLI in Python."
31
41
  # prints the catalog
32
42
  # add --file <path> to read the brief from a file
33
43
  # add --adopt to adopt an existing repo as-is
44
+ # a fresh/empty project is seeded with
45
+ # "bootstrap": {"untilPlansDone": 5}, holding the maintenance
46
+ # loops until 5 plans are done; remove "bootstrap" from
47
+ # tumwater.json to end that early
34
48
  tumwater run # start the loops (Ctrl+C to stop)
49
+ tumwater run --gui # ... and serve the browser dashboard at http://127.0.0.1:7180 from the same process
50
+ tumwater run --for 2h # run for a bounded window (capped at 90d), then drain and exit like Ctrl+C would
35
51
  ```
36
52
 
37
53
  Then, from another terminal:
38
54
 
39
55
  | To | Run |
40
56
  | --- | --- |
41
- | Watch the fleet | `tumwater tui`, or `tumwater gui` for the browser dashboard at http://127.0.0.1:7180 |
42
- | Watch per-tick history | `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>]`, or `tumwater history --json` for the rows as JSON; `tumwater tick <role> <n>` for one tick's full event trail (a summary header — when it ran, how long, result, usage — followed by the tick's events, oldest first; `--json` prints the payload as JSON, `null` when the log holds no such tick) |
43
- | Check state | `tumwater status`, `tumwater logs -f`, `tumwater logs --since <duration>`, `tumwater logs --grep <text>`, `tumwater logs --json` (the event feed as NDJSON, for scripts), `tumwater logs --role <id>`, `tumwater backlog` (planned features, open bugs, open questions as Markdown), `tumwater backlog --json` (the backlog as JSON, for scripts), `tumwater role <id>` (one loop's standing prompt — find text, `instructions` override, resolved model and interval, enabled/paused state — plus its next tick's assembled prompt, which shows the oldest queued prompt without consuming it (one is dequeued per tick; `--json` for scripts)) |
57
+ | Watch the fleet | `tumwater tui`, or the browser dashboard at http://127.0.0.1:7180 — `tumwater gui` on its own, or `tumwater run --gui` to boot the fleet and the dashboard together |
58
+ | Watch per-tick history | `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>]`, or `tumwater history --json` for the rows as JSON; `tumwater tick <role> <n>` for one tick's full event trail (a summary header — when it ran, how long, result, usage — followed by the tick's events, oldest first; `--last` shows the newest completed tick's trail instead of numbering one; `--json` prints the payload as JSON, `null` when the log holds no such tick) |
59
+ | Check state | `tumwater status`, `tumwater logs -f`, `tumwater logs --since <duration>`, `tumwater logs --grep <text>`, `tumwater logs --json` (the event feed as NDJSON, for scripts), `tumwater logs --role <id>`, `tumwater backlog` (planned features, open bugs, open questions as Markdown), `tumwater backlog --json` (the backlog as JSON, for scripts), `tumwater role <id>` (one loop's standing prompt — find text, `instructions` override, resolved model and interval, enabled/paused state, its notebook — plus its next tick's assembled prompt, which shows the oldest queued prompt without consuming it (one is dequeued per tick; `--json` for scripts)) |
44
60
  | See a loop's pending change | `tumwater diff --role <id>` — that loop's branch's unlanded commits (with the patch) and its worktree's uncommitted edits (staged and unstaged); without `--role`, one line per loop holding pending work; `--json` prints the payload as data |
45
- | Steer the project | `tumwater prompt "prefer no third-party deps"` queues a request for the director; add `--role <id>` to aim it at one loop's next tick, `tumwater prompt --list` shows the queued prompts numbered and grouped by loop, with how long each has waited (`--json` for scripts), and `tumwater prompt --cancel <n>` removes the Nth entry as `--list` shows them (add `--role <id>` when several loops share that number) |
61
+ | Steer the project | `tumwater prompt "prefer no third-party deps"` queues a request for the director; add `--role <id>` to aim it at one loop's next tick, `--file <path>` (`-` for stdin) to read the text from a file, or `--at <duration>` to defer it until the duration passes (45s, 90m, 1h30m, 1d; capped at 90d), `tumwater prompt --list` shows the queued prompts numbered and grouped by loop, with how long each has waited (`--json` for scripts), and `tumwater prompt --cancel <n>` removes the Nth entry as `--list` shows them, and `tumwater prompt --edit <n> "new text"` rewrites the Nth entry in place, keeping its position and wait time (both take `--role <id>` when several loops share that number), and `--attach <path>` (repeatable, up to 4 images — png, jpg, jpeg, gif, webp, bmp, each at most 5 MiB) attaches an image the receiving loop reads at its next tick; `tumwater bug "<symptom>"` files a bug into BUGS.md's Open section and wakes the bugfix loop, `tumwater plan "<title>" [body...]` files a plan request into PLANS.md's Planned section and wakes the feature loop — both stamped as operator-reported (the other half of `tumwater backlog`) |
46
62
  | Answer open questions | `tumwater questions` lists QUESTIONS.md's open questions numbered (as the loops wrote them); `tumwater questions answer <n> "decision text"` moves the Nth entry to ## Answered with your dated answer (loops read the file back on their next tick); `--json` prints the list as data |
47
- | Control the loops | `tumwater pause [--for <duration>]` / `resume [--role <id>]` (fleet or one loop; `--for 2h` auto-resumes, capped at 90d; add `--reason <text>` on a fleet pause to state why — it shows on `status`, the TUI, and the dashboard), `tumwater wake` (skip backoff), `tumwater abort --role <id>`, `tumwater stop` (drain and exit, like Ctrl+C) |
48
- | Audit | `tumwater doctor` (pre-flight; `--json` prints the report as JSON, for scripts), `tumwater report` (usage and cost; totals include landing runs — reviewer + conflict resolution), `tumwater report --since <duration>` (totals over a trailing window, capped at 7d), `tumwater report --json` (the `--days`/`--since`/`--failures` reports as JSON, for scripts), `tumwater report --failures` (the failure digest: per-role outcomes, each role's time and spend by outcome, and the top five loss causes ranked by agent-hours, with a marker naming how many were cut) |
63
+ | Control the loops | `tumwater pause [--for <duration>]` / `resume [--role <id>]` (fleet or one loop; `--for 2h` auto-resumes, capped at 90d; add `--reason <text>` on a fleet pause to state why — it shows on `status`, the TUI, and the dashboard), `tumwater wake` (skip backoff; `wake --in 45m` schedules the wake for 45 minutes from now — the marker is written immediately but consumed no earlier than the deadline, like `pause --for`'s auto-resume), `tumwater reclaim [--dry-run]` (drop gitignored build outputs from every harness worktree; `--dry-run` lists the candidates and cleans nothing), `tumwater retire --role <id>` (remove a disabled loop's worktree, branch, and per-role state — use after setting `enabled: false` for that role; `--force` overrides the safety rails), `tumwater abort --role <id>`, `tumwater stop` (drain and exit, like Ctrl+C), `tumwater reset-counters [--role <id>]` (zero the per-loop counters the dashboards show, starting a fresh observation window — scheduling is untouched) |
64
+ | Audit | `tumwater doctor` (pre-flight; `--json` prints the report as JSON, for scripts), `tumwater report` (usage and cost, with landed commits by role and the work/maintenance split; totals include landing runs — reviewer + conflict resolution), `tumwater report --since <duration>` (totals over a trailing window, capped at 7d), `tumwater report --json` (the `--days`/`--since`/`--failures` reports as JSON, for scripts), `tumwater report --failures` (the failure digest: per-role outcomes, each role's time and spend by outcome, and the top five loss causes ranked by agent-hours, with a marker naming how many were cut) |
49
65
 
50
66
  `tumwater help` lists every command and flag; `tumwater help <command>` shows one command's usage. `gui --all-interfaces` exposes the dashboard, and
51
67
  with it the director prompt, to your whole network, so pair it with `--token <secret>`.
52
68
 
53
- Settings live in `tumwater.json`: enabled roles, model, intervals, the daily spend cap
69
+ Settings live in `tumwater.json`: enabled roles, model (either one selector or a map of tiers
70
+ `small`/`default`/`strong` — omitted tiers inherit `default`; per-tier `fallback` overrides may name a
71
+ model or `"pause"`, and a role's `model` may name a tier; see [plans/model-tiers.md](plans/model-tiers.md)),
72
+ intervals, per-role `instances` (`feature` and `bugfix` may each run 1–8 parallel runners, every one
73
+ with its own branch and state; default 1), the daily spend cap
54
74
  (`maxDailyCostUsd`, with optional per-role caps `maxDailyCostUsdPerRole` — a loop over its own
55
- cap starts no new ticks until the next local day or a live edit), a nightly `quietHours` window
75
+ cap starts no new ticks until the next local day or a live edit), a
76
+ `maintenancePerWorkLanding` ratio (default 2; a rolling 24 h allowance of that many
77
+ code-maintenance landings per feature/bugfix/director landing, plus a floor of 12 — when the
78
+ window reaches it the scheduler holds the enabled maintenance loops until it rolls under, and a
79
+ fresh `tumwater wake <role>` or a queued prompt for one admits a single tick anyway), a nightly `quietHours` window
56
80
  (e.g. `"23:00-07:00"` local time) during
57
- which role loops start no new ticks (the director is exempt), user-defined `customLoops`, and an
81
+ which role loops start no new ticks (the director is exempt), with optional per-role windows
82
+ `quietHoursPerRole` — a loop inside its own window starts no new ticks, whether or not the
83
+ fleet-wide window covers now — a `diskHoldGB` free-space floor (default 10; when the volume
84
+ holding the worktrees drops below it, no new work starts until free space recovers 5 GB above it;
85
+ 0 disables), a `diskReclaimGB` pressure-reclaim threshold (default 40; below it idle worktrees
86
+ drop their gitignored build outputs before the hold engages; 0 disables), and a
87
+ `worktreeIdleReclaimHours` window (default 24; an hourly pass drops a worktree's gitignored build
88
+ outputs once it has sat unused that long, whatever the free space; 0 disables), a
89
+ `worktreeSlots` count of pooled checkouts shared by role ticks and landing vets (default
90
+ `maxConcurrent` + 1; the director's worktree sits outside the pool) — user-defined
91
+ `customLoops`, and an
58
92
  optional `notify` shell command run when the fleet needs a human (a budget pause, a budget warning
59
93
  at 80% of the cap while the gate is still open, an error-streak
60
94
  breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER_EVENT_TYPE`,
@@ -62,10 +96,17 @@ breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER
62
96
  Edits apply live while the fleet runs.
63
97
  From the terminal, `tumwater config` prints the effective config as JSON, `tumwater config get
64
98
  <key>` reads one resolved value, and `tumwater config set <key> <value>` writes one top-level
65
- key.
66
-
67
- **Backends:** any OpenAI-compatible model pi can reach works; set `provider` and `model` in
68
- `tumwater.json`. See [docs/backends.md](docs/backends.md) for requirements and a worked setup.
99
+ key; dotted keys (`maxDailyCostUsdPerRole.feature 1.5`, `roles.qa.model x`) merge one entry
100
+ into the existing map or role entry, while bare keys replace the whole value.
101
+
102
+ **Backends:** any OpenAI-compatible model pi can reach works; set `model` in `tumwater.json` to
103
+ one `provider/id[:thinking]` selector (plus `fallback` to a free selector), or a map of tiers
104
+ `small`/`default`/`strong`. A role whose primary keeps failing with provider-class errors
105
+ (three consecutive 429s or backend failures) runs its next ticks on its tier's resolved
106
+ `fallback` pair and probes the primary once the 5-minute cooldown elapses, returning to it
107
+ when a probe answers; `tumwater status`, the TUI, the dashboard, and `tumwater role <id>` name
108
+ the off-model episode. See [docs/backends.md](docs/backends.md) for requirements and a
109
+ worked setup.
69
110
 
70
111
  For how the loops, review gate, scheduling, and self-redeploy work, see
71
112
  [docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "4c69994273d9f4d707b0c5a6e7d00f50a3b265e6",
3
- "builtAt": 1791110618512,
2
+ "sha": "dd415e942c5569f2a1c468345a9277febbdaaed0",
3
+ "builtAt": 1791538389108,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -0,0 +1,141 @@
1
+ /** Backlog eligibility: which PLANS.md / BUGS.md entries a work loop may take now (plans,
2
+ * parallel-work-instances, part 2/7). An entry is held when its body carries a **Refused …**,
3
+ * **Needs review …** or **Needs replan …** note, or when its heading's trailing parenthetical
4
+ * names a prerequisite (`requires parts 1/5–3/5 landed`) that is itself still listed under
5
+ * `## Planned`. The parsers are pure over markdown text; `eligibleEntries` is the stat-backed
6
+ * reader for a caller that wants the list. The backlog index marks every held entry so a loop
7
+ * skips it without re-deriving the rule. */
8
+ import path from "node:path";
9
+ import { readTextOrNull } from "../files/files.js";
10
+ import { collapseWhitespace } from "../text/text.js";
11
+ import { NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX } from "../roles/role-guidance.js";
12
+ import { baseRoleOf } from "../roles/loop-ids.js";
13
+ import { trailingParenthetical } from "./backlog-md.js";
14
+ import { actionableEntryRanges, stripEntryStamp } from "./backlog-structure.js";
15
+ import { openBugEntries, plannedPlanEntries } from "./backlog.js";
16
+ /** The `**Refused …` note prefix a refusing tick writes. Every hold note — this one and the
17
+ * imported Needs-review and Needs-replan prefixes — is matched as a line prefix (hasNoteLine)
18
+ * so a mention in prose never holds an entry. */
19
+ const REFUSED_PREFIX = "**Refused ";
20
+ /** True when `body` carries a line (ignoring leading indentation) that begins with `prefix`.
21
+ * The one home of the hold-note rule: a note holds its entry only when written as its own
22
+ * line, never when its prefix is quoted inside prose. */
23
+ function hasNoteLine(body, prefix) {
24
+ return body.split("\n").some((line) => line.trimStart().startsWith(prefix));
25
+ }
26
+ /** One prerequisite ref, resolved against the entry's own series when it names none. */
27
+ function parseRef(chunk, ownSeries) {
28
+ const text = chunk.trim().replace(/^parts?\s+/i, "");
29
+ if (text === "")
30
+ return [];
31
+ const m = /^(?:([\s\S]+?)\s+)?([0-9]+[a-z]?)\/(\d+)(?:\s*[–-]\s*(\d+)\/(\d+))?$/i.exec(text);
32
+ if (m === null)
33
+ return [];
34
+ const series = (m[1]?.trim() ?? "") || ownSeries;
35
+ if (series === null || series === "")
36
+ return [];
37
+ const of = Number(m[3]);
38
+ const start = m[2].toLowerCase();
39
+ if (!Number.isInteger(of) || of < 1)
40
+ return [];
41
+ if (m[4] === undefined)
42
+ return [{ series, part: start, of }];
43
+ // A range is numeric on both ends (the lettered parts never range); expand it inclusively.
44
+ const from = Number(start);
45
+ const to = Number(m[4]);
46
+ if (!Number.isInteger(from) || !Number.isInteger(to) || to < from)
47
+ return [];
48
+ const parts = [];
49
+ for (let p = from; p <= to; p++)
50
+ parts.push({ series, part: String(p), of });
51
+ return parts;
52
+ }
53
+ /** The stable identity of an entry heading: its `(planned …)`/`(done …)` stamp suffix
54
+ * removed through the shared stripEntryStamp, whitespace collapsed, lowercased. Adding a
55
+ * done stamp or a Refused note therefore leaves the key unchanged. */
56
+ export function entryKey(title) {
57
+ return collapseWhitespace(stripEntryStamp(title)).toLowerCase();
58
+ }
59
+ /** An entry's own series and part from its heading, or null when the heading is not a
60
+ * `<Series>, part i/n: …` title. The series is the text before `, part i/n:`. */
61
+ export function seriesPart(title) {
62
+ const m = /^(.+?),\s*part\s+([0-9]+[a-z]?)\/(\d+)\s*:/i.exec(stripEntryStamp(title));
63
+ if (m === null)
64
+ return null;
65
+ const series = m[1].trim();
66
+ if (series === "")
67
+ return null;
68
+ return { series, part: m[2].toLowerCase(), of: Number(m[3]) };
69
+ }
70
+ /** The prerequisite parts a plan heading's trailing parenthetical names through the clause
71
+ * `requires <ref>((, | and )<ref>)* landed`, with each range (e.g. `parts 1/5–3/5`) expanded.
72
+ * Returns [] when the heading has no such clause or the clause does not parse — an agent judges
73
+ * an unparseable clause, exactly as it does today. Only the heading is read, never the body. */
74
+ export function requiredParts(title) {
75
+ const meta = trailingParenthetical(title);
76
+ if (meta === "")
77
+ return [];
78
+ const m = /requires\s+(.+?)\s+landed\b/i.exec(meta);
79
+ if (m === null)
80
+ return [];
81
+ const ownSeries = seriesPart(title)?.series ?? null;
82
+ const refs = [];
83
+ for (const chunk of m[1].split(/\s*,\s*|\s+and\s+/i))
84
+ refs.push(...parseRef(chunk, ownSeries));
85
+ return refs;
86
+ }
87
+ /** Why `entry` is held against the still-`planned` entries, or null when it may be taken: a
88
+ * `**Refused …` line (refused), a Needs-review line (needs-review), a Needs-replan line
89
+ * (needs-replan — the plan loop owns it), or a prerequisite `(series, part)` still among
90
+ * `planned` ({ blockedBy }). Series compare case-insensitively; part tokens compare verbatim.
91
+ * Bodies mentioning "requires" are never consulted — only the heading's trailing
92
+ * parenthetical. */
93
+ export function entryHold(entry, planned) {
94
+ if (hasNoteLine(entry.body, REFUSED_PREFIX))
95
+ return "refused";
96
+ if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX))
97
+ return "needs-review";
98
+ if (hasNoteLine(entry.body, NEEDS_REPLAN_PREFIX))
99
+ return "needs-replan";
100
+ const refs = requiredParts(entry.title);
101
+ if (refs.length === 0)
102
+ return null;
103
+ const parts = planned
104
+ .map((p) => seriesPart(p.title))
105
+ .filter((p) => p !== null);
106
+ const blocked = refs.filter((ref) => parts.some((p) => p.series.toLowerCase() === ref.series.toLowerCase() && p.part === ref.part));
107
+ return blocked.length === 0
108
+ ? null
109
+ : { blockedBy: blocked.map((ref) => `${ref.series} ${ref.part}/${ref.of}`) };
110
+ }
111
+ /** The entries a `role` loop may take now, in file order: PLANS.md's `## Planned` entries for
112
+ * feature, BUGS.md's `## Open` entries for bugfix, minus every entry entryHold holds, each with
113
+ * its stamp-free key, verbatim title and 1-based line range. Reads the primary checkout through
114
+ * the same stat-cached readers the index uses; a missing or unreadable file yields []. */
115
+ export function eligibleEntries(root, role) {
116
+ // An instance id (`bugfix-2`) resolves through its base role, exactly as claims.ts's
117
+ // roleSection does, so a claim's line range is read from the right file.
118
+ const bugfix = baseRoleOf(role) === "bugfix";
119
+ const file = bugfix ? "BUGS.md" : "PLANS.md";
120
+ const section = bugfix ? "Open" : "Planned";
121
+ const md = readTextOrNull(path.join(root, file));
122
+ if (md === null)
123
+ return [];
124
+ const entries = bugfix ? openBugEntries(root) : plannedPlanEntries(root);
125
+ const ranges = actionableEntryRanges(md, section);
126
+ const planned = plannedPlanEntries(root);
127
+ const out = [];
128
+ for (let i = 0; i < entries.length && i < ranges.length; i++) {
129
+ const entry = entries[i];
130
+ if (entryHold(entry, planned) !== null)
131
+ continue;
132
+ const range = ranges[i];
133
+ out.push({
134
+ key: entryKey(entry.title),
135
+ title: entry.title,
136
+ start: range.start,
137
+ end: range.end,
138
+ });
139
+ }
140
+ return out;
141
+ }
@@ -1,6 +1,18 @@
1
- import path from "node:path";
2
- import { readTextOrNull } from "./files.js";
3
- import { cachedByStat } from "./stat-cache.js";
1
+ /** The pure markdown layer of the backlog parsers: fence-aware reading of PLANS.md / BUGS.md /
2
+ * QUESTIONS.md text, with no filesystem access — every function here takes the markdown text as
3
+ * an argument. Split from src/backlog/backlog.ts, which keeps the stat-cached file readers
4
+ * (sectionEntries, the root-based plannedPlans/openBugs/openQuestions family, and the
5
+ * Done/Fixed date scan): consumers who only parse markdown (question-commands.ts's QUESTIONS.md
6
+ * walks, backlog-write.ts's section appends, backlog-structure.ts's stranding checks)
7
+ * import from here without reaching the file/cache layer, and a caller who parses a string never
8
+ * pays for a stat. Both layers share one fenceTracker state machine, so independent readers can
9
+ * never disagree about what is body content. */
10
+ /** The repo-root backlog markdown files shared by every layer that knows them by name: the
11
+ * tracked files loops edit and readers parse by `## ` section — read by backlog-structure.ts's
12
+ * structural checks, landing/backlog-conflicts.ts's mechanical insert-conflict resolver, and
13
+ * doctor-backlog.ts's duplicate-heading check. One definition, so the three cannot disagree
14
+ * about which files count. */
15
+ export const BACKLOG_FILES = new Set(["PLANS.md", "BUGS.md", "QUESTIONS.md"]);
4
16
  /** A per-line CommonMark fenced-code state machine, shared by every line-level parser of
5
17
  * backlog markdown. `inside(line)` feeds one line and returns whether it is fence syntax or
6
18
  * fenced content — never markdown structure: a fence opens at a ```` ``` ````/`~~~` line (an
@@ -41,20 +53,32 @@ export function fenceTracker() {
41
53
  },
42
54
  };
43
55
  }
56
+ /** The trimmed title of `line` when it is a markdown heading line at `prefix` level and not
57
+ * quoted content — null otherwise, including when fenceTracker reports the line inside a fenced
58
+ * code block. The single home of the fence-aware heading guard (`!fenced.inside(line) &&
59
+ * line.startsWith(prefix)` plus the slice/trim) that every line-level reader of the backlog
60
+ * docs repeats: sectionLines' section boundaries and parseEntryDetails' entry boundaries here,
61
+ * and question-commands.ts's walks of QUESTIONS.md (the Open/Answered scan, the `### ` block
62
+ * split, and the Answered section's start); that file's two pure next-`## ` boundary walks
63
+ * go through nextSectionHeading below instead. Readers that walk
64
+ * whole documents collecting heading lines go through fenceAwareHeadingLines instead; callers
65
+ * that only need "is this a heading" pass a tracker and compare the title or test for null. */
66
+ export function fencedHeadingTitle(line, fenced, prefix) {
67
+ return !fenced.inside(line) && line.startsWith(prefix) ? line.slice(prefix.length).trim() : null;
68
+ }
44
69
  /** The body lines of the `## <sectionTitle>` section of a markdown document: everything
45
70
  * between that heading line and the next `## ` line (or EOF), neither boundary included. A
46
71
  * `## ` line inside a fenced code block (entries quote markdown templates and shell traces) is
47
72
  * body content, never a boundary. The single home of "where a section starts and ends" — every
48
73
  * reader of a `## ` section (backlog entry parsing here, the usage report's Done/Fixed date
49
- * scan in src/report-data.ts, and backlog-structure.ts's strandedPlanEntries) walks its
74
+ * scan in src/report/report-data.ts, and backlog-structure.ts's strandedPlanEntries) walks its
50
75
  * section through this, so independent readers can never disagree about the boundary. */
51
76
  export function sectionLines(md, sectionTitle) {
52
77
  const lines = [];
53
78
  let inSection = false;
54
79
  const fenced = fenceTracker();
55
80
  for (const line of md.split("\n")) {
56
- const inFence = fenced.inside(line);
57
- if (!inFence && line.startsWith("## ")) {
81
+ if (fencedHeadingTitle(line, fenced, "## ") !== null) {
58
82
  inSection = line.slice(3).trim() === sectionTitle;
59
83
  continue;
60
84
  }
@@ -63,6 +87,20 @@ export function sectionLines(md, sectionTitle) {
63
87
  }
64
88
  return lines;
65
89
  }
90
+ /** The index of the next `## ` section heading at or after `from`, fence-aware through the
91
+ * caller's `fenced` tracker — or `lines.length` when none follows. The section-end boundary
92
+ * rule as an index, for readers that must cut or splice at the boundary rather than collect
93
+ * its content (question-commands.ts cuts the Open section at its end and inserts a moved
94
+ * block before Answered's next `## `; backlog-write.ts cuts the section a filed entry is
95
+ * appended to). Shares fencedHeadingTitle's guard with sectionLines, so
96
+ * a boundary found here is the same boundary sectionLines would stop at. */
97
+ export function nextSectionHeading(lines, from, fenced) {
98
+ for (let i = from; i < lines.length; i++) {
99
+ if (fencedHeadingTitle(lines[i] ?? "", fenced, "## ") !== null)
100
+ return i;
101
+ }
102
+ return lines.length;
103
+ }
66
104
  /** The body lines of one `## <sectionTitle>` section with fenced content stripped: the
67
105
  * sectionLines walk, then a fresh fenceTracker's filter — the shape every reader that
68
106
  * classifies a section's lines as markdown structure (entry headings, bullets, dates) walks.
@@ -117,7 +155,7 @@ export function parseEntryDetails(md, sectionTitle) {
117
155
  for (const line of sectionLines(md, sectionTitle)) {
118
156
  if (fenced.inside(line))
119
157
  bodyLines.push(line);
120
- else if (line.startsWith("### ")) {
158
+ else if (fencedHeadingTitle(line, fenced, "### ") !== null) {
121
159
  close();
122
160
  title = line.slice(4).trim();
123
161
  }
@@ -179,7 +217,7 @@ export function entryDates(md, sectionTitle, dateRe) {
179
217
  * paragraph is body, never matched). Joining with a space keeps "done\n2026-…" matchable.
180
218
  * Returns the joined text and the index of the first line NOT consumed as metadata, so a
181
219
  * walker can resume its scan there. Extracted from entryDates (its only original caller) so
182
- * the stranded-plan detector (src/backlog-structure.ts) matches dates against exactly the
220
+ * the stranded-plan detector (src/backlog/backlog-structure.ts) matches dates against exactly the
183
221
  * same joined text instead of growing a second, drifting copy of the join rule. */
184
222
  export function headingMetadata(lines, start) {
185
223
  const line = lines[start] ?? "";
@@ -197,14 +235,16 @@ export function headingMetadata(lines, start) {
197
235
  }
198
236
  return { text: meta.join(" "), next: j };
199
237
  }
200
- /** A `- ` line's trailing parenthetical's inner text, nesting-aware: a backward scan from the
201
- * line's closing ")" to its matching "(" returns the group's FULL inner text, so an epitaph
202
- * that quotes a parenthetical of its own — "(planned …, done …; commit abc1234 (re-landed
203
- * after review fix))" — still yields its date-bearing text. The previous flat `\([^()]*\)$`
204
- * match saw only the innermost group ("" when the line ended in two closes) and silently
205
- * dropped the epitaph's date from the day report (BUGS.md 2026-09-29). Unbalanced text (no
206
- * matching open paren) yields "" — the guard then treats the bullet as body text, as before. */
207
- function trailingParenthetical(line) {
238
+ /** A `- ` line's or `### ` heading's trailing parenthetical's inner text, nesting-aware: a
239
+ * backward scan from the text's closing ")" to its matching "(" returns the group's FULL inner
240
+ * text, so an epitaph that quotes a parenthetical of its own — "(planned …, done …; commit
241
+ * abc1234 (re-landed after review fix))" — still yields its date-bearing text. The previous
242
+ * flat `\([^()]*\)$` match saw only the innermost group ("" when the line ended in two closes)
243
+ * and silently dropped the epitaph's date from the day report (BUGS.md 2026-09-29). Unbalanced
244
+ * text (no matching open paren) yields "" — the guard then treats the bullet as body text, as
245
+ * before. The one home for this rule: entryDates reads it for `- ` epitaphs and
246
+ * backlog-eligibility.ts's requiredParts for a heading's prerequisite clause. */
247
+ export function trailingParenthetical(line) {
208
248
  if (!line.endsWith(")"))
209
249
  return "";
210
250
  let depth = 0;
@@ -228,58 +268,3 @@ function lastDate(text, dateRe) {
228
268
  const all = [...text.matchAll(global)];
229
269
  return all[all.length - 1]?.[1] ?? null;
230
270
  }
231
- /** Parsed sections keyed by file + section title (a future reader of a second section from the
232
- * same file must not collide with the first). Bounded inside cachedByStat: many short-lived
233
- * roots in tests would otherwise accumulate. Stores the richer {title, body} shape — one parse
234
- * per file change subsumes both the titles-only and the full-entry reads. */
235
- const sectionCache = new Map();
236
- /** The entries under `<root>/<fileName>`'s `## <sectionTitle>`: fresh when the file's identity
237
- * or mtime/size changed since the last read, cached otherwise (see module docs). A missing or
238
- * unreadable file yields [] — a render path must never throw on backlog state. */
239
- function sectionEntries(root, fileName, sectionTitle) {
240
- const file = path.join(root, fileName);
241
- return (cachedByStat(sectionCache, `${file}\u0000${sectionTitle}`, file, () => {
242
- const md = readTextOrNull(file); // Missing or unreadable — no data.
243
- return md === null ? null : parseEntryDetails(md, sectionTitle);
244
- }, (entries) => entries.map((e) => ({ ...e }))) ?? []);
245
- }
246
- /** Planned features: the `### ` headings under PLANS.md's `## Planned` section. Missing or
247
- * unreadable → []. */
248
- export function plannedPlans(root) {
249
- return sectionEntries(root, "PLANS.md", "Planned").map((e) => e.title);
250
- }
251
- /** Open bugs: the `### ` headings under BUGS.md's `## Open` section. Missing or unreadable → []. */
252
- export function openBugs(root) {
253
- return sectionEntries(root, "BUGS.md", "Open").map((e) => e.title);
254
- }
255
- /** Open questions: the `### ` headings under QUESTIONS.md's `## Open` section — loops post
256
- * them when a decision is genuinely the user's (see plans/questions-outbox.md). Missing or
257
- * unreadable → []. */
258
- export function openQuestions(root) {
259
- return sectionEntries(root, "QUESTIONS.md", "Open").map((e) => e.title);
260
- }
261
- /** Planned features as full entries (title + body), in file order — the TUI's project-status
262
- * browse and the GUI's /api/backlog endpoint read these. Missing or unreadable → []. */
263
- export function plannedPlanEntries(root) {
264
- return sectionEntries(root, "PLANS.md", "Planned");
265
- }
266
- /** Open bugs as full entries (title + body), in file order. Missing or unreadable → []. */
267
- export function openBugEntries(root) {
268
- return sectionEntries(root, "BUGS.md", "Open");
269
- }
270
- /** Open questions as full entries (title + body), in file order. Missing or unreadable → []. */
271
- export function openQuestionEntries(root) {
272
- return sectionEntries(root, "QUESTIONS.md", "Open");
273
- }
274
- /** The backlog as one machine-readable document: the three entry arrays the Markdown renderer
275
- * and the GUI's /api/backlog endpoint serve, so `tumwater backlog --json` prints the same data
276
- * every surface reads (status --json's "print the endpoint's payload" pattern). Each array keeps
277
- * file order and {title, body} verbatim, and a missing or unreadable file degrades to [] like the
278
- * individual readers — a bare directory yields the all-empty object, never an error. */
279
- export function backlogPayload(root) {
280
- return {
281
- plans: plannedPlanEntries(root),
282
- bugs: openBugEntries(root),
283
- questions: openQuestionEntries(root),
284
- };
285
- }
@@ -4,8 +4,9 @@
4
4
  * backlog.ts entry readers with their cache behavior, placeholder skipping, and Done/Fixed
5
5
  * exclusion the dashboards rely on — so the terminal view cannot drift from the dashboard
6
6
  * view (one parser, two surfaces) and the CLI's --json/human branches share one collection
7
- * (sayJsonOrRender's thunk) instead of each reading the entry files afresh. Entries render verbatim — heading text with its `(planned …)`/`(reported …)`
8
- * suffix, body lines indented two spaces under it — because these are markdown the loops
7
+ * (sayJsonOrRender's thunk) instead of each reading the entry files afresh. Entries render
8
+ * verbatim — heading text with its `(planned …)`/`(reported …)` suffix, body lines
9
+ * indented two spaces under it — because these are markdown the loops
9
10
  * wrote, including their Goal/Approach/Acceptance-criteria structure; reflowing them here
10
11
  * would make this command a worse reader of its own backlog than the files it summarizes. An
11
12
  * empty section renders a single `_(none)_` line rather than disappearing, so an all-clear