tumwater 0.2.0 → 0.4.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 (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -0,0 +1,398 @@
1
+ import { andMore, readDoc, readBacklogChecked, FIX_CLAIMS_CHECKED, FIX_CLAIM_HEADING_MAX, REFUSAL_NOTE_MAX } from "./doctor-backlog.js";
2
+ import { cachedByStat } from "../files/stat-cache.js";
3
+ import { VALIDATION_GAP_TAGS } from "../roles/role-guidance.js";
4
+ import path from "node:path";
5
+ import { truncate } from "../text/text.js";
6
+ import { donePlans, fixedBugEntries, openBugEntries, plannedPlanEntries } from "../backlog/backlog.js";
7
+ import { entryHold, entryKindLabel, heldCount } from "../backlog/backlog-eligibility.js";
8
+ import { planDependencyProblems } from "../backlog/backlog-parts.js";
9
+ import { BACKLOG_FILES, FINISHED_ARCHIVES, validationGapTag } from "../backlog/backlog-md.js";
10
+ import { statOrNull } from "../files/files.js";
11
+ import { duplicateHeadings, strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
12
+ import { liveFinishedHistory } from "../backlog/backlog-archive.js";
13
+ import { fixSymbols, missingSymbolNames, cachedSourceHaystack, unbackedSymbols } from "../verdict/fix-claim.js";
14
+ import { plural } from "../text/phrases.js";
15
+ import { okOutcome, warnOutcome } from "./outcome.js";
16
+ /** The doctor's PLANS.md-structure and fix-claim checks, split out of doctor-backlog.ts: the
17
+ * checks that re-verify BUGS.md's newest Fixed records against the tree (checkFixClaims,
18
+ * checkValidationGaps) and the PLANS.md / backlog-file integrity checks that read sections and
19
+ * clauses rather than a single document's entries (checkStrandedPlans, checkLiveFinished,
20
+ * checkBacklogHeadings, checkStalledEntries, checkPlanDependencies). They import the shared helpers
21
+ * (readDoc and its checked wrapper readBacklogChecked, andMore) from doctor-backlog.ts and the
22
+ * heading-trim bounds,
23
+ * and all of them warn instead of failing: damaged documentation is operator signal, not a
24
+ * broken environment. doctor.ts composes them beside the open-item and queue checks; the shared
25
+ * CheckOutcome shape is imported from outcome.ts. */
26
+ /** Fix claims — the standalone half of the merge gate's false-fix check (src/verdict/fix-claim.ts):
27
+ * that gate fires only when an md-only diff moves a BUGS.md entry to Fixed, so a phantom fix that
28
+ * reached main any other way (merged before the gate existed, or through a path it never sees) was
29
+ * visible only to a human reading raw history. This re-verifies the newest Fixed records against
30
+ * the tree at `root` — the primary checkout IS main's tree — with the gate's own parsing and
31
+ * existence rules. Deliberately looser than the gate: a record warns only when EVERY symbol its Fix
32
+ * paragraph names is absent (one live symbol passes — the phantom signature, not a half-stale
33
+ * narrative), and a record naming no symbols is skipped, since pure-documentation fixes are
34
+ * legitimate. A warn, never a fail (the checkFallbackModel precedent): a suspicious record is
35
+ * operator signal, not a broken environment. */
36
+ export function checkFixClaims(root) {
37
+ const doc = readBacklogChecked(root, "BUGS.md");
38
+ if (typeof doc !== "string")
39
+ return doc;
40
+ const problems = fixClaimProblems(root);
41
+ const [first, ...rest] = problems;
42
+ if (!first)
43
+ return okOutcome(`newest ${fixClaimRecords(root).length} Fixed record(s) name code that exists on this tree`);
44
+ // falseFixReason's message shape: the heading, then up to 3 missing names.
45
+ const { heading, missing } = first;
46
+ const names = missingSymbolNames(missing);
47
+ // Every other suspect is still named, shortened: a doctor check is one line.
48
+ const more = andMore(rest, (p) => `"${truncate(p.heading, FIX_CLAIM_HEADING_MAX)}"`, "record(s)");
49
+ return warnOutcome(`BUGS.md records "${heading}" as Fixed, but none of the symbols its Fix paragraph names ` +
50
+ `exist on this tree: ${names}${more} — land the fix or keep the bug Open / refresh a stale record`);
51
+ }
52
+ /** The fix-claim rows shared by the doctor's check and the always-on alert: each newest Fixed
53
+ * record classified through fixSymbols and unbackedSymbols — the gate's own parsing and
54
+ * existence rules — against ONE cachedSourceHaystack built for the whole scan, keeping a record
55
+ * only when EVERY symbol its Fix paragraph names is absent, and skipping a record that names no
56
+ * symbols (pure-documentation fixes are legitimate). */
57
+ function classifyFixClaims(records, root) {
58
+ const problems = [];
59
+ let haystack;
60
+ for (const { title, body } of records) {
61
+ const symbols = fixSymbols(body);
62
+ if (symbols.length === 0)
63
+ continue;
64
+ haystack ??= cachedSourceHaystack(root);
65
+ const missing = unbackedSymbols(root, symbols, haystack);
66
+ if (missing.length === symbols.length)
67
+ problems.push({ heading: title, missing });
68
+ }
69
+ return problems;
70
+ }
71
+ /** The ONE shared fix-claim scan: the newest Fixed records (bounded to FIX_CLAIMS_CHECKED so
72
+ * pre-convention history never warns forever) as fixedBugEntries reads them — the stat-cached
73
+ * section reader BUGS.md + BUGS-FIXED.md, bodies kept whole — classified through
74
+ * classifyFixClaims. Reading through this same reader (and not a one-off fixedEntriesAt walk) is
75
+ * what keeps the alert and the doctor on the identical problem set; there is only one scan. */
76
+ function fixClaimRecords(root) {
77
+ return fixedBugEntries(root).slice(0, FIX_CLAIMS_CHECKED);
78
+ }
79
+ /** The problems the ONE shared fix-claim scan found among the newest Fixed records at `root`
80
+ * (empty when BUGS.md is unreadable or every newest Fixed record names at least one live
81
+ * symbol). Feeds the always-on amber alert through the stat-cached fixedBugEntries reader and
82
+ * cachedSourceHaystack, so it stays in step with the doctor and adds no re-read to the 1 Hz TUI
83
+ * and status-poll path. */
84
+ export function fixClaimProblems(root) {
85
+ return classifyFixClaims(fixClaimRecords(root), root);
86
+ }
87
+ /** The validation-gap rows shared by the doctor's check and the always-on alert: each newest
88
+ * Fixed record classified against the closed `**Validation gap:**` vocabulary through
89
+ * validationGapTag, which is fence-aware — suspects (missing/unrecognized) first, then
90
+ * `unpinned`, so the first row is the one the doctor's message names and the alert's reason
91
+ * picks. */
92
+ function classifyValidationGaps(records) {
93
+ const suspects = [];
94
+ const unpinned = [];
95
+ for (const { title, body } of records) {
96
+ const tag = validationGapTag(body);
97
+ if (tag === null)
98
+ suspects.push({ title, kind: "missing" });
99
+ else if (!VALIDATION_GAP_TAGS.includes(tag))
100
+ suspects.push({ title, kind: "unrecognized", tag });
101
+ else if (tag === "unclear-invariant" && !body.toLowerCase().includes("docs/invariants.md"))
102
+ unpinned.push({ title, kind: "unpinned" });
103
+ }
104
+ return [...suspects, ...unpinned];
105
+ }
106
+ /** The ONE scan the doctor's `validation gaps` check and the always-on alert both hand their
107
+ * rows from: the newest Fixed records (bounded to FIX_CLAIMS_CHECKED so pre-convention history
108
+ * never warns forever) as fixedBugEntries reads them — the stat-cached section reader BUGS.md +
109
+ * BUGS-FIXED.md, bodies kept whole — classified through classifyValidationGaps. Reading both
110
+ * through this same reader (and not the doctor's one-off fixedEntriesAt/fixedSection walk) is
111
+ * what keeps the alert and the doctor on the identical problem set; there is only one scan. */
112
+ function validationGapRecords(root) {
113
+ return fixedBugEntries(root).slice(0, FIX_CLAIMS_CHECKED);
114
+ }
115
+ /** The problems the ONE shared validation-gap scan found among the newest Fixed records at `root`
116
+ * (empty when BUGS.md/BUGS-FIXED.md are unreadable or every newest Fixed record carries a known
117
+ * tag/trace). Feeds the always-on amber alert through the stat-cached fixedBugEntries reader, so
118
+ * it stays in step with the doctor and adds no re-read to the 1 Hz TUI and status-poll path. */
119
+ export function validationGapProblems(root) {
120
+ return classifyValidationGaps(validationGapRecords(root));
121
+ }
122
+ /** The fixed-record validation-gap twin of checkFixClaims, standing in for plans/repair-traces.md's
123
+ * invariant 2 ("the vocabulary is closed and lives in one place"): the bugfix guidance tells every
124
+ * run to write exactly one `**Validation gap:**` line with a known tag, and `none` rather than no
125
+ * line at all so a tally's denominator stays honest. Nothing checks that a Fixed entry actually
126
+ * carries such a line or that its tag is one of the closed vocabulary — a typo like `no-reprod`
127
+ * lands as its own key on `tumwater report`'s `Validation gaps` line and matches no
128
+ * closed-vocabulary consumer that enumerates the six non-`none` tags, and an entry with no line at
129
+ * all is invisible to every one of them. Re-reads the newest Fixed records with the same
130
+ * FIX_CLAIMS_CHECKED bound (pre-convention history would otherwise warn forever) and warns on a
131
+ * missing or unrecognized tag. An entry declaring `unclear-invariant` should also have pinned the
132
+ * reconstructed guarantee in docs/invariants.md in the same commit — a fix that names no such
133
+ * section warns with that nudge (informational: code-pinned or prose-that-skips-the-filename fixes
134
+ * may spuriously warn). A warn, never a fail, like its sibling: a suspect record is
135
+ * operator signal, not a broken environment. */
136
+ export function checkValidationGaps(root) {
137
+ // The doctor's check reads BUGS.md itself so it can return its no-file outcome (a missing
138
+ // BUGS.md — nothing to verify); the classification then runs on the SAME fixedBugEntries
139
+ // reader the always-on alert feeds from, so the two cannot drift on the problem set. There
140
+ // is only one scan (validationGapRecords).
141
+ const doc = readBacklogChecked(root, "BUGS.md");
142
+ if (typeof doc !== "string")
143
+ return doc;
144
+ const problems = classifyValidationGaps(validationGapRecords(root));
145
+ const suspects = problems.filter((p) => p.kind !== "unpinned");
146
+ const unpinned = problems.filter((p) => p.kind === "unpinned");
147
+ const [first, ...rest] = suspects;
148
+ if (!first) {
149
+ if (unpinned.length > 0) {
150
+ const up = unpinned[0];
151
+ const more = andMore(unpinned.slice(1), (s) => `"${truncate(s.title, FIX_CLAIM_HEADING_MAX)}"`, "record(s)");
152
+ return warnOutcome(`BUGS.md record "${up.title}" is an \`unclear-invariant\` fix but names no ` +
153
+ `docs/invariants.md section — record the reconstructed guarantee there (a "Pinned by" line) in the ` +
154
+ `same commit so the next bug in that area does not reconstruct it again${more}`);
155
+ }
156
+ return okOutcome(`newest ${validationGapRecords(root).length} Fixed record(s) carry a known Validation gap tag`);
157
+ }
158
+ const more = andMore(rest, (s) => `"${truncate(s.title, FIX_CLAIM_HEADING_MAX)}"`, "record(s)");
159
+ const what = first.kind === "missing"
160
+ ? `BUGS.md record "${first.title}" has no **Validation gap:** line`
161
+ : `BUGS.md record "${first.title}" names unrecognized Validation gap tag "${first.tag}"`;
162
+ const valid = VALIDATION_GAP_TAGS.join(", ");
163
+ return warnOutcome(`${what}${more} — every Fixed entry writes exactly one of: ${valid}; ` +
164
+ `write \`none\` rather than omitting the line (the bugfix role records it; see plans/repair-traces.md)`);
165
+ }
166
+ /** Stranded plans — plan headings filed under the wrong PLANS.md section (the stranded-plan
167
+ * detector, src/backlog/backlog-structure.ts): a `(planned …)` heading sitting under `## Done` is
168
+ * invisible to every Planned reader, and a done-dated heading still under `## Planned` invites
169
+ * a second implementation. The clean role repairs these when it happens to run; this makes
170
+ * the state visible to an operator without waiting for that run. Reads the tree at `root` —
171
+ * the primary checkout IS main's tree — like checkFixClaims. A warn, never a fail: a misplaced
172
+ * heading is operator signal, not a broken environment. */
173
+ export function checkStrandedPlans(root) {
174
+ const doc = readBacklogChecked(root, "PLANS.md");
175
+ if (typeof doc !== "string")
176
+ return doc;
177
+ // PLANS-DONE.md too: finished plans are archived there (plans/backlog-archive.md), and a plan
178
+ // filed under its ## Done is as invisible as one under PLANS.md's. Each entry names its file.
179
+ const stranded = strandedPlanEntriesFor(root);
180
+ const clean = "no plan headings filed under the wrong PLANS.md section";
181
+ if (stranded.length === 0)
182
+ return okOutcome(clean);
183
+ const [first, ...rest] = stranded;
184
+ if (!first)
185
+ return okOutcome(clean);
186
+ const more = andMore(rest, (e) => `"${truncate(e.title, FIX_CLAIM_HEADING_MAX)}"`);
187
+ const file = first.file ?? "PLANS.md";
188
+ return warnOutcome(`${file} has a plan stranded under ## ${first.section}: "${first.title}"${more} — ` +
189
+ `move it under ## ${first.section === "Done" ? "Planned" : "Done"}${file === "PLANS.md" ? "" : " in PLANS.md"} ` +
190
+ "(the clean role repairs these)");
191
+ }
192
+ /** Finished history still in the live backlog files (plans/backlog-archive.md): entries under
193
+ * PLANS.md's `## Done` or BUGS.md's `## Fixed` that have not moved to PLANS-DONE.md or
194
+ * BUGS-FIXED.md. Every run that reads the live file pays for them. A starting team archives them
195
+ * unless that is refused (a dirty file, a checkout off the base branch), and a hand commit can put
196
+ * new ones there between starts, so this names how many there are and the fix. A warn, never a
197
+ * fail: the team works either way, only more expensively. */
198
+ export function checkLiveFinished(root) {
199
+ const live = liveFinishedHistory(root);
200
+ if (live.length === 0)
201
+ return okOutcome("PLANS.md and BUGS.md hold no finished entries — they are archived");
202
+ const parts = live.map((l) => `${l.file} ## ${l.section} holds ${plural(l.entries, "finished entry", "finished entries")} (${Math.round(l.bytes / 1024)} KB)`);
203
+ return warnOutcome(`${parts.join("; ")} — every run that reads the file pays for them; ` +
204
+ "`tumwater archive` moves them to the archive files and commits them (a starting team does too)");
205
+ }
206
+ /** The always-on `backlog-headings` alert polls this on every observer surface and every
207
+ * observer tick, so the per-file read must be stat-cached: a steady-state poll pays one stat
208
+ * per file, not a full re-read of every BACKLOG_FILES member — the archives grow without bound
209
+ * (PLANS-DONE.md and BUGS-FIXED.md run to MBs), and an uncached read on every poll would copy
210
+ * them wholesale each time, unlike the stat-cached section readers the rest of the poll path
211
+ * uses. cachedByStat serves an unchanged file from cache (dev/ino/mtime/size) and re-reads on
212
+ * a change, so this shares the poll path's freshness contract. The one loop over BACKLOG_FILES
213
+ * that feeds both checkBacklogHeadings and the always-on `backlog-headings` team alert: absent
214
+ * files (an early repo) contribute nothing, an existing file that cannot be read adds
215
+ * `${file} (unreadable)` to `rows` (its duplicate headings, if any, are invisible to every
216
+ * reader), and each duplicate `## ` title joins both `duplicates` and, as
217
+ * `${file} "## ${title}"`, `rows` — which therefore keeps BACKLOG_FILES order, the order the
218
+ * doctor's `backlog headings` row lists its messages in. Two public functions share this so
219
+ * they cannot drift on the file set, the unreadable clause, or the headings. */
220
+ function scanBacklogHeadings(root) {
221
+ const duplicates = [];
222
+ const rows = [];
223
+ for (const file of BACKLOG_FILES) {
224
+ const p = path.join(root, file);
225
+ const scan = cachedByStat(backlogHeadingFileCache, p, p, () => {
226
+ const read = readDoc(p);
227
+ if ("error" in read)
228
+ return { status: "unreadable", headings: [] };
229
+ return { status: "ok", headings: duplicateHeadings(read.doc) };
230
+ }, (v) => ({ status: v.status, headings: [...v.headings] }));
231
+ if (scan === null)
232
+ continue; // Absent file (early repo): contributes nothing.
233
+ if (scan.status === "unreadable") {
234
+ rows.push(`${file} (unreadable)`);
235
+ continue;
236
+ }
237
+ for (const heading of scan.headings) {
238
+ duplicates.push({ file, heading });
239
+ rows.push(`${file} "## ${heading}"`);
240
+ }
241
+ }
242
+ return { duplicates, rows };
243
+ }
244
+ /** The module-level stat cache backing scanBacklogHeadings, shared by the doctor row and the
245
+ * always-on alert so the poll path never re-reads an unchanged backlog or archive file. */
246
+ const backlogHeadingFileCache = new Map();
247
+ /** The duplicated `## ` headings already on main, exported so the always-on `backlog-headings`
248
+ * team alert (src/ui/team-alerts.ts) renders the same set the doctor's `backlog headings` row
249
+ * warns about. */
250
+ export function duplicatedBacklogHeadings(root) {
251
+ return scanBacklogHeadings(root).duplicates;
252
+ }
253
+ /** The module-level stat cache backing liveFinishedEntriesFor, keyed by root, so a steady-state
254
+ * status poll pays four stat calls (two live files, two archives) instead of re-parsing the
255
+ * live files and the unbounded archives (PLANS-DONE.md runs to MBs) on every poll. Keyed by a
256
+ * combined stat signature rather than the shared cachedByStat because each row spans FOUR files
257
+ * (a live file, its finished entries, and the archive it empties into), so no single-file
258
+ * stat-keyed cache entry can serve it. */
259
+ const liveFinishedCache = new Map();
260
+ /** The finished-history rows already on main, exported so the always-on `backlog-archive` team
261
+ * alert (src/ui/team-alerts.ts) renders the same set the doctor's `backlog archive` row warns
262
+ * about — stat-gated so a steady-state poll pays four stats, not a full re-parse. The cache key
263
+ * is the stat identity (`dev:ino:mtimeMs:size`, `absent` when the stat is null) of every
264
+ * FINISHED_ARCHIVES live file and archive under `root`, so any write that changes finished
265
+ * history or the archive refreshes the rows on the next call without a process restart. Returns
266
+ * a copy on a hit; on a miss it recomputes via liveFinishedHistory, and returns `[]` (uncached)
267
+ * when that throws, so an unreadable file stays checkLiveFinished's loud signal and never
268
+ * breaks a status poll. checkLiveFinished keeps calling liveFinishedHistory directly and is
269
+ * unchanged. */
270
+ export function liveFinishedEntriesFor(root) {
271
+ const key = FINISHED_ARCHIVES
272
+ .flatMap((spec) => [path.join(root, spec.file), path.join(root, spec.archive)])
273
+ .map((p) => {
274
+ const st = statOrNull(p);
275
+ return st ? `${st.dev}:${st.ino}:${st.mtimeMs}:${st.size}` : "absent";
276
+ })
277
+ .join("|");
278
+ const cached = liveFinishedCache.get(root);
279
+ if (cached?.key === key)
280
+ return cached.rows.map((r) => ({ ...r }));
281
+ let rows;
282
+ try {
283
+ rows = liveFinishedHistory(root);
284
+ }
285
+ catch {
286
+ liveFinishedCache.delete(root); // Never serve a stale read for an unreadable tree.
287
+ return []; // Unreadable — checkLiveFinished's signal; a status poll must not throw.
288
+ }
289
+ if (liveFinishedCache.size >= 64)
290
+ liveFinishedCache.clear(); // Safety cap, like backlogHeadingFileCache.
291
+ liveFinishedCache.set(root, { key, rows });
292
+ return rows.map((r) => ({ ...r }));
293
+ }
294
+ /** Duplicated `## ` headings already on main (the duplicate-heading half of the backlog-structure
295
+ * check, src/backlog/backlog-structure.ts): a `## Done` a past merge or conflict resolution added
296
+ * twice leaves every later entry after the first section invisible to the section readers, and
297
+ * nothing repairs it on its own. Reads the tree at `root` — the primary checkout IS main's tree —
298
+ * like checkFixClaims. A warn, never a fail: existing damage is operator signal, not a broken
299
+ * environment, and a merge that adds another copy of the heading trips the gate's same rule (a) —
300
+ * src/backlog/backlog-structure.ts fires only when a count EXCEEDS the merge-base's, so an edit
301
+ * that leaves an existing duplicate alone passes and the duplicate itself needs a deliberate
302
+ * removal edit. */
303
+ export function checkBacklogHeadings(root) {
304
+ const { rows } = scanBacklogHeadings(root);
305
+ if (rows.length === 0)
306
+ return okOutcome("no duplicated ## section headings in the backlog files");
307
+ const [first, ...rest] = rows;
308
+ const more = andMore(rest, (d) => d);
309
+ return warnOutcome(`${first}${more} appears more than once — readers take the first section of a name, so ` +
310
+ `later entries are invisible; keep one heading per name (the gate blocks only a merge ` +
311
+ `that adds another copy — remove the duplicate in a deliberate edit)`);
312
+ }
313
+ /** Stalled entries — plans or bugs held by a **Needs review …** or **Needs replan …** note,
314
+ * parked until the plan role splits or replans them, plus the held-for-operator entries
315
+ * parked until the operator releases them. No role takes a held entry (the claim gate refuses
316
+ * one) and nothing clears the note on its own, so the state is visible only as the
317
+ * `[needs review]`/`[needs replan]` marks in the backlog listings; this names them so an
318
+ * operator gets a single-line health signal from `tumwater doctor`. Reads the tree at `root`
319
+ * — the primary checkout IS main's tree — like checkRefusedEntries, which is also where
320
+ * refusal notes (a different hold) are surfaced. entryHold (src/backlog/backlog-eligibility.ts)
321
+ * already classifies the notes, so this check adds no parsing: only the entries whose hold is
322
+ * exactly needs-review, needs-replan, or held-for-operator are collected, from both PLANS.md
323
+ * and BUGS.md. A warn, never a fail — the checkRefusedEntries precedent: a parked entry is
324
+ * operator signal, not a broken environment; the plan role resolves the first two itself, and
325
+ * a held-for-operator entry is the operator's to release (`tumwater plan|bug --release`). */
326
+ export function checkStalledEntries(root) {
327
+ const planned = plannedPlanEntries(root);
328
+ const collect = (entry, file) => {
329
+ const hold = entryHold(entry, planned);
330
+ return hold === "needs-review" || hold === "needs-replan" || hold === "held-for-operator" // vocabulary-ok: the held-for-operator hold's key
331
+ ? { file, entry }
332
+ : null;
333
+ };
334
+ const stalled = [
335
+ ...planned.map((e) => collect(e, "plans")),
336
+ ...openBugEntries(root).map((e) => collect(e, "bugs")),
337
+ ].filter((x) => x !== null);
338
+ const [first, ...rest] = stalled;
339
+ if (!first)
340
+ return okOutcome("no planned plan or open bug is held by a Needs-review, Needs-replan, or held-for-operator note"); // vocabulary-ok: names the held-for-operator hold
341
+ const more = andMore(rest, (s) => `"${truncate(s.entry.title, FIX_CLAIM_HEADING_MAX)}"`);
342
+ const kind = entryKindLabel(first.file); // "Bug" | "Plan"
343
+ const head = `${first.file === "bugs" ? "BUGS.md" : "PLANS.md"} ${kind.toLowerCase()}`;
344
+ const hold = entryHold(first.entry, planned);
345
+ const note = (first.entry.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ") || line.trimStart().startsWith("**Needs replan ")) ?? "").replace(/^\s*\*\*/, "").replace("**", "");
346
+ if (hold === "held-for-operator") { // vocabulary-ok: the held-for-operator hold's key
347
+ return warnOutcome(`${head} "${truncate(first.entry.title, FIX_CLAIM_HEADING_MAX)}" is held for the operator ${heldCount(first.entry.body)} times${more} — release it with tumwater ${kind.toLowerCase()} --release`); // vocabulary-ok: a held-for-operator entry is the operator's to release
348
+ }
349
+ return warnOutcome(`${head} "${truncate(first.entry.title, FIX_CLAIM_HEADING_MAX)}" is held by a ${hold === "needs-review" ? "Needs review" : "Needs replan"} note ` +
350
+ `(${truncate(note, REFUSAL_NOTE_MAX)})${more} — the plan role resolves it by splitting or replanning`);
351
+ }
352
+ /** Plan dependencies — mistakes in the planned plans' `requires … landed` clauses that the claim
353
+ * allocator (entryHoldAgainst) cannot see: a ref naming no heading (the hold is void, so the
354
+ * dependent is claimable now), a clause that parses to no refs (it holds nothing), and a
355
+ * dependency cycle (every part on it stays blocked forever). Reads the tree at `root` through
356
+ * planDependencyProblems. A warn, never a fail, like checkStalledEntries: the plan role writes the
357
+ * clauses by hand and fixes them itself. The most serious kind is named — cycle, then dangling,
358
+ * then unparsed — with a count of the other kinds. */
359
+ export function planDependencyProblem(root) {
360
+ const { cycle, dangling, unparsed, misworded } = planDependencyProblems(plannedPlanEntries(root), donePlans(root));
361
+ const [firstDangling, ...restDangling] = dangling;
362
+ const [firstUnparsed, ...restUnparsed] = unparsed;
363
+ const [firstMisworded, ...restMisworded] = misworded;
364
+ const quote = (t) => `"${truncate(t, FIX_CLAIM_HEADING_MAX)}"`;
365
+ const others = (parts) => parts.some(([n]) => n > 0) ? ` (also ${parts.map(([n, noun]) => `${n} ${noun}`).join(", ")})` : "";
366
+ const danglingMsg = (d) => `PLANS.md plan ${quote(d.title)} requires ${d.ref}, which no PLANS.md heading carries — the hold is void, ` +
367
+ "so the plan is claimable now; fix the series name or part number";
368
+ const unparsedMsg = (t) => `PLANS.md plan ${quote(t)} has a \`requires … landed\` clause that names no part the allocator can read — ` + // vocabulary-ok: PLANS.md heading syntax
369
+ "it holds nothing; write each ref as `<Series> i/n` or give the heading a `<Series>, part i/n:` prefix";
370
+ const miswordedMsg = (w) => `PLANS.md plan ${quote(w.title)} names ${w.ref} as a prerequisite without a \`requires … landed\` clause — ` + // vocabulary-ok: PLANS.md heading syntax
371
+ "the claim allocator ignores that wording and hands the part out now; write it as `requires part i/n landed`"; // vocabulary-ok: PLANS.md heading syntax
372
+ if (cycle !== null) {
373
+ return { kind: "cycle", message: `plan dependency cycle: ${cycle.join(" → ")} — every part on it stays blocked forever; ` +
374
+ "edit one heading's `requires … landed` clause to break it" + // vocabulary-ok: PLANS.md heading syntax
375
+ others([[dangling.length, "dangling ref(s)"], [unparsed.length, "unparsed clause(s)"], [misworded.length, "misworded dependency(ies)"]]) };
376
+ }
377
+ if (firstDangling) {
378
+ return { kind: "dangling", message: danglingMsg(firstDangling) +
379
+ andMore(restDangling, (d) => `${quote(d.title)} → ${d.ref}`, "ref(s)") +
380
+ others([[unparsed.length, "unparsed clause(s)"], [misworded.length, "misworded dependency(ies)"]]) };
381
+ }
382
+ if (firstUnparsed) {
383
+ return { kind: "unparsed", message: unparsedMsg(firstUnparsed) + andMore(restUnparsed, quote, "clause(s)") +
384
+ others([[misworded.length, "misworded dependency(ies)"]]) };
385
+ }
386
+ if (firstMisworded) {
387
+ return { kind: "misworded", message: miswordedMsg(firstMisworded) +
388
+ andMore(restMisworded, (w) => `${quote(w.title)} → ${w.ref}`, "dependency(ies)") };
389
+ }
390
+ return null;
391
+ }
392
+ /** The doctor's `plan dependencies` row: warn on the first problem planDependencyProblem finds
393
+ * (sharing its problem set, severity order, and wording), ok when all four rules pass. A warn,
394
+ * never a fail: a broken clause is operator signal, not a broken environment. */
395
+ export function checkPlanDependencies(root) {
396
+ const p = planDependencyProblem(root);
397
+ return p ? warnOutcome(p.message) : okOutcome("every planned plan's requires clause names a known part, with no cycle or misworded dependency");
398
+ }