tumwater 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -30,10 +30,9 @@ import { commitAuthorArgs, committerIdentArgs, git, gitTry } from "../git/git-ru
30
30
  import { mergeLockDir } from "../paths.js";
31
31
  import { plural } from "../text/phrases.js";
32
32
  import { collapseWhitespace, errorFirstLine, errorMessage } from "../text/text.js";
33
- import { FINISHED_ARCHIVES, fenceTracker, fencedHeadingTitle, parseEntryDetails } from "./backlog-md.js";
34
- import { actionableEntryRanges, strandedPlanEntries } from "./backlog-structure.js";
35
- import { appendBugEntryLocked } from "./backlog-write.js";
36
- import { formatDate } from "../text/datetime.js";
33
+ import { FINISHED_ARCHIVES, fenceTracker, fencedHeadingTitle } from "./backlog-md.js";
34
+ import { actionableEntryRanges } from "./backlog-index-data.js";
35
+ import { strandedPlanEntries } from "./backlog-structure.js";
37
36
  /** The placeholder a role leaves under an emptied section: never archived content. */
38
37
  const PLACEHOLDER = /^_None yet\._$/;
39
38
  /** The text a new archive file starts with: a title, how to read the file, and the section
@@ -132,6 +131,57 @@ function entryBlocks(lines, entries) {
132
131
  }
133
132
  return out;
134
133
  }
134
+ /** The selected entries of `entries`: the keep slice, or the baseKeys/park filter. */
135
+ function selectArchiveEntries(entries, select) {
136
+ return "keep" in select
137
+ ? entries.slice(Math.max(0, select.keep))
138
+ : entries.filter((e) => !select.baseKeys.has(archiveKey(e.title)) && !(select.park?.has(archiveKey(e.title)) ?? false));
139
+ }
140
+ /** Add the 0-based live lines of `entries` to `into`. */
141
+ function addEntryLines(entries, into) {
142
+ for (const e of entries)
143
+ for (let i = e.start - 1; i < e.end; i++)
144
+ into.add(i);
145
+ }
146
+ /** The bulk-archive split: the tail past the kept entries moves but for the dropped and staying
147
+ * entries, and a `_None yet._` placeholder stays in the live section, never in the archive.
148
+ * null when no entry sits past the kept ones. */
149
+ function splitByKeep(lines, span, entries, keep, staying, droppedLine) {
150
+ const from = keep <= 0 ? span.heading + 1 : (entries[keep]?.start ?? 0) - 1;
151
+ if (from < 0)
152
+ return null; // no entry past the kept ones
153
+ // The tail moves but for the dropped and staying entries; a `_None yet._` placeholder stays
154
+ // in the live section, never in the archive. Staying entries follow the kept ones.
155
+ const stayLine = new Set();
156
+ addEntryLines(staying, stayLine);
157
+ const tail = [];
158
+ for (let i = from; i < span.end; i++)
159
+ if (!droppedLine.has(i) && !stayLine.has(i))
160
+ tail.push(lines[i] ?? "");
161
+ const moved = trimBlank(tail.filter((l) => !PLACEHOLDER.test(l.trim())));
162
+ const stayingBlocks = entryBlocks(lines, staying);
163
+ const head = keep <= 0 ? [] : trimBlank(lines.slice(span.heading + 1, from));
164
+ const kept = head.length > 0 && stayingBlocks.length > 0 ? [...head, "", ...stayingBlocks] : [...head, ...stayingBlocks];
165
+ return {
166
+ kept: kept.length === 0 ? tail.filter((l) => PLACEHOLDER.test(l.trim())).slice(0, 1) : kept,
167
+ moved,
168
+ };
169
+ }
170
+ /** The commit-time-sweep split: the kept body drops the moved and dropped lines, refusing when
171
+ * no archive exists and other content would stay in the live section. */
172
+ function splitByBaseKeys(lines, span, archiveMd, moving, droppedLine) {
173
+ const leaves = new Set(droppedLine);
174
+ addEntryLines(moving, leaves);
175
+ let kept = [];
176
+ for (let i = span.heading + 1; i < span.end; i++)
177
+ if (!leaves.has(i))
178
+ kept.push(lines[i] ?? "");
179
+ kept = trimBlank(kept);
180
+ if (archiveMd === null && kept.some((l) => l.trim() !== "" && !PLACEHOLDER.test(l.trim())))
181
+ return null;
182
+ const moved = entryBlocks(lines, moving);
183
+ return { kept, moved };
184
+ }
135
185
  /** Move the selected part of `liveMd`'s finished section to the top of `archiveMd`'s (a new
136
186
  * archive starts from archiveTemplate), keeping the moved lines' order. A selected entry whose
137
187
  * heading the archive already holds never moves (EntryFate): with the same text it is dropped
@@ -146,9 +196,7 @@ export function archiveFinishedText(liveMd, archiveMd, spec, select) {
146
196
  if (span === null)
147
197
  return null;
148
198
  const entries = entriesIn(liveMd, span, spec.section);
149
- const selected = "keep" in select
150
- ? entries.slice(Math.max(0, select.keep))
151
- : entries.filter((e) => !select.baseKeys.has(archiveKey(e.title)) && !(select.park?.has(archiveKey(e.title)) ?? false));
199
+ const selected = selectArchiveEntries(entries, select);
152
200
  if (!("keep" in select) && selected.length === 0)
153
201
  return null;
154
202
  const archiveText = archiveMd ?? archiveTemplate(spec);
@@ -160,46 +208,14 @@ export function archiveFinishedText(liveMd, archiveMd, spec, select) {
160
208
  const withFate = (fate) => selected.filter((_, i) => fates[i] === fate);
161
209
  // The 0-based live lines of the entries that leave the live section without moving.
162
210
  const droppedLine = new Set();
163
- for (const e of withFate("drop"))
164
- for (let i = e.start - 1; i < e.end; i++)
165
- droppedLine.add(i);
166
- let moved;
167
- let kept;
168
- if ("keep" in select) {
169
- const from = select.keep <= 0 ? span.heading + 1 : (entries[select.keep]?.start ?? 0) - 1;
170
- if (from < 0)
171
- return null; // no entry past the kept ones
172
- // The tail moves but for the dropped and staying entries; a `_None yet._` placeholder stays
173
- // in the live section, never in the archive. Staying entries follow the kept ones.
174
- const stayLine = new Set();
175
- for (const e of withFate("stay"))
176
- for (let i = e.start - 1; i < e.end; i++)
177
- stayLine.add(i);
178
- const tail = [];
179
- for (let i = from; i < span.end; i++)
180
- if (!droppedLine.has(i) && !stayLine.has(i))
181
- tail.push(lines[i] ?? "");
182
- moved = trimBlank(tail.filter((l) => !PLACEHOLDER.test(l.trim())));
183
- const staying = entryBlocks(lines, withFate("stay"));
184
- const head = select.keep <= 0 ? [] : trimBlank(lines.slice(span.heading + 1, from));
185
- kept = head.length > 0 && staying.length > 0 ? [...head, "", ...staying] : [...head, ...staying];
186
- if (kept.length === 0)
187
- kept = tail.filter((l) => PLACEHOLDER.test(l.trim())).slice(0, 1);
188
- }
189
- else {
190
- const leaves = new Set(droppedLine);
191
- for (const e of withFate("move"))
192
- for (let i = e.start - 1; i < e.end; i++)
193
- leaves.add(i);
194
- kept = [];
195
- for (let i = span.heading + 1; i < span.end; i++)
196
- if (!leaves.has(i))
197
- kept.push(lines[i] ?? "");
198
- kept = trimBlank(kept);
199
- if (archiveMd === null && kept.some((l) => l.trim() !== "" && !PLACEHOLDER.test(l.trim())))
200
- return null;
201
- moved = entryBlocks(lines, withFate("move"));
202
- }
211
+ addEntryLines(withFate("drop"), droppedLine);
212
+ const split = "keep" in select
213
+ ? splitByKeep(lines, span, entries, select.keep, withFate("stay"), droppedLine)
214
+ : splitByBaseKeys(lines, span, archiveMd, withFate("move"), droppedLine);
215
+ if (split === null)
216
+ return null;
217
+ const moved = split.moved;
218
+ const kept = split.kept;
203
219
  const dropped = withFate("drop").map((e) => e.title);
204
220
  if (moved.length === 0 && dropped.length === 0)
205
221
  return null;
@@ -301,6 +317,15 @@ function archiveCommitWhat(plan) {
301
317
  const archived = `Archive ${plural(count, "finished entry", "finished entries")} to ${archives.join(" and ")}`;
302
318
  return dropped > 0 ? `${archived}; drop ${drop}` : archived;
303
319
  }
320
+ /** One live backlog file's finished-history rows as liveFinishedHistory reports them, phrased
321
+ * and joined: `file ## <section> holds N finished entries (X KB)` rows separated by "; " — the
322
+ * shared detail head the doctor's `backlog archive` check (checkLiveFinished) and the always-on
323
+ * `backlog-archive` alert (src/ui/team-alert-backlog.ts) both render, so the row shape, the
324
+ * plural, and the KB rounding cannot drift between the two. Each caller appends its own trailing
325
+ * explanation, so the two texts stay byte-identical up to that suffix. */
326
+ export function liveFinishedRowList(rows) {
327
+ return rows.map((l) => `${l.file} ## ${l.section} holds ${plural(l.entries, "finished entry", "finished entries")} (${Math.round(l.bytes / 1024)} KB)`).join("; ");
328
+ }
304
329
  /** One archived file as a line of `tumwater archive`'s output and the team-start event: what
305
330
  * moved where, and what was dropped because the archive already held it. */
