tumwater 0.1.3 → 0.2.0

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 (359) hide show
  1. package/README.md +63 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -0,0 +1,296 @@
1
+ // vocabulary: plain
2
+ /** The backlog archive (plans/backlog-archive.md): finished entries leave the live backlog files.
3
+ *
4
+ * Every role run reads PLANS.md and BUGS.md to find work, and by 2026-10-18 nine tenths of both
5
+ * files was finished history (1.88 MB of PLANS.md's 2.09 MB sat under `## Done`), re-read on
6
+ * every run. Finished entries therefore live in an archive file per backlog file —
7
+ * FINISHED_ARCHIVES in backlog-md.ts: PLANS-DONE.md and BUGS-FIXED.md — under the same `## `
8
+ * heading, newest first, so every section parser reads both files alike. The live file keeps
9
+ * its finished heading, empty.
10
+ *
11
+ * Roles keep the move step they already follow (paste the finished entry under `## Done` or
12
+ * `## Fixed`); the harness does the rest, two ways:
13
+ * - archiveNewFinishedEntries, right before a run's commit: the entries THIS change added to a
14
+ * live finished section move to the top of the archive, so the commit, the review and the
15
+ * merge all see the archived form, and a role's diff never carries anyone else's move.
16
+ * - archiveFinishedOnMain, at team start and from `tumwater archive`: the whole live section —
17
+ * legacy entries, compressed one-line records, a hand commit's move — goes to the archive in
18
+ * one commit on main.
19
+ * Readers of finished entries read the live section first, then the archive (backlog.ts). */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { withLock, withStateLockAsync } from "../concurrency/lock.js";
23
+ import { loadConfigSafe } from "../config/config.js";
24
+ import { logEventBestEffort, TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
25
+ import { readTextOrNull, writeTextAtomic } from "../files/files.js";
26
+ import { changeBaseRev, currentBranch, fileContentAt, headOf, targetBranch } from "../git/git.js";
27
+ import { stampedSubject } from "../git/commit-message.js";
28
+ import { COMMIT_IDENT, git, gitTry } from "../git/git-run.js";
29
+ import { mergeLockDir, primaryWorktreeLockPath } from "../paths.js";
30
+ import { plural } from "../text/phrases.js";
31
+ import { collapseWhitespace, errorMessage } from "../text/text.js";
32
+ import { FINISHED_ARCHIVES, fenceTracker, fencedHeadingTitle } from "./backlog-md.js";
33
+ import { actionableEntryRanges } from "./backlog-structure.js";
34
+ /** The placeholder a role leaves under an emptied section: never archived content. */
35
+ const PLACEHOLDER = /^_None yet\._$/;
36
+ /** The text a new archive file starts with: a title, how to read the file, and the section
37
+ * heading its entries sit under. */
38
+ export function archiveTemplate(spec) {
39
+ const [title, what] = spec.file === "PLANS.md" ? ["Done plans", "Finished plans"] : ["Fixed bugs", "Fixed bugs"];
40
+ return [
41
+ `# ${title}`,
42
+ "",
43
+ `${what}, newest first, moved here by tumwater from ${spec.file}'s \`## ${spec.section}\`. This file`,
44
+ "only grows: search it with grep, or read one entry by its line range. Never read it whole.",
45
+ "",
46
+ `## ${spec.section}`,
47
+ "",
48
+ ].join("\n");
49
+ }
50
+ /** An entry's identity across the change's base and head: its heading, whitespace-collapsed. */
51
+ function archiveKey(title) {
52
+ return collapseWhitespace(title);
53
+ }
54
+ /** The 0-based span of the first `## <title>` section in `lines`: its heading line, and the
55
+ * index of the next `## ` heading (or `lines.length`). Fence-aware through backlog-md.ts's
56
+ * shared guard, so a quoted `## ` line is body text. null when the section is absent. */
57
+ function sectionSpan(lines, title) {
58
+ const fenced = fenceTracker();
59
+ let heading = -1;
60
+ for (let i = 0; i < lines.length; i++) {
61
+ const found = fencedHeadingTitle(lines[i] ?? "", fenced, "## ");
62
+ if (found === null)
63
+ continue;
64
+ if (heading >= 0)
65
+ return { heading, end: i };
66
+ if (found === title)
67
+ heading = i;
68
+ }
69
+ return heading >= 0 ? { heading, end: lines.length } : null;
70
+ }
71
+ /** `lines` without its leading and trailing blank lines. */
72
+ function trimBlank(lines) {
73
+ let a = 0;
74
+ let b = lines.length;
75
+ while (a < b && (lines[a] ?? "").trim() === "")
76
+ a++;
77
+ while (b > a && (lines[b - 1] ?? "").trim() === "")
78
+ b--;
79
+ return lines.slice(a, b);
80
+ }
81
+ /** `lines` rejoined with the section at `span` holding `body` (already trimmed): the heading, a
82
+ * blank line and the body, then one blank line before the next section. An empty body leaves the
83
+ * bare heading. A document this section ends closes with exactly one newline. */
84
+ function withSectionBody(lines, span, body) {
85
+ const out = lines.slice(0, span.heading + 1);
86
+ if (body.length > 0)
87
+ out.push("", ...body);
88
+ const tail = lines.slice(span.end);
89
+ if (tail.length === 0)
90
+ return `${out.join("\n")}\n`;
91
+ return [...out, "", ...tail].join("\n");
92
+ }
93
+ /** The section's entries with their ranges, limited to the span's first section of that name. */
94
+ function entriesIn(md, span, section) {
95
+ return actionableEntryRanges(md, section).filter((e) => e.start - 1 > span.heading && e.start - 1 < span.end);
96
+ }
97
+ /** Move the selected part of `liveMd`'s finished section to the top of `archiveMd`'s (a new
98
+ * archive starts from archiveTemplate), keeping the moved lines' order. null when nothing moves,
99
+ * when either file lacks the section heading, or — for the commit-time sweep — when there is no
100
+ * archive yet and the live section holds other content: a repository that has not been archived
101
+ * keeps today's layout until `tumwater archive` (or the next team start) moves its history, so an
102
+ * archive never sits above older live entries. Pure. */
103
+ export function archiveFinishedText(liveMd, archiveMd, spec, select) {
104
+ const lines = liveMd.split("\n");
105
+ const span = sectionSpan(lines, spec.section);
106
+ if (span === null)
107
+ return null;
108
+ const entries = entriesIn(liveMd, span, spec.section);
109
+ let moved;
110
+ let kept;
111
+ let titles;
112
+ if ("keep" in select) {
113
+ const from = select.keep <= 0 ? span.heading + 1 : (entries[select.keep]?.start ?? 0) - 1;
114
+ if (from < 0)
115
+ return null; // no entry past the kept ones
116
+ // A `_None yet._` placeholder stays in the live section, never in the archive.
117
+ const tail = lines.slice(from, span.end);
118
+ moved = trimBlank(tail.filter((l) => !PLACEHOLDER.test(l.trim())));
119
+ kept = select.keep <= 0 ? [] : trimBlank(lines.slice(span.heading + 1, from));
120
+ if (kept.length === 0)
121
+ kept = tail.filter((l) => PLACEHOLDER.test(l.trim())).slice(0, 1);
122
+ titles = entries.slice(Math.max(0, select.keep)).map((e) => e.title);
123
+ }
124
+ else {
125
+ const fresh = entries.filter((e) => !select.baseKeys.has(archiveKey(e.title)));
126
+ if (fresh.length === 0)
127
+ return null;
128
+ const inFresh = (i) => fresh.some((e) => i >= e.start - 1 && i < e.end);
129
+ kept = [];
130
+ for (let i = span.heading + 1; i < span.end; i++)
131
+ if (!inFresh(i))
132
+ kept.push(lines[i] ?? "");
133
+ kept = trimBlank(kept);
134
+ if (archiveMd === null && kept.some((l) => l.trim() !== "" && !PLACEHOLDER.test(l.trim())))
135
+ return null;
136
+ moved = [];
137
+ for (const e of fresh) {
138
+ if (moved.length > 0)
139
+ moved.push("");
140
+ moved.push(...trimBlank(lines.slice(e.start - 1, e.end)));
141
+ }
142
+ titles = fresh.map((e) => e.title);
143
+ }
144
+ if (moved.length === 0)
145
+ return null;
146
+ const archiveLines = (archiveMd ?? archiveTemplate(spec)).split("\n");
147
+ const archiveSpan = sectionSpan(archiveLines, spec.section);
148
+ if (archiveSpan === null)
149
+ return null;
150
+ const existing = trimBlank(archiveLines.slice(archiveSpan.heading + 1, archiveSpan.end));
151
+ return {
152
+ live: withSectionBody(lines, span, kept),
153
+ archive: withSectionBody(archiveLines, archiveSpan, existing.length > 0 ? [...moved, "", ...existing] : moved),
154
+ titles,
155
+ };
156
+ }
157
+ /** Write one move: the archive first, so a crash in between leaves the entries in both files —
158
+ * a duplicate a reader shows twice — and never in neither. */
159
+ function writeMove(root, spec, move) {
160
+ writeTextAtomic(path.join(root, spec.archive), move.archive);
161
+ writeTextAtomic(path.join(root, spec.file), move.live);
162
+ }
163
+ /** The commit-time sweep: move the entries this change added to a live finished section of the
164
+ * worktree `wt` — those whose heading its merge-base with `mainBranch` did not hold there — to
165
+ * the top of the archive file, writing both files. Runs right before a run's commit, so the
166
+ * commit, the review and the merge see the archived form. Returns what moved; a file whose
167
+ * section is missing, or a repository not yet archived (archiveFinishedText), moves nothing. */
168
+ export async function archiveNewFinishedEntries(wt, mainBranch) {
169
+ const baseRev = await changeBaseRev(wt, mainBranch);
170
+ const out = [];
171
+ for (const spec of FINISHED_ARCHIVES) {
172
+ const live = readTextOrNull(path.join(wt, spec.file));
173
+ if (live === null)
174
+ continue;
175
+ const base = await fileContentAt(wt, baseRev, spec.file);
176
+ const baseKeys = new Set(actionableEntryRanges(base, spec.section).map((e) => archiveKey(e.title)));
177
+ const move = archiveFinishedText(live, readTextOrNull(path.join(wt, spec.archive)), spec, { baseKeys });
178
+ if (move === null)
179
+ continue;
180
+ writeMove(wt, spec, move);
181
+ out.push({ file: spec.file, archive: spec.archive, titles: move.titles });
182
+ }
183
+ return out;
184
+ }
185
+ /** The bulk moves for the checkout at `root`, computed in memory: each live finished section's
186
+ * body past its newest `keep` entries. Nothing is written. */
187
+ function planArchiveAll(root, keep) {
188
+ const plan = [];
189
+ for (const spec of FINISHED_ARCHIVES) {
190
+ const live = readTextOrNull(path.join(root, spec.file));
191
+ if (live === null)
192
+ continue;
193
+ const move = archiveFinishedText(live, readTextOrNull(path.join(root, spec.archive)), spec, { keep });
194
+ if (move !== null)
195
+ plan.push({ spec, move });
196
+ }
197
+ return plan;
198
+ }
199
+ /** How much finished history each live file of `root` still carries: the `### ` entries under
200
+ * its finished section and the section's size in bytes. Files with an empty section are left
201
+ * out — doctor warns about the rest. */
202
+ export function liveFinishedHistory(root) {
203
+ const out = [];
204
+ for (const { spec, move } of planArchiveAll(root, 0)) {
205
+ const live = readTextOrNull(path.join(root, spec.file)) ?? "";
206
+ out.push({
207
+ file: spec.file,
208
+ section: spec.section,
209
+ archive: spec.archive,
210
+ entries: move.titles.length,
211
+ bytes: Buffer.byteLength(live) - Buffer.byteLength(move.live),
212
+ });
213
+ }
214
+ return out;
215
+ }
216
+ /** The bulk archive, committed: move every live finished section of the primary checkout at
217
+ * `root` (past its newest `keep` entries) to its archive file and commit the touched files on the
218
+ * base branch as one `tumwater(operator):` commit. Holds the team's merge lock, so a merge
219
+ * cannot lose its fast-forward to this commit, and the primary-worktree lock the CLI backlog
220
+ * writers and the fast-forward share. Writes nothing when there is nothing to move, when the
221
+ * primary checkout is not on the base branch, or when any of the files has uncommitted changes;
222
+ * a failed commit restores the files. `tumwater archive` and team start call it. */
223
+ export async function archiveFinishedOnMain(root, opts = {}) {
224
+ const keep = opts.keep ?? 0;
225
+ if (planArchiveAll(root, keep).length === 0)
226
+ return { status: "nothing" };
227
+ return withLock(mergeLockDir(root), () => withStateLockAsync(primaryWorktreeLockPath(root), async () => {
228
+ const primary = await currentBranch(root);
229
+ const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
230
+ if (primary !== base) {
231
+ return { status: "skipped", reason: `your checkout is on ${primary ?? "a detached HEAD"}, not ${base}` };
232
+ }
233
+ const files = FINISHED_ARCHIVES.flatMap((a) => [a.file, a.archive]);
234
+ const status = await gitTry(root, "status", "--porcelain", "--", ...files);
235
+ if (status === null)
236
+ return { status: "skipped", reason: "could not read git status" };
237
+ const dirty = status.split("\n").filter((l) => l.length > 3).map((l) => l.slice(3));
238
+ if (dirty.length > 0) {
239
+ return { status: "skipped", reason: `${dirty.join(", ")} ${dirty.length > 1 ? "have" : "has"} uncommitted changes` };
240
+ }
241
+ // Re-planned under the locks: a merge may have moved main since the pre-check.
242
+ const plan = planArchiveAll(root, keep);
243
+ if (plan.length === 0)
244
+ return { status: "nothing" };
245
+ const touched = plan.flatMap(({ spec }) => [spec.file, spec.archive]);
246
+ const created = plan.map(({ spec }) => spec.archive).filter((f) => !fs.existsSync(path.join(root, f)));
247
+ for (const { spec, move } of plan)
248
+ writeMove(root, spec, move);
249
+ const count = plan.reduce((n, { move }) => n + move.titles.length, 0);
250
+ const subject = stampedSubject("operator", // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
251
+ `Archive ${plural(count, "finished entry", "finished entries")} to ${plan.map(({ spec }) => spec.archive).join(" and ")}`);
252
+ try {
253
+ await git(root, "add", "--", ...touched);
254
+ await git(root, ...COMMIT_IDENT, "commit", "--only", "-m", subject, "--", ...touched);
255
+ }
256
+ catch (err) {
257
+ await gitTry(root, "reset", "-q", "--", ...touched);
258
+ const tracked = touched.filter((f) => !created.includes(f));
259
+ await gitTry(root, "checkout", "--", ...tracked);
260
+ for (const f of created)
261
+ fs.rmSync(path.join(root, f), { force: true });
262
+ return { status: "skipped", reason: `the commit failed: ${errorMessage(err).trim().split("\n")[0]}` };
263
+ }
264
+ return {
265
+ status: "archived",
266
+ commit: await headOf(root, "HEAD"),
267
+ moved: plan.map(({ spec, move }) => ({ file: spec.file, archive: spec.archive, titles: move.titles })),
268
+ };
269
+ }), opts.lockTimeoutMs);
270
+ }
271
+ /** The team-start archive: before any run starts — no merge is in progress and no run is in
272
+ * flight — move whatever finished history the live files still carry into the archives, in one
273
+ * commit on main. That migrates a repository on its first start with this build and sweeps
274
+ * entries a hand commit moved into a live finished section. A move logs one `state_migrated`
275
+ * event; a skipped one (a dirty file, a checkout off the base branch) or a failure warns, and
276
+ * the team starts either way. */
277
+ export async function archiveAtTeamStart(root) {
278
+ try {
279
+ const outcome = await archiveFinishedOnMain(root);
280
+ if (outcome.status === "archived") {
281
+ logEventBestEffort(root, {
282
+ source: TUMWATER_SOURCE,
283
+ type: "state_migrated",
284
+ step: "backlogArchive",
285
+ moved: outcome.moved.reduce((n, f) => n + f.titles.length, 0),
286
+ detail: outcome.moved.map((f) => `${f.file} → ${f.archive}: ${plural(f.titles.length, "entry", "entries")}`).join("; "),
287
+ });
288
+ }
289
+ else if (outcome.status === "skipped") {
290
+ warnEventBestEffort(root, TUMWATER_SOURCE, `finished backlog entries were not archived: ${outcome.reason} — fix that, then run \`tumwater archive\``);
291
+ }
292
+ }
293
+ catch (err) {
294
+ warnEventBestEffort(root, TUMWATER_SOURCE, `finished backlog entries were not archived: ${errorMessage(err)}`);
295
+ }
296
+ }