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,397 @@
1
+ /** The doctor's checks that inspect the project's own on-disk state, disk and build,
2
+ * split out of doctor-checks.ts (whose environment/repo file still holds the toolchain and
3
+ * setup checks — the node/git/exec binaries, the repo/init/config/brief/bootstrap setup, the
4
+ * agent backends' CLI versions and the operator pi-extension/funes coordination). This module
5
+ * holds the checks that look at the project itself: a writable .tumwater (checkStateDir), a
6
+ * readable event feed (checkEventLog), enough free disk (checkDiskSpace), a declared project
7
+ * check (checkBuildCheck), the review gate's flake memory (checkFlakyChecks) and build
8
+ * provenance (checkBuild). Read-only like every doctor check; the private helpers each cluster
9
+ * uses (sweepStaleProbes, eventLogHistoryPhrase, firstEventLine, diskReclaimPhrase,
10
+ * checkDurationPhrase, currentHead) live here beside the checks that use them. doctor.ts stays
11
+ * the composition layer that calls each check in fixed order. */
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { detectBuildCheck, mergeCommandOf } from "../build/build-check-detect.js";
15
+ import { isPassingFullCheck } from "../build/build-check-events.js";
16
+ import { buildStaleness, isSelfHosted, readBuildInfo, STALE_INPUTS_LABEL } from "../build/build-info.js";
17
+ import { dirEntryNames, statOrNull } from "../files/files.js";
18
+ import { refSha } from "../git/git.js";
19
+ import { STATE_DIR, eventsArchivePath, eventsLogPath } from "../paths.js";
20
+ import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gate.js";
21
+ import { DAY_MS, dayKey } from "../text/datetime.js";
22
+ import { errorMessage } from "../text/text.js";
23
+ import { lowerMedian } from "../collections.js";
24
+ import { formatGB, shortSha } from "../text/format.js";
25
+ import { eventLogState, readEventsSince } from "../events/event-window.js";
26
+ import { EVENTS_KEEP } from "../events/events.js";
27
+ import { finiteNumber } from "../files/json-object.js";
28
+ import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
29
+ import { KNOWN_FLAKE_WINDOW_MS, loadQuarantine, repeatedFlakeCounts } from "../review/known-flakes.js";
30
+ import { POST_MERGE_REJECT_WINDOW_MS, postMergeChurn } from "../review/post-merge-review.js";
31
+ import { roleLabel } from "../roles/role-labels.js";
32
+ import { quarantineWarning } from "../baseline/main-guardrails.js";
33
+ import { readCiParity, currentCiParityState, parityIsCurrent, ciParityStalled, CI_PARITY_STALE_MS } from "../run/ci-parity.js";
34
+ import { readQaCoverage, qaCoverageStalled, QA_COVERAGE_STALE_MS } from "../run/qa-coverage.js";
35
+ import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
36
+ /** .tumwater writable — absent is fine (created on first run); present, prove it by writing
37
+ * and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
38
+ * The write is the property under test: only a write that fails is "not writable", so the
39
+ * cleanup below — deleting the probe, sweeping stales — never vetoes a dir the write just
40
+ * proved writable, and a probe left over is swept rather than accumulated. */
41
+ export function checkStateDir(root) {
42
+ const dir = path.join(root, STATE_DIR);
43
+ if (!fs.existsSync(dir))
44
+ return okOutcome("absent — created on first run");
45
+ const probe = path.join(dir, `.doctor-probe-${process.pid}`);
46
+ try {
47
+ fs.writeFileSync(probe, "");
48
+ }
49
+ catch (err) {
50
+ // A partial write may already have created the file — sweep it like any other leftover.
51
+ sweepStaleProbes(dir);
52
+ return failOutcome(`not writable: ${errorMessage(err)}`);
53
+ }
54
+ // The write proved writability. Deleting the probe is cleanup, not measurement: a removal
55
+ // failure must not turn a succeeded write into a false alarm, and the stray is collected by
56
+ // the sweep below (or a later pass's sweep).
57
+ try {
58
+ fs.rmSync(probe);
59
+ }
60
+ catch {
61
+ // Best-effort — the stray is swept in a moment.
62
+ }
63
+ sweepStaleProbes(dir);
64
+ return okOutcome("writable");
65
+ }
66
+ /** Best-effort removal of every `.doctor-probe-*` file in `dir` — the only artifact
67
+ * checkStateDir creates, so any present probe is a leftover: a prior run killed between its
68
+ * write and delete, a partial write above, or a removal that failed. Removed one at a time so
69
+ * a single unremovable probe cannot stop the sweep. */
70
+ function sweepStaleProbes(dir) {
71
+ for (const name of dirEntryNames(dir)) {
72
+ if (!name.startsWith(".doctor-probe-"))
73
+ continue;
74
+ try {
75
+ fs.rmSync(path.join(dir, name));
76
+ }
77
+ catch {
78
+ // Best-effort — leave it for the next pass.
79
+ }
80
+ }
81
+ }
82
+ /** Event log readable — open and parse events.jsonl (and its rotated archive) so the one
83
+ * failure the selfcheck role is chartered to catch — a feed that hid itself — is named by the
84
+ * pre-flight too. An absent or empty log is fine: a young project writes its first event on the
85
+ * first run. A present-but-unparseable log (a directory at the path, a permission failure, or
86
+ * every complete line corrupt) fails, because every observer — selfcheck, the TUI/GUI feeds,
87
+ * `logs`, `report` — reads that one feed. Read-only. */
88
+ export function checkEventLog(root) {
89
+ let state;
90
+ try {
91
+ state = eventLogState(root);
92
+ }
93
+ catch (err) {
94
+ return failOutcome(`could not read ${eventsLogPath(root)}: ${errorMessage(err)}`);
95
+ }
96
+ if (state === "unreadable")
97
+ return failOutcome(`unreadable: ${eventsLogPath(root)} — the team's event feed cannot be read`);
98
+ if (state === "empty")
99
+ return okOutcome("no events yet");
100
+ const history = eventLogHistoryPhrase(root);
101
+ return okOutcome(history ? `readable: ${eventsLogPath(root)} — ${history}` : `readable: ${eventsLogPath(root)}`);
102
+ }
103
+ /** The `history from <date>, <n> files` phrase for checkEventLog's ok detail (G4): the first
104
+ * complete line of the oldest event-log generation present names how far back the kept history
105
+ * reaches, and the count is how many generations (live file plus archives) exist. Undefined
106
+ * when no generation parses an event, so a readable-but-shapeless log still reads as readable. */
107
+ function eventLogHistoryPhrase(root) {
108
+ const present = [];
109
+ const livePath = eventsLogPath(root);
110
+ if (statOrNull(livePath))
111
+ present.push(livePath);
112
+ for (let gen = 1; gen <= EVENTS_KEEP; gen++) {
113
+ const archive = eventsArchivePath(root, gen);
114
+ if (statOrNull(archive))
115
+ present.push(archive);
116
+ }
117
+ if (present.length === 0)
118
+ return undefined;
119
+ const oldest = present[present.length - 1];
120
+ const ev = firstEventLine(oldest);
121
+ if (!ev || typeof ev.ts !== "number")
122
+ return undefined;
123
+ const noun = plural(present.length, "file", "files");
124
+ return `history from ${dayKey(ev.ts)}, ${noun}`;
125
+ }
126
+ /** Parse the first complete line of an append-only JSONL `file`, or null when it is unreadable
127
+ * or does not parse. Reads only the first chunk — the event log is up to 16 MB a generation,
128
+ * and a doctor pre-flight should not slurp the oldest archive just to name its first day. */
129
+ function firstEventLine(file) {
130
+ try {
131
+ const fd = fs.openSync(file, "r");
132
+ try {
133
+ const buf = Buffer.alloc(4096);
134
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
135
+ const line = buf.toString("utf8", 0, n).split("\n")[0] ?? "";
136
+ if (!line)
137
+ return null;
138
+ try {
139
+ return JSON.parse(line);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ finally {
146
+ fs.closeSync(fd);
147
+ }
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ /** Disk floor (plans/disk-floor.md, parts 1/4 and 2/4) — is there room on the volume the
154
+ * worktrees live on? Fail below `diskHoldGB`, the same floor the running team holds new work
155
+ * at, so doctor and the gate agree on when the disk is a problem. Warn in the band between
156
+ * `diskHoldGB` and `diskReclaimGB`: pressure reclaim will delete ignored build outputs from
157
+ * idle worktrees but the team still runs. Ok at or above `diskReclaimGB` (or when reclaim is
158
+ * disabled and free space is above the hold). A statfs failure is a warning, not a failure: an
159
+ * unmeasurable volume never holds the team, so it must not fail a scripted pre-flight. The
160
+ * detail names the measured path, the free GB to one decimal, and the floor. `holdGB` 0
161
+ * disables the hold, so a free space above `reclaimGB`'s own band (or `reclaimGB` 0) passes.
162
+ * After a successful sample the detail also carries `diskReclaimPhrase` (what `disk_reclaim`
163
+ * passes freed over the last 24 h); it is informational and never changes the level. */
164
+ export function checkDiskSpace(root, holdGB, reclaimGB, sample = sampleFreeBytes) {
165
+ const where = diskVolumePath(root);
166
+ const free = sample(root);
167
+ if (free === null)
168
+ return warnOutcome(`cannot measure free space at ${where} (statfs failed) — the disk hold is off for this process`);
169
+ const freeGB = free / BYTES_PER_GB;
170
+ const freeText = `${formatGB(freeGB)} free at ${where}`;
171
+ const phrase = diskReclaimPhrase(readEventsSince(root, DAY_MS).events);
172
+ const withReclaim = (detail) => (phrase ? `${detail} · ${phrase}` : detail);
173
+ if (holdGB > 0 && freeGB < holdGB)
174
+ return failOutcome(withReclaim(`${freeText} — below the ${holdGB} GB diskHoldGB floor; the team waits to start new work until space recovers`));
175
+ if (reclaimGB > 0 && freeGB < reclaimGB)
176
+ return warnOutcome(withReclaim(`${freeText} — below the ${reclaimGB} GB diskReclaimGB threshold; a pressure reclaim pass deletes ignored build outputs from idle worktrees`));
177
+ const floor = holdGB > 0 ? `above the ${holdGB} GB floor` : "diskHoldGB is 0 (hold disabled)";
178
+ return okOutcome(withReclaim(`${freeText}, ${floor}`));
179
+ }
180
+ /** The `reclaim in the last 24h freed …` phrase for the `disk space` row: the summed
181
+ * `freedGB` of the `disk_reclaim` passes, their count, and how many ran under pressure (an
182
+ * event without a `mode` counts as pressure, like its feed line). Each pass is a point event,
183
+ * so summing cannot straddle the window edge. Undefined when no pass ran. */
184
+ export function diskReclaimPhrase(events) {
185
+ const passes = events.filter((e) => e.type === "disk_reclaim");
186
+ if (passes.length === 0)
187
+ return undefined;
188
+ const freed = passes.reduce((sum, e) => sum + finiteNumber(e.freedGB, 0), 0);
189
+ const pressure = passes.filter((e) => (typeof e.mode === "string" && e.mode ? e.mode : "pressure") === "pressure").length;
190
+ const base = `reclaim in the last 24h freed ${formatGB(freed)} over ${plural(passes.length, "pass", "passes")}`;
191
+ return pressure > 0 ? `${base}, ${pressure} under pressure` : base;
192
+ }
193
+ /** The `passing merge checks …` phrase for the `check` row: median and slowest wall clock of
194
+ * the passed full-suite runs (`merge`, `batch`, `baseline` scopes — a `gate` check can run a
195
+ * subset, and failed or skipped runs do not measure a full pass). The lower median keeps the
196
+ * figure a run that happened. Undefined when no event qualifies. */
197
+ export function checkDurationPhrase(events) {
198
+ const durations = events
199
+ .filter(isPassingFullCheck)
200
+ .map((e) => finiteNumber(e.durationMs, 0))
201
+ .filter((ms) => ms > 0);
202
+ if (durations.length === 0)
203
+ return undefined;
204
+ const median = lowerMedian(durations);
205
+ const slowest = Math.max(...durations);
206
+ return `passing merge checks in the last 24h took ${shortSpanPhrase(median)} median, ${shortSpanPhrase(slowest)} slowest (${plural(durations.length, "run")})`;
207
+ }
208
+ /** Declared project check — names what the review gate's deterministic pre-check, the
209
+ * red-main baseline, and redeploy's green check will run, without running it: a configured
210
+ * `check.command` (plans/portability.md §6/7), else the npm auto-detection. None declared is
211
+ * a warn, not informational (the 2026-09-05 stance predates non-npm targets being supported,
212
+ * when a warn would have been noise no operator could act on — with `check.command` available
213
+ * the warning is actionable): the three gates degrade to "no check" and a silently absent
214
+ * safety layer is the failure this check exists to surface. Warn does not fail the exit code
215
+ * (the fallback check's precedent, doctor-fallback-checks.ts's checkFallbackModel). */
216
+ export function checkBuildCheck(root, config) {
217
+ const check = detectBuildCheck(root, config ?? undefined);
218
+ if (!check)
219
+ return warnOutcome("none declared — the review gate's build pre-check, the red-main baseline, and redeploy's green check are all off (set `check.command` in tumwater.json for a non-npm repo)");
220
+ // The in-lock command from mergeCommandOf (absent mergeCommand falls back to the gate
221
+ // command): the row names which command the merge lock runs and where the full check lands,
222
+ // facts a reader cannot get from detectBuildCheck alone. The after-merge roles are not named
223
+ // here — they live under `review`, which this slice does not hold, and the `review` row in
224
+ // docs/config.md documents them.
225
+ const lockPhrase = (() => {
226
+ const mc = mergeCommandOf(config ?? undefined);
227
+ return mc !== undefined
228
+ ? `merge lock runs ${mc}, full check after merging`
229
+ : "full check in the merge lock";
230
+ })();
231
+ const phrase = checkDurationPhrase(readEventsSince(root, DAY_MS).events);
232
+ const withTiming = (detail) => okOutcome(`${detail} · ${lockPhrase}${phrase ? ` · ${phrase}` : ""}`);
233
+ if (check.kind === "command") {
234
+ const where = path.relative(root, check.cwd);
235
+ return withTiming(where ? `${check.command} (cwd ${where})` : check.command);
236
+ }
237
+ const where = path.relative(root, check.rootDir);
238
+ return withTiming(where ? `npm ${check.script} in ${where}` : `npm ${check.script}`);
239
+ }
240
+ /** The review gate's flake memory surfaced read-only: names which checks the gate kept excusing as
241
+ * flakes over its own KNOWN_FLAKE_WINDOW_MS window. The gate logs `gate check failed then passed on
242
+ * retry — flaky: <headline>` for each failure that passed on its one immediate re-run, and the
243
+ * in-lock merge check logs the same under MERGE_FLAKY_WARNING_PREFIX when its kept-pin retry passes
244
+ * (src/review/known-flakes.ts); that record is internal to the gate's matching, so a genuinely
245
+ * flaky check silently burns a re-run per merge and hides real instability — exactly the
246
+ * information that explains intermittent rejections. This check reads the same window and folds the
247
+ * same normalized headline the gate matches with (repeatedFlakeCounts over flakeCounts, so the
248
+ * prefix slice-offset and
249
+ * cluster normalization cannot drift), and warns only when a normalized check flapped more than
250
+ * once, so a routine one-off flake stays quiet. ok/warn only — never a fail — so a flaky suite
251
+ * cannot fail a scripted doctor pre-flight (the checkStalledEntries precedent; the exit-code
252
+ * rule is
253
+ * outcome.ts's). */
254
+ export function checkFlakyChecks(root, now = Date.now()) {
255
+ const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
256
+ const flapped = repeatedFlakeCounts(events);
257
+ const phrases = [];
258
+ const flappedPhrase = flappedChecksPhrase(flapped);
259
+ if (flappedPhrase !== null)
260
+ phrases.push(flappedPhrase);
261
+ const q = quarantineWarning(loadQuarantine(root), now);
262
+ if (q !== null)
263
+ phrases.push(`${q} — see \`tumwater quarantine\``);
264
+ if (phrases.length === 0)
265
+ return okOutcome("no check flapped more than once in the last 24h");
266
+ return warnOutcome(phrases.join("; "));
267
+ }
268
+ /** A trailing day of at least `POST_MERGE_REJECT_WARN` post-merge `review_rejected` events — the
269
+ * audit twin of the always-on `post-merge-churn` amber alert (src/ui/status-payload.ts
270
+ * `postMergeChurnAlert`). Each rejection is an already-landed commit the reviewer turned back, so
271
+ * it stays on main with the reviewer's reasons, so a churn day must not read as a clean
272
+ * main. Shares the alert's
273
+ * window, threshold and
274
+ * event selection through `postMergeChurn`, so the doctor row and the alert cannot disagree on
275
+ * when churn fires. ok/warn only — a churn burst does not stop the team. */
276
+ export function checkPostMergeChurn(root) {
277
+ const { events } = readEventsSince(root, POST_MERGE_REJECT_WINDOW_MS);
278
+ const churn = postMergeChurn(events);
279
+ if (churn === null)
280
+ return okOutcome("no post-merge rejection burst in the last day");
281
+ return warnOutcome(`${plural(churn.count, "post-merge review rejection")} in the last day (${churn.roles
282
+ .map((r) => `${roleLabel(r.role)} ×${r.count}`)
283
+ .join(", ")}) — each commit had already reached main and stays there with the reviewer's reasons; narrow \`review.afterMerge\` for the noisy role or raise the authoring bar`);
284
+ }
285
+ /** The one home of the doctor's `flaky checks` sentence: names each check that flapped more than
286
+ * once, most frequent first, with how many times it flapped — the same wording the always-on
287
+ * flapped-checks alert carries, so the doctor row and the alert cannot drift. null when `flapped`
288
+ * is empty (nothing crossed the bar). */
289
+ export function flappedChecksPhrase(flapped) {
290
+ if (flapped.length === 0)
291
+ return null;
292
+ const render = ([k, n]) => `"${k}" ×${n}`;
293
+ const [first, ...rest] = flapped;
294
+ const more = rest.length > 0 ? ` (and ${rest.length} more: ${rest.map(render).join(", ")})` : "";
295
+ return `${agree(flapped.length, "check", "checks")} flapped more than once in the last 24h: ${render(first)}${more}`;
296
+ }
297
+ /** Build provenance — is the harness about to run (this process's dist/) the code main
298
+ * describes? Only meaningful when this project IS the harness (isSelfHosted); elsewhere the
299
+ * stamp is reported as-is. A stale build is a warning, not a failure: the team runs, just not
300
+ * the newest code, and auto-restart (or a rebuild + restart) resolves it. `info`, `head` and
301
+ * `running` are injectable so tests can exercise every branch without compiling anything. */
302
+ export async function checkBuild(root, info = readBuildInfo(), head = undefined,
303
+ /** What the running orchestrator published about this build (orchestrator.json), when one is
304
+ * up. A stale build whose restart was REFUSED must not be reported with the boilerplate
305
+ * "auto-restart does this for a running team": nothing will happen until main moves, and
306
+ * doctor is where an operator goes to find out why. */
307
+ running = null) {
308
+ if (!info)
309
+ return okOutcome("no build stamp — dist/ compiled without `npm run build`");
310
+ const sha = shortSha(info.sha);
311
+ if (!(await isSelfHosted(root, info)))
312
+ // Not "dist/ from …": in someone else's project that names a dist/ directory the project
313
+ // does not have — the stamp read here is the running harness install's own, so say that.
314
+ return okOutcome(`tumwater version ${sha} (this project is not tumwater itself)`);
315
+ const mainHead = head === undefined ? await currentHead(root) : head;
316
+ if (!mainHead)
317
+ return okOutcome(`dist/ from ${sha}`);
318
+ const stale = await buildStaleness(root, info.sha, mainHead);
319
+ if (!stale)
320
+ return okOutcome(`dist/ from ${sha}`);
321
+ if (stale.stale) {
322
+ const next = running?.restartBlocked
323
+ ? `; auto-restart is BLOCKED (${running.restartBlocked}) and will not retry until main moves or the block clears`
324
+ : running?.restartPending
325
+ ? "; auto-restart is under way"
326
+ : "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
327
+ return warnOutcome(`dist/ from ${sha} is stale — main has ${stale.aheadCommits} later commit(s) touching ${STALE_INPUTS_LABEL}${next}`);
328
+ }
329
+ return okOutcome(`dist/ from ${sha}, matches main`);
330
+ }
331
+ /** The primary checkout's HEAD sha, or null when it cannot be resolved (no repo) —
332
+ * git/git.ts's refSha, the one home of the rev-parse probe. */
333
+ function currentHead(root) {
334
+ return refSha(root, "HEAD");
335
+ }
336
+ /** The doctor's `ci parity` row: does the full suite cover main's current tip? Reads the
337
+ * CI-parity ledger (src/run/ci-parity.ts) — the operator surface answering "is main verified by
338
+ * CI?" — and reports whether the last run covered the current main tip @ lock. Missing ledger
339
+ * reads "no CI parity recorded yet" (ok); a failed record is a `warn` naming the covered
340
+ * tip/lock (the ci role's contract is to fix or file what it finds, so a lingering failed record
341
+ * is actionable until the next green run); the `current`/`stale` split is `parityIsCurrent`'s
342
+ * one home, so the row and the ci prompt block cannot drift. Stale is normally `ok` — main moves
343
+ * on every merge and the ci role runs on its own clock, so a post-merge gap is normal and must
344
+ * not turn the Health card or the exit code red. A stale verdict that stays stale past
345
+ * `CI_PARITY_STALE_MS` (ciParityStalled) is a stall — the full suite has silently stopped
346
+ * covering new tips — and warns, alongside the standing `ci-stale` alert. Read-only against
347
+ * .tumwater/. */
348
+ export function checkCiParity(root, config, now = Date.now()) {
349
+ const entry = readCiParity(root);
350
+ if (!entry)
351
+ return okOutcome("no CI parity recorded yet — the ci role has not completed a workflow run");
352
+ const { tip, lockHash } = currentCiParityState(root, config ?? undefined);
353
+ const age = shortSpanPhrase(Math.max(0, now - entry.lastRunAt));
354
+ if (entry.result === "failed") {
355
+ return warnOutcome(`the last CI run failed covering ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} ${age} ago — the ci role must fix or file what it found before the next green run`);
356
+ }
357
+ if (parityIsCurrent(entry, tip, lockHash)) {
358
+ return okOutcome(`current — CI covered main at ${shortSha(tip)} @ lock ${shortSha(lockHash)}, passed ${age} ago`);
359
+ }
360
+ // A stale verdict is normal right after a merge (main moves and the ci role runs on its own
361
+ // clock), but one that stays stale past CI_PARITY_STALE_MS is a stall: the full suite has
362
+ // silently stopped covering new tips. The doctor warn and the standing `ci-stale` alert share
363
+ // this one predicate (ciParityStalled), so the row and the alert cannot disagree on when a
364
+ // gap is a problem. Inside the window the existing friendly stale wording stands.
365
+ if (ciParityStalled({ state: "stale", lastRunAt: entry.lastRunAt }, now)) {
366
+ return warnOutcome(`stale and stalled — the last run covered ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} ${age} ago (${entry.result}); main is now ${shortSha(tip)} @ lock ${shortSha(lockHash)} — no full-suite run has covered a tip in over ${CI_PARITY_STALE_MS / DAY_MS} days, so the ci role is disabled, paused, or unable to record`);
367
+ }
368
+ return okOutcome(`stale — the last run covered ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} (${entry.result}, ${age} ago); main is now ${shortSha(tip)} @ lock ${shortSha(lockHash)}, so the ci role has not yet covered this tip`);
369
+ }
370
+ /** The doctor's `qa coverage` row: has the `qa` observer — the team's only flow-level
371
+ * observer — stopped exercising any of the product's own flows? Reads the flow-coverage ledger
372
+ * (src/run/qa-coverage.ts), the operator surface answering "is qa still recording what it
373
+ * exercises?". A missing ledger reads "no qa flow coverage recorded yet" (ok — no record yet
374
+ * is not a stall); a fresh ledger reads ok naming the newest exercised flow and its age. A
375
+ * ledger whose newest exercise is older than `QA_COVERAGE_STALE_MS` (qaCoverageStalled) is a
376
+ * `warn` naming that flow and its age — the role is disabled, paused, or unable to record —
377
+ * alongside the standing `qa-stale` alert, both through the one `qaCoverageStalled` predicate
378
+ * so the row and the alert cannot disagree. Read-only against .tumwater/. */
379
+ export function checkQaCoverage(root, now = Date.now()) {
380
+ const coverage = readQaCoverage(root);
381
+ const entries = Object.entries(coverage);
382
+ if (entries.length === 0)
383
+ return okOutcome("no qa flow coverage recorded yet — the qa role has not completed a flow");
384
+ let newestFlow = entries[0][0];
385
+ let newestAt = entries[0][1].lastRunAt;
386
+ for (const [name, entry] of entries) {
387
+ if (entry.lastRunAt > newestAt) {
388
+ newestAt = entry.lastRunAt;
389
+ newestFlow = name;
390
+ }
391
+ }
392
+ const age = shortSpanPhrase(Math.max(0, now - newestAt));
393
+ if (qaCoverageStalled(coverage, now)) {
394
+ return warnOutcome(`the newest exercised flow ${newestFlow} ran ${age} ago — no qa flow has been exercised in over ${QA_COVERAGE_STALE_MS / DAY_MS} days, so the qa role is disabled, paused, or unable to record`);
395
+ }
396
+ return okOutcome(`newest flow coverage: ${newestFlow} exercised ${age} ago`);
397
+ }