306
331
  export function describeArchivedFile(f) {
@@ -316,6 +341,17 @@ function planOrSkip(root, keep) {
316
341
  return { status: "skipped", reason: `a backlog file could not be read: ${errorFirstLine(err)}` };
317
342
  }
318
343
  }
344
+ /** If the primary checkout is not on the base branch, the reason a main-side write should be
345
+ * skipped, else null. Both `archiveFinishedOnMain` and `fileBugsOnMain` (backlog-file-bugs.ts)
346
+ * guard on this before committing a backlog file. */
347
+ export async function primaryOffBaseBranchReason(root) {
348
+ const primary = await currentBranch(root);
349
+ const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
350
+ if (primary !== base) {
351
+ return `your checkout is on ${primary ?? "a detached HEAD"}, not ${base}`;
352
+ }
353
+ return null;
354
+ }
319
355
  /** The bulk archive, committed: move every live finished section of the primary checkout at
320
356
  * `root` (past its newest `keep` entries) to its archive file and commit the touched files on the
321
357
  * base branch as one `tumwater(operator):` commit. Holds the team's merge lock, so a merge
@@ -332,11 +368,9 @@ export async function archiveFinishedOnMain(root, opts = {}) {
332
368
  if (pre.length === 0)
333
369
  return { status: "nothing" };
334
370
  return withLock(mergeLockDir(root), () => withPrimaryWorktreeLockAsync(root, async () => {
335
- const primary = await currentBranch(root);
336
- const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
337
- if (primary !== base) {
338
- return { status: "skipped", reason: `your checkout is on ${primary ?? "a detached HEAD"}, not ${base}` };
339
- }
371
+ const offBase = await primaryOffBaseBranchReason(root);
372
+ if (offBase !== null)
373
+ return { status: "skipped", reason: offBase };
340
374
  const files = FINISHED_ARCHIVES.flatMap((a) => [a.file, a.archive]);
341
375
  const status = await gitTry(root, "status", "--porcelain", "--", ...files);
342
376
  if (status === null)
@@ -402,121 +436,3 @@ export async function archiveAtTeamStart(root) {
402
436
  warnEventBestEffort(root, TUMWATER_SOURCE, `finished backlog entries were not archived: ${errorMessage(err)}`);
403
437
  }
404
438
  }
405
- /** The start of a quarantine entry's title for `test`, up to the date. appendBugEntryLocked
406
- * collapses whitespace in the title, so the test part is collapsed the same way; collapsing the
407
- * whole prefix would trim its trailing space. */
408
- function quarantineTitlePrefix(test) {
409
- return collapseWhitespace(`\`${test}\` failed, then passed on the same tree`) + " (quarantined by tumwater ";
410
- }
411
- /** Output fenced into a bug body: a fence of its own length cannot be closed by the quoted text. */
412
- function fencedTail(tail) {
413
- return ["~~~~", ...tail.replaceAll("~~~~", "~~~").split("\n").slice(-30), "~~~~"].join("\n");
414
- }
415
- /** File one BUGS.md `## Open` entry per given entry and commit BUGS.md on the base branch as one
416
- * `tumwater(operator):` commit (the subject built from the number of entries actually appended is
417
- * stamped under the operator role), under the merge lock and the primary-worktree lock exactly as
418
- * archiveFinishedOnMain does. Skipped,
419
- * writing nothing, when the primary checkout is off the base branch, BUGS.md has uncommitted
420
- * changes, or BUGS.md exists but cannot be read; a failed append or commit restores BUGS.md.
421
- * Every entry is appended verbatim as its own `## Open` heading — unless its `ifOpen` dedupe
422
- * matches an existing heading, which is reused instead — and the returned headings are the
423
- * entry headings in input order. */
424
- export async function fileBugsOnMain(root, entries, subject, opts = {}) {
425
- return withLock(mergeLockDir(root), () => withPrimaryWorktreeLockAsync(root, async () => {
426
- const primary = await currentBranch(root);
427
- const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
428
- if (primary !== base) {
429
- return { status: "skipped", reason: `your checkout is on ${primary ?? "a detached HEAD"}, not ${base}` };
430
- }
431
- const status = await gitTry(root, "status", "--porcelain", "--", "BUGS.md");
432
- if (status === null)
433
- return { status: "skipped", reason: "could not read git status" };
434
- if (status.trim() !== "" && !status.startsWith("??")) {
435
- return { status: "skipped", reason: "BUGS.md has uncommitted changes" };
436
- }
437
- const file = path.join(root, "BUGS.md");
438
- let before;
439
- try {
440
- before = readTextIfExists(file);
441
- }
442
- catch (err) {
443
- return { status: "skipped", reason: `BUGS.md could not be read: ${errorFirstLine(err)}` };
444
- }
445
- const open = before === null ? [] : parseEntryDetails(before, "Open").map((e) => e.title);
446
- const headings = [];
447
- let appended = 0;
448
- const restoreBugs = async () => {
449
- await gitTry(root, "reset", "-q", "--", "BUGS.md");
450
- if (before === null)
451
- fs.rmSync(file, { force: true });
452
- else
453
- writeTextAtomic(file, before);
454
- };
455
- try {
456
- for (const entry of entries) {
457
- if (entry.ifOpen !== undefined) {
458
- const existing = entry.ifOpen(open);
459
- if (existing !== undefined) {
460
- headings.push(existing);
461
- continue;
462
- }
463
- }
464
- headings.push(appendBugEntryLocked(root, entry.title, entry.body));
465
- appended++;
466
- }
467
- }
468
- catch (err) {
469
- await restoreBugs();
470
- return { status: "skipped", reason: `a BUGS.md entry could not be written: ${errorFirstLine(err)}` };
471
- }
472
- if (appended === 0)
473
- return { status: "filed", headings };
474
- const commitSubject = stampedSubject("operator", // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
475
- subject(appended));
476
- try {
477
- await git(root, "add", "--", "BUGS.md");
478
- await git(root, ...(await committerIdentArgs(root)), "commit", ...commitAuthorArgs(), "--only", "-m", commitSubject, "--", "BUGS.md");
479
- }
480
- catch (err) {
481
- await restoreBugs();
482
- return { status: "skipped", reason: `the commit failed: ${errorFirstLine(err)}` };
483
- }
484
- return { status: "filed", headings };
485
- }), opts.lockTimeoutMs);
486
- }
487
- /** File one BUGS.md `## Open` entry per newly quarantined test and commit BUGS.md on the base
488
- * branch as one `tumwater(operator):` commit, delegating the locked write/commit to
489
- * fileBugsOnMain and preserving the existing subject. A test whose quarantine entry is already
490
- * under `## Open` (a filing whose markQuarantineFiled failed after the commit) gets that heading
491
- * back and is not filed again — the dedupe runs inside fileBugsOnMain under the merge lock (via
492
- * each entry's `ifOpen`), so concurrent quarantine filings cannot both append a duplicate, and
493
- * the real skip on a dirty or off-base BUGS.md is enforced under the same lock. */
494
- export async function fileQuarantineBugsOnMain(root, tests, opts = {}) {
495
- const date = formatDate(new Date());
496
- const toFile = [];
497
- for (const { test, tail } of tests) {
498
- const prefix = quarantineTitlePrefix(test);
499
- const body = [
500
- `The check failed on this test and then passed on a re-run of the same tree, so tumwater quarantined it: it counts as a known failure until this entry leaves \`## Open\`. Find why it is unstable and fix it.`,
501
- ...(tail ? ["", fencedTail(tail)] : []),
502
- ].join("\n");
503
- toFile.push({
504
- title: `${prefix}${date})`,
505
- body,
506
- // Dedupe under the lock, against the headings fileBugsOnMain reads: the reused heading
507
- // (not a duplicate) is returned in headings and zipped back to this test below.
508
- ifOpen: (open) => open.find((t) => t.startsWith(prefix)),
509
- });
510
- }
511
- const outcome = await fileBugsOnMain(root, toFile,
512
- // Size the subject by what was actually appended: tests whose `## Open` entry was already
513
- // present are reused, not filed, so a mixed batch keeps the original "File N …" wording
514
- // with N the count of new entries only.
515
- (appended) => `File ${plural(appended, "quarantined flaky test", "quarantined flaky tests")} in BUGS.md`, opts);
516
- if (outcome.status === "skipped")
517
- return { status: "skipped", reason: outcome.reason };
518
- // fileBugsOnMain's headings align with `tests` in order (reused headings or appended titles).
519
- const bugs = new Map();
520
- outcome.headings.forEach((heading, i) => bugs.set(tests[i].test, heading));
521
- return { status: "filed", bugs };
522
- }
@@ -6,7 +6,7 @@ import { fail, say, sayJson } from "../cli/cli-output.js";
6
6
  import { parseCountFlag, rejectRepeatFlag, scanPositionArgs } from "../cli/cli-args.js";
