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,119 @@
1
+ /** The pre-queue self-check's acceptance-grep runner: a finished plan entry that says a `grep …`
2
+ * "finds nothing" is checked by running that grep in the worktree, so a change is not rejected
3
+ * for an acceptance grep that still matches. Only the strict "matches nothing" form runs;
4
+ * criteria worded "prints only …" need judgement and stay out. Staging-only (stage-check.ts). */
5
+ import { execFileAsync } from "../process/process.js";
6
+ import { moreSuffix } from "../text/phrases.js";
7
+ import { collapseWhitespace, truncate } from "../text/text.js";
8
+ const MAX_CRITERIA = 5;
9
+ const GREP_TIMEOUT_MS = 20_000;
10
+ const EMPTY_PHRASE_RE = new RegExp("^\\s*(?:(?:finds|prints|returns|outputs|matches|lists) nothing|is empty|lists none|" +
11
+ "(?:prints|finds|lists|returns|gives|shows) no (?:lines?|hits?|matches?|output))", "i");
12
+ const ALLOWANCE_RE = /apart from|except|excluding|other than|besides|beyond/i;
13
+ /** The backticked `grep …`/`git grep …` commands in `body` that the text says match nothing
14
+ * ("finds nothing", "is empty", …), without duplicates, in body order, at most five. A criterion
15
+ * whose sentence (up to the next `.` or `;`) allows some hits ("apart from", "except", …) is
16
+ * dropped. */
17
+ export function emptyGrepCriteria(body) {
18
+ const text = collapseWhitespace(body);
19
+ const found = [];
20
+ for (const m of text.matchAll(/`([^`]+)`/g)) {
21
+ const cmd = m[1] ?? "";
22
+ if (!/^(?:git )?grep /.test(cmd))
23
+ continue;
24
+ const rest = text.slice((m.index ?? 0) + m[0].length);
25
+ const phrase = EMPTY_PHRASE_RE.exec(rest);
26
+ if (phrase === null)
27
+ continue;
28
+ const sentence = rest.slice(0, rest.search(/[.;]/) === -1 ? undefined : rest.search(/[.;]/));
29
+ if (ALLOWANCE_RE.test(sentence))
30
+ continue;
31
+ if (!found.includes(cmd))
32
+ found.push(cmd);
33
+ if (found.length >= MAX_CRITERIA)
34
+ break;
35
+ }
36
+ return found;
37
+ }
38
+ /** A minimal shell-word split of a `grep …`/`git grep …` command, or undefined when it uses
39
+ * shell syntax (an unquoted `|;&<>$()` or backtick) or leaves a quote open. Nothing runs through
40
+ * a shell, so `*` stays literal. */
41
+ export function grepArgv(cmd) {
42
+ const words = [];
43
+ let cur = "";
44
+ let inWord = false;
45
+ let quote = null;
46
+ for (let i = 0; i < cmd.length; i++) {
47
+ const c = cmd[i];
48
+ if (quote === "'") {
49
+ if (c === "'")
50
+ quote = null;
51
+ else
52
+ cur += c;
53
+ }
54
+ else if (quote === '"') {
55
+ const next = cmd[i + 1];
56
+ if (c === "\\" && (next === '"' || next === "\\")) {
57
+ cur += next;
58
+ i++;
59
+ }
60
+ else if (c === '"')
61
+ quote = null;
62
+ else
63
+ cur += c;
64
+ }
65
+ else if (c === "'" || c === '"') {
66
+ quote = c;
67
+ inWord = true;
68
+ }
69
+ else if (c === "\\") {
70
+ if (i + 1 < cmd.length)
71
+ cur += cmd[++i];
72
+ inWord = true;
73
+ }
74
+ else if (/[|;&<>$()`]/.test(c)) {
75
+ return undefined;
76
+ }
77
+ else if (/\s/.test(c)) {
78
+ if (inWord)
79
+ words.push(cur);
80
+ cur = "";
81
+ inWord = false;
82
+ }
83
+ else {
84
+ cur += c;
85
+ inWord = true;
86
+ }
87
+ }
88
+ if (quote !== null)
89
+ return undefined;
90
+ if (inWord)
91
+ words.push(cur);
92
+ if (words[0] === "git" && words[1] === "grep")
93
+ return { file: "git", args: words.slice(1) };
94
+ if (words[0] === "grep")
95
+ return { file: "grep", args: words.slice(1) };
96
+ return undefined;
97
+ }
98
+ /** Run each entry's matches-nothing greps in `wt`; one finding per entry, for its first grep that
99
+ * still prints lines. An exit of 1 passes; any other failure (exit 2, timeout, spawn error) skips
100
+ * the criterion, since an unrunnable command is not evidence. */
101
+ export async function acceptanceGrepFindings(wt, entries) {
102
+ const findings = [];
103
+ for (const entry of entries) {
104
+ for (const cmd of emptyGrepCriteria(entry.body)) {
105
+ const argv = grepArgv(cmd);
106
+ if (argv === undefined)
107
+ continue;
108
+ const out = await execFileAsync(argv.file, argv.args, { cwd: wt, timeout: GREP_TIMEOUT_MS }).then((r) => r.stdout, () => "");
109
+ const lines = out.split("\n").filter((l) => l !== "");
110
+ if (lines.length === 0)
111
+ continue;
112
+ const shown = lines.slice(0, 5).map((l) => truncate(l, 200)).join("; ");
113
+ findings.push(`PLANS.md's finished entry "${truncate(entry.title, 160)}" says \`${cmd}\` matches nothing, but it prints ` +
114
+ `${lines.length} line(s): ${shown}${moreSuffix(lines.length - 5)} — fix those lines, or correct the claim if the criterion is wrong`);
115
+ break;
116
+ }
117
+ }
118
+ return findings;
119
+ }
@@ -0,0 +1,121 @@
1
+ /** The `ci` observer's CI-parity ledger (plans, part 3/3). Every `ci` run starts a fresh pi
2
+ * session, so the only durable memory of which main tip and `package-lock.json` each run
3
+ * covered is a runtime file the harness — not pi — maintains. The role runs the workflow's own
4
+ * full suite and e2e, which is minutes of work, so its nothing-to-do decision must be cheap:
5
+ * the parity block tells the fresh session what the last run covered, and when main has not
6
+ * moved (and the lock is unchanged) since a green run, the role declares nothing-to-do in one
7
+ * model run instead of re-running the whole suite. The ledger is gitignored runtime state
8
+ * under .tumwater/state/ and degrades to "no data" on any missing or malformed file: an
9
+ * observer must never fail a run on bookkeeping (the same policy as qa-coverage.ts). A stale
10
+ * verdict that stays stale past `CI_PARITY_STALE_MS` is a stall — the full suite has silently
11
+ * stopped covering new tips — surfaced as a doctor warn and a standing `ci-stale` alert through
12
+ * the one `ciParityStalled` predicate. */
13
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
14
+ import { ciParityPath } from "../paths.js";
15
+ import { isJsonObject } from "../files/json-object.js";
16
+ import { currentBranchFromHeadFile, readBranchHead, targetBranch } from "../git/git.js";
17
+ import { DAY_MS } from "../text/datetime.js";
18
+ import * as crypto from "node:crypto";
19
+ import * as fs from "node:fs";
20
+ import path from "node:path";
21
+ /** A stale verdict that stays stale past this window is a stall: the `ci` role — now the
22
+ * full suite's own runner — has silently stopped covering new tips (disabled, paused, wedged,
23
+ * or failing before it records). The doctor `ci parity` warn and the standing `ci-stale` alert
24
+ * share this one threshold, so the surfaces cannot disagree on when stale is a problem. */
25
+ export const CI_PARITY_STALE_MS = 3 * DAY_MS;
26
+ /** True only when a parity record exists, reads `stale` (a `failed` record already warns and
27
+ * a `current` one is verified for the tip), and has not covered a tip within the stall
28
+ * window. Parameter is structural — not `CiParityStatus` — so the status-polls → ci-parity
29
+ * import edge stays one-way. This is the single stall predicate shared by the doctor row and
30
+ * the alert. */
31
+ export function ciParityStalled(parity, now) {
32
+ return !!parity && parity.state === "stale" && now - parity.lastRunAt > CI_PARITY_STALE_MS;
33
+ }
34
+ /** True when `value` is a well-formed parity record. A record missing any field, or carrying
35
+ * a non-`passed|failed` result, reads as no data — the caller renders no block and the run
36
+ * proceeds as before. */
37
+ function isParityEntry(value) {
38
+ if (!isJsonObject(value))
39
+ return false;
40
+ return (typeof value.tip === "string" &&
41
+ typeof value.lockHash === "string" &&
42
+ (value.result === "passed" || value.result === "failed") &&
43
+ typeof value.lastRunAt === "number");
44
+ }
45
+ /** Read the parity ledger. A missing, torn, or wrong-shaped file reads as null — the caller
46
+ * renders no block and the run proceeds as before. */
47
+ export function readCiParity(root) {
48
+ const file = readJsonFile(ciParityPath(root));
49
+ return file?.parity && isParityEntry(file.parity) ? file.parity : null;
50
+ }
51
+ /** Record what a `ci` run covered: the tip it named on its `CI:` line and the verdict that
52
+ * line carried, plus the lock hash of the coverage tree computed here. The single record is
53
+ * written atomically (writeJsonAtomic) so a concurrent reader never sees a torn file; a
54
+ * concurrent last-writer simply wins the slot. */
55
+ export function recordParity(root, sha, result, now = Date.now()) {
56
+ writeJsonAtomic(ciParityPath(root), { parity: { tip: sha, lockHash: lockHashOf(root), result, lastRunAt: now } });
57
+ }
58
+ /** The hash of `package-lock.json` at `root` itself — what the current tree carries. An
59
+ * empty string when the file is missing (or unreadable) reads as an unmatched hash, so a
60
+ * swing to "no lock file" renders stale rather than pretending nothing moved. */
61
+ export function lockHashOf(root) {
62
+ let file;
63
+ try {
64
+ file = fs.readFileSync(path.join(root, "package-lock.json"), "utf8");
65
+ }
66
+ catch {
67
+ return "";
68
+ }
69
+ return crypto.createHash("sha256").update(file).digest("hex");
70
+ }
71
+ /** The current main tip and lock hash, resolved synchronously from the ref files and the
72
+ * working tree (mirroring status-polls.ts's per-poll read: readBranchHead + targetBranch, no
73
+ * spawn). Either degrades to "" when it cannot be resolved, which the render treats as
74
+ * "has definitely not matched", so a resolution failure can never declare a false current. */
75
+ export function currentCiParityState(root, config) {
76
+ const branch = targetBranch(config?.baseBranch, currentBranchFromHeadFile(root));
77
+ return { tip: readBranchHead(root, branch) ?? "", lockHash: lockHashOf(root) };
78
+ }
79
+ /** True when `entry` counts as "current": the last run passed AND it covered exactly this
80
+ * tip with exactly this lock hash. This is the single home of the "current" decision, so the
81
+ * ci prompt block (`renderParityBlock`) and the doctor `ci parity` row (`checkCiParity`) cannot
82
+ * disagree on what "current" means. `entry.tip` is whatever the run named on its `CI:` line — a
83
+ * 7-64-char abbreviated sha (the model's own shorthand) or the full 40-char head; match it as a
84
+ * prefix of the current tip so an abbreviated recorded tip still reads current once the full
85
+ * head shares that prefix. */
86
+ export function parityIsCurrent(entry, tip, lockHash) {
87
+ return entry.result === "passed" && tip !== "" && tip.startsWith(entry.tip) && entry.lockHash === lockHash;
88
+ }
89
+ /** Shorten an id for display without losing its discriminating prefix (a 40-char sha and a
90
+ * 64-char lock hash both collapse to a 7-char prefix the model can compare). */
91
+ function shortId(id) {
92
+ return id.length > 7 ? id.slice(0, 7) : id;
93
+ }
94
+ /** Render the parity block for the next `ci` prompt from the last record and the current
95
+ * main state. "current" only when the last run was green AND it covered exactly this tip with
96
+ * exactly this lock hash — the cheap nothing-to-do case. Every other combination — a failed
97
+ * last run, main moved, or the lock changed — renders "stale" naming both the covered and the
98
+ * current tip/hash, so the role runs the suite. */
99
+ export function renderParityBlock(entry, tip, lockHash) {
100
+ const lines = ["CI parity (from this team's own record):"];
101
+ if (parityIsCurrent(entry, tip, lockHash)) {
102
+ lines.push(` current — main is unmoved since the last green run covered tip ${shortId(entry.tip)} with lock ${shortId(entry.lockHash)}; declare nothing-to-do without running the suite.`);
103
+ }
104
+ else {
105
+ const covered = `${shortId(entry.tip)} @ ${shortId(entry.lockHash)} (${entry.result})`;
106
+ const now = tip === "" ? "could not be resolved" : `tip ${shortId(tip)} @ ${shortId(lockHash)}`;
107
+ lines.push(` stale — the last run covered ${covered}; main is now ${now}. Run the full suite and e2e before declaring anything.`);
108
+ }
109
+ return lines.join("\n");
110
+ }
111
+ /** The full parity block for a `ci` prompt, or undefined when there is no record yet — a
112
+ * missing or malformed ledger must render nothing rather than an empty verdict, so the very
113
+ * first run carries no block and simply runs the suite (the same omission policy as qa's
114
+ * coverage block). */
115
+ export function parityBlockFor(root, config) {
116
+ const record = readCiParity(root);
117
+ if (!record)
118
+ return undefined;
119
+ const { tip, lockHash } = currentCiParityState(root, config);
120
+ return renderParityBlock(record, tip, lockHash);
121
+ }
@@ -0,0 +1,51 @@
1
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
+ import { feedbackFilingsPath } from "../paths.js";
3
+ import { isJsonObject } from "../files/json-object.js";
4
+ import { formatAge } from "../text/datetime.js";
5
+ /** The `feedback` observer's per-install filing ledger (plans/feedback-role.md 2/2). Every
6
+ * `feedback` run starts a fresh pi session, so the only durable record of which GitHub issues
7
+ * this install already filed must live in harness state — the same cross-run memory qa's
8
+ * flow-coverage ledger provides (plans/observer-roles.md 2/2), and what makes the next run's
9
+ * prompt able to render a "previously filed" block so the charter's dedupe does not rely on the
10
+ * network alone. The ledger is gitignored runtime state under .tumwater/state/ and degrades to
11
+ * "no data" on any missing or malformed file: an observer must never fail a run on bookkeeping
12
+ * (a filing that fails to persist is a warning, never a run failure). */
13
+ /** The newest bound the ledger keeps. A fresh session files at most one issue per run, so this
14
+ * caps the block's growth and the prompt's size; older filings are dropped (the charter still
15
+ * dedupes against the live `gh issue list`). */
16
+ export const FEEDBACK_FILINGS_MAX = 100;
17
+ function isFiling(value) {
18
+ if (!isJsonObject(value))
19
+ return false;
20
+ return (typeof value.issue === "number" &&
21
+ typeof value.title === "string" &&
22
+ typeof value.filedAt === "number");
23
+ }
24
+ /** Read the ledger, keeping only well-formed entries. A missing, torn, or wrong-shaped file
25
+ * reads as an empty ledger — the caller renders no block and the run proceeds as before. */
26
+ export function readFeedbackFilings(root) {
27
+ const file = readJsonFile(feedbackFilingsPath(root));
28
+ const filings = file?.filings;
29
+ if (!Array.isArray(filings))
30
+ return [];
31
+ return filings.filter(isFiling);
32
+ }
33
+ /** Record that this install filed issue `issue` now (or at `now`, for tests). Reads-modifies-
34
+ * writes the whole ledger atomically so a concurrent writer cannot leave a torn file behind,
35
+ * and keeps only the newest FEEDBACK_FILINGS_MAX entries. */
36
+ export function recordFiled(root, issue, title, now = Date.now()) {
37
+ const filings = readFeedbackFilings(root);
38
+ const entry = { issue, title, filedAt: now };
39
+ writeJsonAtomic(feedbackFilingsPath(root), {
40
+ filings: [entry, ...filings].slice(0, FEEDBACK_FILINGS_MAX),
41
+ });
42
+ }
43
+ /** Render the "previously filed" block for the next `feedback` prompt; undefined when the
44
+ * ledger is empty (the common first-run case), so the prompt is unchanged then. Newest-first,
45
+ * each line `#<n> <title> (<age>)` mirroring qa's coverage lines. */
46
+ export function renderFilingsBlock(filings, now = Date.now()) {
47
+ if (filings.length === 0)
48
+ return undefined;
49
+ const lines = filings.map((f) => ` #${f.issue} ${f.title} (${formatAge(now - f.filedAt)})`);
50
+ return ["Previously filed from this install:", ...lines].join("\n");
51
+ }
@@ -0,0 +1,125 @@
1
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
+ import { qaCoveragePath } from "../paths.js";
3
+ import { isJsonObject, setOwn } from "../files/json-object.js";
4
+ import { DAY_MS, formatAge } from "../text/datetime.js";
5
+ /** The `qa` observer's flow-coverage ledger (plans/observer-roles.md 2/2). Every `qa` run
6
+ * starts a fresh pi session, so the only durable memory of which flow it last exercised is a
7
+ * runtime file the harness — not pi — maintains. A passing cheap check writes nothing into the
8
+ * repo (a note commit per check would move main and wake every sleeping role), so without this
9
+ * ledger the model picks a flow blind every run and converges on the top of its menu. The
10
+ * ledger is gitignored runtime state under .tumwater/state/ and degrades to "no data" on any
11
+ * missing or malformed file: an observer must never fail a run on bookkeeping. */
12
+ /** The flows `qa` exercises, cheapest-first — the ordered menu from plans/qa-role.md, plus the
13
+ * expensive `run (real)` variant. The names are the universe the coverage block renders; a flow
14
+ * recorded by name but absent here (a project-specific flow) still renders, appended after. */
15
+ export const QA_FLOWS = [
16
+ "init",
17
+ "status",
18
+ "logs",
19
+ "prompt",
20
+ "reset-counters",
21
+ "gui",
22
+ "tui",
23
+ "run",
24
+ "run (real)",
25
+ ];
26
+ /** A coverage ledger whose newest exercise is older than this window is a stall: the `qa`
27
+ * role — the team's only flow-level observer, the one role that exercises the product's own
28
+ * flows like a user — has silently stopped recording any exercise (disabled, paused, wedged,
29
+ * or failing before it records). The doctor `qa coverage` warn and the standing `qa-stale`
30
+ * alert share this one threshold, mirroring `ciParityStalled`'s `CI_PARITY_STALE_MS`. */
31
+ export const QA_COVERAGE_STALE_MS = 3 * DAY_MS;
32
+ /** True only when the ledger has at least one well-formed entry and every entry's `lastRunAt`
33
+ * is older than the stall window (`now - max(lastRunAt) > QA_COVERAGE_STALE_MS`). An empty
34
+ * ledger — a missing file, or a project that never ran qa — is not a stall: "no record yet" is
35
+ * not "stopped recording", the same rule `ciParityStalled` applies to a missing parity record.
36
+ * This is the one stall predicate the doctor row (`checkQaCoverage`) and the alert
37
+ * (`qaCoverageAlert`) share, so the surfaces cannot disagree on when a quiet qa is a problem. */
38
+ export function qaCoverageStalled(coverage, now) {
39
+ let newest = 0;
40
+ let has = false;
41
+ for (const entry of Object.values(coverage)) {
42
+ has = true;
43
+ if (entry.lastRunAt > newest)
44
+ newest = entry.lastRunAt;
45
+ }
46
+ return has && now - newest > QA_COVERAGE_STALE_MS;
47
+ }
48
+ function isResult(value) {
49
+ return value === "passed" || value === "bug";
50
+ }
51
+ /** Read the ledger, keeping only well-formed entries. A missing, torn, or wrong-shaped file
52
+ * reads as an empty ledger — the caller renders no block and the run proceeds as before. */
53
+ export function readQaCoverage(root) {
54
+ const file = readJsonFile(qaCoveragePath(root));
55
+ const flows = file?.flows;
56
+ if (!isJsonObject(flows))
57
+ return {};
58
+ const out = {};
59
+ for (const [name, raw] of Object.entries(flows)) {
60
+ if (!isJsonObject(raw))
61
+ continue;
62
+ const entry = raw;
63
+ if (typeof entry.lastRunAt !== "number" || !isResult(entry.result))
64
+ continue;
65
+ setOwn(out, name, {
66
+ lastRunAt: entry.lastRunAt,
67
+ result: entry.result,
68
+ ...(typeof entry.summary === "string" ? { summary: entry.summary } : {}),
69
+ });
70
+ }
71
+ return out;
72
+ }
73
+ /** Record that `flow` was exercised now (or at `now`, for tests). Reads-modifies-writes the
74
+ * whole ledger atomically so a concurrent writer cannot leave a torn file behind. */
75
+ export function recordFlow(root, flow, result, summary, now = Date.now()) {
76
+ const coverage = readQaCoverage(root);
77
+ setOwn(coverage, flow, { lastRunAt: now, result, ...(summary ? { summary } : {}) });
78
+ writeJsonAtomic(qaCoveragePath(root), { flows: coverage });
79
+ }
80
+ /** The coverage as ordered rows — the one universe (menu plus recorded project-specific flows)
81
+ * and one ordering (never-exercised first, then exercised stalest-first, ties in menu order)
82
+ * that the next `qa` prompt's block and the operator-facing views share, so they cannot drift.
83
+ * Never-exercised flows lead (age = ∞), then exercised flows oldest-first, so "exercise the
84
+ * flow at the top" actually rotates through the whole menu instead of re-picking whatever is
85
+ * cheapest. The flow universe is `QA_FLOWS` plus any recorded name (a project-specific flow
86
+ * still surfaces, appended after the menu). */
87
+ export function coverageRows(coverage, now = Date.now()) {
88
+ const names = [...QA_FLOWS];
89
+ for (const name of Object.keys(coverage)) {
90
+ if (!names.includes(name))
91
+ names.push(name);
92
+ }
93
+ return names
94
+ .map((name, index) => ({ name, index, entry: coverage[name] }))
95
+ .sort((a, b) => {
96
+ const ageA = a.entry ? now - a.entry.lastRunAt : Number.POSITIVE_INFINITY;
97
+ const ageB = b.entry ? now - b.entry.lastRunAt : Number.POSITIVE_INFINITY;
98
+ if (ageA !== ageB)
99
+ return ageB - ageA; // stalest first; never-exercised (∞) leads
100
+ return a.index - b.index; // stable: keep the menu order among equals
101
+ })
102
+ .map(({ name, entry }) => ({
103
+ flow: name,
104
+ lastRunAt: entry?.lastRunAt ?? null,
105
+ result: entry?.result ?? null,
106
+ ...(entry?.summary !== undefined ? { summary: entry.summary } : {}),
107
+ }));
108
+ }
109
+ /** Render the coverage block for the next `qa` prompt — the same rows `coverageRows` orders,
110
+ * one line each (a two-space-prefixed row, never-exercised flows first). */
111
+ export function renderCoverageBlock(coverage, now = Date.now()) {
112
+ const lines = coverageRows(coverage, now).map(({ flow, lastRunAt, result, summary }) => {
113
+ if (lastRunAt === null)
114
+ return ` ${flow} — never exercised`;
115
+ const age = formatAge(now - lastRunAt);
116
+ const status = result === "bug"
117
+ ? `bug filed (BUGS.md: "${summary ?? "see BUGS.md"}")`
118
+ : "passed";
119
+ return ` ${flow} — ${age} ago, ${status}`;
120
+ });
121
+ return [
122
+ "Flow coverage (from this team's own record; least recently exercised first):",
123
+ ...lines,
124
+ ].join("\n");
125
+ }