7
7
  import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, donePlanEntries, fixedBugEntries, openBugEntries, plannedPlanEntries, ageInDays, entryAgeSuffix, entryMatches, filingDate, } from "./backlog.js";
8
8
  import { backlogHolds, backlogInProgress, holdMark, inProgressMark } from "./backlog-eligibility.js";
9
- import { groupBlockedSeries, partsLabel } from "./backlog-structure.js";
9
+ import { groupBlockedSeries, partsLabel } from "./backlog-index-data.js";
10
10
  import { rejectUnknownArgs } from "../cli/cli-flag-specs.js";
11
11
  import { refuseTeamWorkspace } from "../cli/cli-repo-gate.js";
12
12
  import { sectionNoun, showEntry } from "./backlog-write.js";
@@ -23,31 +23,12 @@ function positionRuns(positions) {
23
23
  }
24
24
  return runs.join(", ");
25
25
  }
26
- /** The shared shell of the `bug --list` and `plan --list` CLI cases — the read half of the
27
- * operator queue whose `--edit`/`--cancel` flags address entries by position. Reads the same
28
- * entries, in the same file order, that locateEntry numbers for `--edit`/`--cancel`, pairs
29
- * each with its eligibility hold (backlogHolds), and prints them numbered from 1, or the
30
- * command's `no …` line on an empty or missing file. Like `tumwater backlog` and `tumwater
31
- * questions` it is read-only: no requireReadyRepo gate and no wake. `--list` and `--json` are
32
- * the two booleans this shell consumes (repeats refused), `--help`/`-h` is tolerated, and any
33
- * positional or other flag is refused through rejectUnknownArgs.
34
- *
35
- * `--grep <text>` searches the active entries too: it keeps the entries whose heading or
36
- * body contains the text (case-insensitive), each under its unfiltered number so a printed
37
- * position still reaches `--show`/`--edit`/`--cancel` — the active twin of the finished
38
- * entries' `--done --grep`.
39
- *
40
- * `--ready` keeps the entries with no hold under their unfiltered numbers (composes with
41
- * `--grep` and `--json`; refused with `--done`, whose entries carry no holds).
42
- *
43
- * `--done [-n <count>] [--grep <text>]` (each once; `-n` and `--grep` need `--done`) lists the
44
- * newest finished entries instead (BUGS.md ## Fixed or PLANS.md ## Done, numbered, default 10)
45
- * with `older` counting those left out; --json carries each entry's `position` and
46
- * `completedOn` heading date. `--grep` keeps the entries whose heading or body contains the
47
- * text (case-insensitive); each keeps its unfiltered number, so it is the `--show <n> --done`
48
- * number, and `older` counts matches left out. `plan --list` prose
49
- * groups a series' blocked parts into one line (groupBlockedSeries). */
50
- export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
26
+ /** Scans the `--list` flag vocabulary (`--list`, `--json`, `--done`, `--ready`, `-n`, `--grep`,
27
+ * each once, plus tolerated `--help`/`-h`) into the parsed locals and returns them with `rest`,
28
+ * the leftover args rejectUnknownArgs will refuse. Reads no entry, filters nothing, prints
29
+ * nothing; the `fail` calls it makes are the flag-usage refusals (a repeat or a `--grep`/`-n`
30
+ * with missing text). */
31
+ function parseListArgs(args, command) {
51
32
  let json = false;
52
33
  let list = false;
53
34
  let done = false;
@@ -95,52 +76,48 @@ export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
95
76
  continue;
96
77
  rest.push(arg);
97
78
  }
98
- // rejectUnknownArgs names a stray positional and an unknown flag with this command's (empty)
99
- // valid-flag list: the vocabulary a list invocation accepts is exactly --list and --json,
100
- // both consumed above.
101
- rejectUnknownArgs(command, rest, []);
102
- refuseTeamWorkspace(root);
103
- if (count !== null && !done)
104
- fail("-n needs --done (it limits the finished entries listed)");
105
- // BUGS.md's `## Open` is the one section whose noun is "bug"; every other caller is a plan.
106
- if (ready && done)
107
- fail("--ready needs the active list (finished entries carry no holds)");
108
- const bugSection = sectionTitle === "Open";
109
- if (done) {
110
- const numbered = (bugSection ? fixedBugEntries(root) : donePlanEntries(root)).map((entry, i) => ({
111
- entry,
112
- position: i + 1,
113
- }));
114
- const pattern = grep;
115
- const matches = pattern === null ? numbered : numbered.filter((m) => entryMatches(m.entry, pattern));
116
- const limit = count ?? 10;
117
- const shown = matches.slice(0, limit);
118
- const older = matches.length - shown.length;
119
- if (json) {
120
- const dateRe = bugSection ? FIXED_DATE_RE : DONE_DATE_RE;
121
- sayJson({
122
- entries: shown.map(({ entry: e, position }) => ({
123
- position,
124
- title: e.title,
125
- body: e.body,
126
- completedOn: completionDate(e.title, dateRe),
127
- })),
128
- older,
129
- });
130
- return;
131
- }
132
- const what = bugSection ? "fixed bugs" : "done plans";
133
- if (shown.length === 0) {
134
- say(pattern === null ? `no ${what}` : `no ${what} match "${pattern}"`);
135
- return;
136
- }
137
- const lines = shown.map((m) => `${m.position}. ${m.entry.title}`);
138
- if (older > 0)
139
- lines.push(`(${older} older; -n ${matches.length} shows more)`);
140
- const header = pattern === null ? `${what} in ${fileName}, newest first:` : `${what} in ${fileName} matching "${pattern}", newest first:`;
141
- say(`${header}\n${lines.join("\n")}`);
79
+ return { json, done, ready, count, grep, rest };
80
+ }
81
+ /** The `--done` half of the list shell: the newest finished entries (BUGS.md ## Fixed or
82
+ * PLANS.md ## Done), filtered by `--grep`, limited by `-n`, printed as `--json` or prose. */
83
+ function announceFinishedList(root, fileName, bugSection, grep, count, json) {
84
+ const numbered = (bugSection ? fixedBugEntries(root) : donePlanEntries(root)).map((entry, i) => ({
85
+ entry,
86
+ position: i + 1,
87
+ }));
88
+ const pattern = grep;
89
+ const matches = pattern === null ? numbered : numbered.filter((m) => entryMatches(m.entry, pattern));
90
+ const limit = count ?? 10;
91
+ const shown = matches.slice(0, limit);
92
+ const older = matches.length - shown.length;
93
+ if (json) {
94
+ const dateRe = bugSection ? FIXED_DATE_RE : DONE_DATE_RE;
95
+ sayJson({
96
+ entries: shown.map(({ entry: e, position }) => ({
97
+ position,
98
+ title: e.title,
99
+ body: e.body,
100
+ completedOn: completionDate(e.title, dateRe),
101
+ })),
102
+ older,
103
+ });
104
+ return;
105
+ }
106
+ const what = bugSection ? "fixed bugs" : "done plans";
107
+ if (shown.length === 0) {
108
+ say(pattern === null ? `no ${what}` : `no ${what} match "${pattern}"`);
142
109
  return;
143
110
  }
111
+ const lines = shown.map((m) => `${m.position}. ${m.entry.title}`);
112
+ if (older > 0)
113
+ lines.push(`(${older} older; -n ${matches.length} shows more)`);
114
+ const header = pattern === null ? `${what} in ${fileName}, newest first:` : `${what} in ${fileName} matching "${pattern}", newest first:`;
115
+ say(`${header}\n${lines.join("\n")}`);
116
+ }
117
+ /** The active half of the list shell: the open entries with their holds, in-progress claim
118
+ * marks and age suffixes, optionally reduced to the ready ones, as `--json` or prose, grouping
119
+ * a plan's blocked parts into one line (groupBlockedSeries). */
120
+ function announceActiveList(root, fileName, command, bugSection, sectionTitle, grep, ready, json) {
144
121
  const entries = bugSection ? openBugEntries(root) : plannedPlanEntries(root);
145
122
  const holds = backlogHolds(root);
146
123
  const entryHolds = bugSection ? holds.bugs : holds.plans;
@@ -214,6 +191,48 @@ export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
214
191
  lines.push(hiddenNote);
215
192
  say(`${noun}s in ${fileName}:\n${lines.join("\n")}`);
216
193
  }
194
+ /** The shared shell of the `bug --list` and `plan --list` CLI cases — the read half of the
195
+ * operator queue whose `--edit`/`--cancel` flags address entries by position. Reads the same
196
+ * entries, in the same file order, that locateEntry numbers for `--edit`/`--cancel`, pairs
197
+ * each with its eligibility hold (backlogHolds), and prints them numbered from 1, or the
198
+ * command's `no …` line on an empty or missing file. Like `tumwater backlog` and `tumwater
199
+ * questions` it is read-only: no requireReadyRepo gate and no wake. `--list` and `--json` are
200
+ * the two booleans this shell consumes (repeats refused), `--help`/`-h` is tolerated, and any
201
+ * positional or other flag is refused through rejectUnknownArgs.
202
+ *
203
+ * `--grep <text>` searches the active entries too: it keeps the entries whose heading or
204
+ * body contains the text (case-insensitive), each under its unfiltered number so a printed
205
+ * position still reaches `--show`/`--edit`/`--cancel` — the active twin of the finished
206
+ * entries' `--done --grep`.
207
+ *
208
+ * `--ready` keeps the entries with no hold under their unfiltered numbers (composes with
209
+ * `--grep` and `--json`; refused with `--done`, whose entries carry no holds).
210
+ *
211
+ * `--done [-n <count>] [--grep <text>]` (each once; `-n` and `--grep` need `--done`) lists the
212
+ * newest finished entries instead (BUGS.md ## Fixed or PLANS.md ## Done, numbered, default 10)
213
+ * with `older` counting those left out; --json carries each entry's `position` and
214
+ * `completedOn` heading date. `--grep` keeps the entries whose heading or body contains the
215
+ * text (case-insensitive); each keeps its unfiltered number, so it is the `--show <n> --done`
216
+ * number, and `older` counts matches left out. `plan --list` prose
217
+ * groups a series' blocked parts into one line (groupBlockedSeries). */
218
+ export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
219
+ const { json, done, ready, count, grep, rest } = parseListArgs(args, command);
220
+ // rejectUnknownArgs names a stray positional and an unknown flag with this command's (empty)
221
+ // valid-flag list: the vocabulary a list invocation accepts is exactly --list and --json,
222
+ // both consumed above.
223
+ rejectUnknownArgs(command, rest, []);
224
+ refuseTeamWorkspace(root);
225
+ if (count !== null && !done)
226
+ fail("-n needs --done (it limits the finished entries listed)");
227
+ // BUGS.md's `## Open` is the one section whose noun is "bug"; every other caller is a plan.
228
+ if (ready && done)
229
+ fail("--ready needs the active list (finished entries carry no holds)");
230
+ const bugSection = sectionTitle === "Open";
231
+ if (done)
232
+ announceFinishedList(root, fileName, bugSection, grep, count, json);
233
+ else
234
+ announceActiveList(root, fileName, command, bugSection, sectionTitle, grep, ready, json);
235
+ }
217
236
  /** The shell of `bug --show` and `plan --show`: `--show <n>` (once), an optional `--json`
218
237
  * and `--done` (each once; `--done` shows the Nth newest finished entry, as `--list --done`
219
238
  * numbers them, with its heading stamp kept and `completedOn` in --json),