tumwater 0.2.0 → 0.3.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 (446) 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 +42 -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 +4 -3
  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 +402 -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 +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -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 +1 -2
  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 +118 -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 +150 -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 +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -0,0 +1,303 @@
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 { flakeCounts, KNOWN_FLAKE_WINDOW_MS, loadQuarantine } from "../review/known-flakes.js";
30
+ import { quarantineWarning } from "../baseline/main-guardrails.js";
31
+ import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
32
+ /** .tumwater writable — absent is fine (created on first run); present, prove it by writing
33
+ * and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
34
+ * The write is the property under test: only a write that fails is "not writable", so the
35
+ * cleanup below — deleting the probe, sweeping stales — never vetoes a dir the write just
36
+ * proved writable, and a probe left over is swept rather than accumulated. */
37
+ export function checkStateDir(root) {
38
+ const dir = path.join(root, STATE_DIR);
39
+ if (!fs.existsSync(dir))
40
+ return okOutcome("absent — created on first run");
41
+ const probe = path.join(dir, `.doctor-probe-${process.pid}`);
42
+ try {
43
+ fs.writeFileSync(probe, "");
44
+ }
45
+ catch (err) {
46
+ // A partial write may already have created the file — sweep it like any other leftover.
47
+ sweepStaleProbes(dir);
48
+ return failOutcome(`not writable: ${errorMessage(err)}`);
49
+ }
50
+ // The write proved writability. Deleting the probe is cleanup, not measurement: a removal
51
+ // failure must not turn a succeeded write into a false alarm, and the stray is collected by
52
+ // the sweep below (or a later pass's sweep).
53
+ try {
54
+ fs.rmSync(probe);
55
+ }
56
+ catch {
57
+ // Best-effort — the stray is swept in a moment.
58
+ }
59
+ sweepStaleProbes(dir);
60
+ return okOutcome("writable");
61
+ }
62
+ /** Best-effort removal of every `.doctor-probe-*` file in `dir` — the only artifact
63
+ * checkStateDir creates, so any present probe is a leftover: a prior run killed between its
64
+ * write and delete, a partial write above, or a removal that failed. Removed one at a time so
65
+ * a single unremovable probe cannot stop the sweep. */
66
+ function sweepStaleProbes(dir) {
67
+ for (const name of dirEntryNames(dir)) {
68
+ if (!name.startsWith(".doctor-probe-"))
69
+ continue;
70
+ try {
71
+ fs.rmSync(path.join(dir, name));
72
+ }
73
+ catch {
74
+ // Best-effort — leave it for the next pass.
75
+ }
76
+ }
77
+ }
78
+ /** Event log readable — open and parse events.jsonl (and its rotated archive) so the one
79
+ * failure the selfcheck role is chartered to catch — a feed that hid itself — is named by the
80
+ * pre-flight too. An absent or empty log is fine: a young project writes its first event on the
81
+ * first run. A present-but-unparseable log (a directory at the path, a permission failure, or
82
+ * every complete line corrupt) fails, because every observer — selfcheck, the TUI/GUI feeds,
83
+ * `logs`, `report` — reads that one feed. Read-only. */
84
+ export function checkEventLog(root) {
85
+ let state;
86
+ try {
87
+ state = eventLogState(root);
88
+ }
89
+ catch (err) {
90
+ return failOutcome(`could not read ${eventsLogPath(root)}: ${errorMessage(err)}`);
91
+ }
92
+ if (state === "unreadable")
93
+ return failOutcome(`unreadable: ${eventsLogPath(root)} — the team's event feed cannot be read`);
94
+ if (state === "empty")
95
+ return okOutcome("no events yet");
96
+ const history = eventLogHistoryPhrase(root);
97
+ return okOutcome(history ? `readable: ${eventsLogPath(root)} — ${history}` : `readable: ${eventsLogPath(root)}`);
98
+ }
99
+ /** The `history from <date>, <n> files` phrase for checkEventLog's ok detail (G4): the first
100
+ * complete line of the oldest event-log generation present names how far back the kept history
101
+ * reaches, and the count is how many generations (live file plus archives) exist. Undefined
102
+ * when no generation parses an event, so a readable-but-shapeless log still reads as readable. */
103
+ function eventLogHistoryPhrase(root) {
104
+ const present = [];
105
+ const livePath = eventsLogPath(root);
106
+ if (statOrNull(livePath))
107
+ present.push(livePath);
108
+ for (let gen = 1; gen <= EVENTS_KEEP; gen++) {
109
+ const archive = eventsArchivePath(root, gen);
110
+ if (statOrNull(archive))
111
+ present.push(archive);
112
+ }
113
+ if (present.length === 0)
114
+ return undefined;
115
+ const oldest = present[present.length - 1];
116
+ const ev = firstEventLine(oldest);
117
+ if (!ev || typeof ev.ts !== "number")
118
+ return undefined;
119
+ const noun = plural(present.length, "file", "files");
120
+ return `history from ${dayKey(ev.ts)}, ${noun}`;
121
+ }
122
+ /** Parse the first complete line of an append-only JSONL `file`, or null when it is unreadable
123
+ * or does not parse. Reads only the first chunk — the event log is up to 16 MB a generation,
124
+ * and a doctor pre-flight should not slurp the oldest archive just to name its first day. */
125
+ function firstEventLine(file) {
126
+ try {
127
+ const fd = fs.openSync(file, "r");
128
+ try {
129
+ const buf = Buffer.alloc(4096);
130
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
131
+ const line = buf.toString("utf8", 0, n).split("\n")[0] ?? "";
132
+ if (!line)
133
+ return null;
134
+ try {
135
+ return JSON.parse(line);
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ finally {
142
+ fs.closeSync(fd);
143
+ }
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /** Disk floor (plans/disk-floor.md, parts 1/4 and 2/4) — is there room on the volume the
150
+ * worktrees live on? Fail below `diskHoldGB`, the same floor the running team holds new work
151
+ * at, so doctor and the gate agree on when the disk is a problem. Warn in the band between
152
+ * `diskHoldGB` and `diskReclaimGB`: pressure reclaim will delete ignored build outputs from
153
+ * idle worktrees but the team still runs. Ok at or above `diskReclaimGB` (or when reclaim is
154
+ * disabled and free space is above the hold). A statfs failure is a warning, not a failure: an
155
+ * unmeasurable volume never holds the team, so it must not fail a scripted pre-flight. The
156
+ * detail names the measured path, the free GB to one decimal, and the floor. `holdGB` 0
157
+ * disables the hold, so a free space above `reclaimGB`'s own band (or `reclaimGB` 0) passes.
158
+ * After a successful sample the detail also carries `diskReclaimPhrase` (what `disk_reclaim`
159
+ * passes freed over the last 24 h); it is informational and never changes the level. */
160
+ export function checkDiskSpace(root, holdGB, reclaimGB, sample = sampleFreeBytes) {
161
+ const where = diskVolumePath(root);
162
+ const free = sample(root);
163
+ if (free === null)
164
+ return warnOutcome(`cannot measure free space at ${where} (statfs failed) — the disk hold is off for this process`);
165
+ const freeGB = free / BYTES_PER_GB;
166
+ const freeText = `${formatGB(freeGB)} free at ${where}`;
167
+ const phrase = diskReclaimPhrase(readEventsSince(root, DAY_MS).events);
168
+ const withReclaim = (detail) => (phrase ? `${detail} · ${phrase}` : detail);
169
+ if (holdGB > 0 && freeGB < holdGB)
170
+ return failOutcome(withReclaim(`${freeText} — below the ${holdGB} GB diskHoldGB floor; the team waits to start new work until space recovers`));
171
+ if (reclaimGB > 0 && freeGB < reclaimGB)
172
+ return warnOutcome(withReclaim(`${freeText} — below the ${reclaimGB} GB diskReclaimGB threshold; a pressure reclaim pass deletes ignored build outputs from idle worktrees`));
173
+ const floor = holdGB > 0 ? `above the ${holdGB} GB floor` : "diskHoldGB is 0 (hold disabled)";
174
+ return okOutcome(withReclaim(`${freeText}, ${floor}`));
175
+ }
176
+ /** The `reclaim in the last 24h freed …` phrase for the `disk space` row: the summed
177
+ * `freedGB` of the `disk_reclaim` passes, their count, and how many ran under pressure (an
178
+ * event without a `mode` counts as pressure, like its feed line). Each pass is a point event,
179
+ * so summing cannot straddle the window edge. Undefined when no pass ran. */
180
+ export function diskReclaimPhrase(events) {
181
+ const passes = events.filter((e) => e.type === "disk_reclaim");
182
+ if (passes.length === 0)
183
+ return undefined;
184
+ const freed = passes.reduce((sum, e) => sum + finiteNumber(e.freedGB, 0), 0);
185
+ const pressure = passes.filter((e) => (typeof e.mode === "string" && e.mode ? e.mode : "pressure") === "pressure").length;
186
+ const base = `reclaim in the last 24h freed ${formatGB(freed)} over ${plural(passes.length, "pass", "passes")}`;
187
+ return pressure > 0 ? `${base}, ${pressure} under pressure` : base;
188
+ }
189
+ /** The `passing merge checks …` phrase for the `check` row: median and slowest wall clock of
190
+ * the passed full-suite runs (`merge`, `batch`, `baseline` scopes — a `gate` check can run a
191
+ * subset, and failed or skipped runs do not measure a full pass). The lower median keeps the
192
+ * figure a run that happened. Undefined when no event qualifies. */
193
+ export function checkDurationPhrase(events) {
194
+ const durations = events
195
+ .filter(isPassingFullCheck)
196
+ .map((e) => finiteNumber(e.durationMs, 0))
197
+ .filter((ms) => ms > 0);
198
+ if (durations.length === 0)
199
+ return undefined;
200
+ const median = lowerMedian(durations);
201
+ const slowest = Math.max(...durations);
202
+ return `passing merge checks in the last 24h took ${shortSpanPhrase(median)} median, ${shortSpanPhrase(slowest)} slowest (${plural(durations.length, "run")})`;
203
+ }
204
+ /** Declared project check — names what the review gate's deterministic pre-check, the
205
+ * red-main baseline, and redeploy's green check will run, without running it: a configured
206
+ * `check.command` (plans/portability.md §6/7), else the npm auto-detection. None declared is
207
+ * a warn, not informational (the 2026-09-05 stance predates non-npm targets being supported,
208
+ * when a warn would have been noise no operator could act on — with `check.command` available
209
+ * the warning is actionable): the three gates degrade to "no check" and a silently absent
210
+ * safety layer is the failure this check exists to surface. Warn does not fail the exit code
211
+ * (the fallback check's precedent, doctor-model-checks.ts's checkFallbackModel). */
212
+ export function checkBuildCheck(root, config) {
213
+ const check = detectBuildCheck(root, config ?? undefined);
214
+ if (!check)
215
+ 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)");
216
+ // The in-lock command from mergeCommandOf (absent mergeCommand falls back to the gate
217
+ // command): the row names which command the merge lock runs and where the full check lands,
218
+ // facts a reader cannot get from detectBuildCheck alone. The after-merge roles are not named
219
+ // here — they live under `review`, which this slice does not hold, and the `review` row in
220
+ // docs/config.md documents them.
221
+ const lockPhrase = (() => {
222
+ const mc = mergeCommandOf(config ?? undefined);
223
+ return mc !== undefined
224
+ ? `merge lock runs ${mc}, full check after merging`
225
+ : "full check in the merge lock";
226
+ })();
227
+ const phrase = checkDurationPhrase(readEventsSince(root, DAY_MS).events);
228
+ const withTiming = (detail) => okOutcome(`${detail} · ${lockPhrase}${phrase ? ` · ${phrase}` : ""}`);
229
+ if (check.kind === "command") {
230
+ const where = path.relative(root, check.cwd);
231
+ return withTiming(where ? `${check.command} (cwd ${where})` : check.command);
232
+ }
233
+ const where = path.relative(root, check.rootDir);
234
+ return withTiming(where ? `npm ${check.script} in ${where}` : `npm ${check.script}`);
235
+ }
236
+ /** The review gate's flake memory surfaced read-only: names which checks the gate kept excusing as
237
+ * flakes over its own KNOWN_FLAKE_WINDOW_MS window. The gate logs `gate check failed then passed on
238
+ * retry — flaky: <headline>` for each failure that passed on its one immediate re-run, and the
239
+ * in-lock merge check logs the same under MERGE_FLAKY_WARNING_PREFIX when its kept-pin retry passes
240
+ * (src/review/known-flakes.ts); that record is internal to the gate's matching, so a genuinely
241
+ * flaky check silently burns a re-run per merge and hides real instability — exactly the
242
+ * information that explains intermittent rejections. This check reads the same window and folds the
243
+ * same normalized headline the gate matches with (flakeCounts, so the prefix slice-offset and
244
+ * cluster normalization cannot drift), and warns only when a normalized check flapped more than
245
+ * once, so a routine one-off flake stays quiet. ok/warn only — never a fail — so a flaky suite
246
+ * cannot fail a scripted doctor pre-flight (the checkStalledPlans precedent; the exit-code rule is
247
+ * outcome.ts's). */
248
+ export function checkFlakyChecks(root, now = Date.now()) {
249
+ const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
250
+ const flapped = flakeCounts(events).filter(([, n]) => n >= 2);
251
+ const phrases = [];
252
+ if (flapped.length > 0) {
253
+ const render = ([k, n]) => `"${k}" ×${n}`;
254
+ const [first, ...rest] = flapped;
255
+ const more = rest.length > 0 ? ` (and ${rest.length} more: ${rest.map(render).join(", ")})` : "";
256
+ phrases.push(`${agree(flapped.length, "check", "checks")} flapped more than once in the last 24h: ${render(first)}${more}`);
257
+ }
258
+ const q = quarantineWarning(loadQuarantine(root), now);
259
+ if (q !== null)
260
+ phrases.push(q);
261
+ if (phrases.length === 0)
262
+ return okOutcome("no check flapped more than once in the last 24h");
263
+ return warnOutcome(phrases.join("; "));
264
+ }
265
+ /** Build provenance — is the harness about to run (this process's dist/) the code main
266
+ * describes? Only meaningful when this project IS the harness (isSelfHosted); elsewhere the
267
+ * stamp is reported as-is. A stale build is a warning, not a failure: the team runs, just not
268
+ * the newest code, and auto-restart (or a rebuild + restart) resolves it. `info`, `head` and
269
+ * `running` are injectable so tests can exercise every branch without compiling anything. */
270
+ export async function checkBuild(root, info = readBuildInfo(), head = undefined,
271
+ /** What the running orchestrator published about this build (orchestrator.json), when one is
272
+ * up. A stale build whose restart was REFUSED must not be reported with the boilerplate
273
+ * "auto-restart does this for a running team": nothing will happen until main moves, and
274
+ * doctor is where an operator goes to find out why. */
275
+ running = null) {
276
+ if (!info)
277
+ return okOutcome("no build stamp — dist/ compiled without `npm run build`");
278
+ const sha = shortSha(info.sha);
279
+ if (!(await isSelfHosted(root, info)))
280
+ // Not "dist/ from …": in someone else's project that names a dist/ directory the project
281
+ // does not have — the stamp read here is the running harness install's own, so say that.
282
+ return okOutcome(`tumwater version ${sha} (this project is not tumwater itself)`);
283
+ const mainHead = head === undefined ? await currentHead(root) : head;
284
+ if (!mainHead)
285
+ return okOutcome(`dist/ from ${sha}`);
286
+ const stale = await buildStaleness(root, info.sha, mainHead);
287
+ if (!stale)
288
+ return okOutcome(`dist/ from ${sha}`);
289
+ if (stale.stale) {
290
+ const next = running?.restartBlocked
291
+ ? `; auto-restart is BLOCKED (${running.restartBlocked}) and will not retry until main moves or the block clears`
292
+ : running?.restartPending
293
+ ? "; auto-restart is under way"
294
+ : "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
295
+ return warnOutcome(`dist/ from ${sha} is stale — main has ${stale.aheadCommits} later commit(s) touching ${STALE_INPUTS_LABEL}${next}`);
296
+ }
297
+ return okOutcome(`dist/ from ${sha}, matches main`);
298
+ }
299
+ /** The primary checkout's HEAD sha, or null when it cannot be resolved (no repo) —
300
+ * git/git.ts's refSha, the one home of the rev-parse probe. */
301
+ function currentHead(root) {
302
+ return refSha(root, "HEAD");
303
+ }
@@ -1,43 +1,64 @@
1
- // vocabulary: plain
2
1
  import { loadConfigSafe } from "../config/config.js";
3
2
  import { envPath } from "../files/files.js";
4
3
  import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
5
4
  import { systemProcessProbe } from "../process/process-table.js";
6
5
  import { checkOrphans } from "./doctor-orphans.js";
7
- import { checkBudget, checkClaudeWindow, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours } from "./doctor-holds.js";
6
+ import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours, checkSubscriptionWindow } from "./doctor-holds.js";
8
7
  import { checkRootInstall } from "./doctor-install.js";
9
8
  import { checkNotify } from "./doctor-notify.js";
10
9
  import { checkLaunchServicesPorts } from "./doctor-launch-services.js";
11
10
  import { plural } from "../text/phrases.js";
12
11
  import { shortSha } from "../text/format.js";
13
- import { checkAgentBinary, checkBrief, checkBootstrap, checkBuild, checkBuildCheck, checkClaudeTranscripts, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkDiskSpace, checkEventLog, checkFlakyChecks, checkOpencodeVersion, checkMergeLock, checkNodeVersion, checkPiExtensions, checkProviderConcurrency, checkProviderContention, checkRepo, checkStateDir, checkWorktreePool, checkLeftoverRoleBranches, checkWorkInstances, } from "./doctor-checks.js";
14
- import { checkClaudeAuth, checkClaudeVersion, checkAnthroApiKey, checkFallbackChain, checkFallbackModel, checkImageInput, checkOpencode, checkTierModels } from "./doctor-model-checks.js";
15
- import { agentQualifiedSelector } from "../config/config-views.js";
12
+ import { failOutcome } from "./outcome.js";
13
+ import { errorMessage } from "../text/text.js";
14
+ import { checkBackendBinary, checkBrief, checkBootstrap, checkClaudeTranscripts, checkExecPath, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkOpencodeVersion, checkNodeVersion, checkPiExtensions, checkRepo, } from "./doctor-checks.js";
15
+ import { checkBuild, checkBuildCheck, checkDiskSpace, checkEventLog, checkFlakyChecks, checkStateDir, } from "./doctor-project-checks.js";
16
+ import { sampleFreeBytes } from "../gates/disk-gate.js";
17
+ import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderConcurrency, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
18
+ import { checkFallbackChain, checkFallbackModel, checkFeedbackFiling, checkImageInput, checkTierModels } from "./doctor-model-checks.js";
19
+ import { checkAnthroApiKey, checkClaudeAuth, checkClaudeVersion } from "./doctor-claude-checks.js";
20
+ import { checkOpencode } from "./doctor-opencode-checks.js";
21
+ import { checkCodexAccount, checkCodexAuth, checkCodexHome, checkCodexLeaks, checkCodexModels, checkCodexSessions, checkCodexWriteGuard, checkCodexVersion, } from "./doctor-codex-checks.js";
22
+ import { backendQualifiedSelector } from "../config/config-views.js";
16
23
  import { seamsInUse } from "../config/config-fallbacks.js";
17
- import { checkBacklogHeadings, checkFixClaims, checkLiveFinished, checkPlanDependencies, checkRefusedEntries, checkRegressedFixes, checkStaleQueuedMerges, checkStaleBugs, checkStalePrompts, checkStaleQuestions, checkStalledPlans, checkStrandedPlans, checkUncommittedBacklog, checkValidationGaps, checkWaitingBugs } from "./doctor-backlog.js";
24
+ import { checkBacklogHeadings, checkFixClaims, checkLiveFinished, checkPlanDependencies, checkStalledPlans, checkStrandedPlans, checkValidationGaps } from "./doctor-backlog-plans.js";
25
+ import { checkRefusedEntries, checkRegressedFixes, checkStaleBugs, checkStaleQuestions, checkWaitingBugs } from "./doctor-backlog-bugs.js";
26
+ import { checkMergeQueueFiles, checkStaleQueuedMerges, checkStalePrompts, checkUncommittedBacklog } from "./doctor-backlog.js";
18
27
  /** Pre-flight environment check (`tumwater doctor`). The harness's preconditions are
19
28
  * scattered across fail-fast checks that each command re-runs on its own (requireReadyRepo in
20
29
  * cli.ts walks git-binary → repo → tumwater.json → commits and stops at the first failure;
21
- * cmdRun adds pi-on-PATH and orchestrator-alive), so a user facing a partially initialized or
30
+ * cmdStart adds pi-on-PATH and orchestrator-alive), so a user facing a partially initialized or
22
31
  * drifted environment gets one error at a time. doctor runs every check, reports each result
23
32
  * individually instead of stopping at the first failure, and exits 0/1 so it can be scripted —
24
33
  * the pre-flight sibling of `status --json`, which queries live team state while doctor
25
34
  * verdicts on the environment. Every check is read-only against .tumwater/ (works with or
26
35
  * without a running harness) and none runs the project's build/test: that can take minutes and
27
36
  * belongs in the review gate / red-main check, not a pre-flight. The individual checks live in
28
- * doctor-checks.ts and doctor-model-checks.ts; this module composes them into the report. */
29
- /** Wrap a possibly-absent doctor check into the report's `checks` spread: null omits the row
30
- * entirely (the pi-extensions row for a team with no pi seam; the funes row when funes is not
31
- * on PATH); an outcome contributes `{ name, ...outcome }`. */
32
- function appendNamedOutcome(name, outcome) {
33
- return outcome === null ? [] : [{ name, ...outcome }];
37
+ * doctor-checks.ts, doctor-project-checks.ts, doctor-contention-checks.ts, doctor-model-checks.ts,
38
+ * doctor-claude-checks.ts, doctor-opencode-checks.ts, doctor-backlog.ts,
39
+ * doctor-backlog-plans.ts and doctor-backlog-bugs.ts;
40
+ * this module composes them into the report. */
41
+ /** Run one doctor check, turning a throw or rejection into a fail row: a check that crashed
42
+ * verified nothing, so it must not read as a pass or abort the report. */
43
+ export async function runDoctorCheck(run) {
44
+ try {
45
+ return await run();
46
+ }
47
+ catch (err) {
48
+ return failOutcome(`the check threw: ${errorMessage(err)}`);
49
+ }
34
50
  }
35
51
  /** Run every check in order and compose the report. Read-only against .tumwater/ by
36
52
  * construction — no check removes or repairs anything (the state-dir probe writes a temp file
37
53
  * and deletes it again; the orphan check reports processes, never signals them) — so doctor
38
54
  * works identically with or without a running harness. `probe` feeds the orphan and port
39
- * checks, so tests can pin the report without reading the host's process table. */
40
- export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe, probeImages = "never") {
55
+ * checks, so tests can pin the report without reading the host's process table. `diskSample`
56
+ * feeds the disk check the same way, so tests can pin the report without reading the host's
57
+ * real free space (a hosted runner under the 10 GB floor would fail its row). `only` runs
58
+ * just the named checks, in report order; a skipped check is never evaluated, and a named
59
+ * conditional check that returns null leaves no row. A check that throws becomes its own fail
60
+ * row, and every other check still runs. */
61
+ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe, probeImages = "never", only, diskSample = sampleFreeBytes) {
41
62
  // Loaded once for the checks that read config (repo's baseBranch); a broken file stays
42
63
  // undefined — checkInit reports it verbatim — so doctor still runs every other check.
43
64
  const { config } = loadConfigSafe(root);
@@ -48,7 +69,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
48
69
  const routing = config
49
70
  ? seamsInUse(config).map((s) => ({
50
71
  label: s.label,
51
- selector: s.model !== undefined ? agentQualifiedSelector({ ...s, model: s.model }) : "no model declared",
72
+ selector: s.model !== undefined ? backendQualifiedSelector({ ...s, model: s.model }) : "no model declared",
52
73
  backend: s.agent ?? "pi",
53
74
  }))
54
75
  : [];
@@ -56,80 +77,103 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
56
77
  const header = orchestratorAlive(root, info) && info
57
78
  ? `tumwater doctor — running (pid ${info.pid}${info.build ? `, version ${shortSha(info.build.sha)}${info.build.stale ? " — update available" : ""}${info.build.restartBlocked ? " (restart blocked)" : ""}` : ""})`
58
79
  : "tumwater doctor — not running";
59
- const checks = [
60
- { name: "node", ...checkNodeVersion() },
61
- { name: "git binary", ...checkGitBinary(pathEnv) },
62
- { name: "repo", ...(await checkRepo(root, config)) },
63
- { name: "init", ...checkInit(root) },
64
- { name: "config key names", ...checkConfigKeyNames(root) },
65
- { name: "brief", ...checkBrief(root) },
80
+ const steps = [
81
+ { name: "node", run: () => checkNodeVersion() },
82
+ { name: "node binary", run: () => checkExecPath() },
83
+ { name: "git binary", run: () => checkGitBinary(pathEnv) },
84
+ { name: "repo", run: () => checkRepo(root, config) },
85
+ { name: "init", run: () => checkInit(root) },
86
+ { name: "config key names", run: () => checkConfigKeyNames(root) },
87
+ { name: "brief", run: () => checkBrief(root) },
66
88
  // New-project bootstrap (plans/work-ratio.md, part 2/2): an active hold reports as ok
67
89
  // information, never a warning — a young project is not misconfigured.
68
- { name: "bootstrap", ...checkBootstrap(root, config) },
69
- { name: "pause", ...checkPause(root) },
70
- { name: "quiet hours", ...checkQuietHours(config) },
71
- { name: "budget", ...checkBudget(root, config, (orchestratorAlive(root, info) && info?.budget) || null) },
72
- { name: "claude window", ...checkClaudeWindow(root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
73
- { name: "provider waits", ...checkProviderHolds(root) },
74
- { name: "maintenance allowance", ...checkMaintenanceQuota(root, config, orchestratorAlive(root, info)) },
75
- { name: "provider concurrency", ...checkProviderConcurrency(config, root) },
76
- { name: "provider contention", ...checkProviderContention(root, config) },
77
- { name: "repeated failures", ...checkErrorStreaks(root, config) },
78
- { name: "main's check", ...checkMainRed(root, config) },
79
- { name: "over-cap model", ...checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
80
- { name: "fallback chain", ...(await checkFallbackChain(root)) },
81
- { name: "models for each tier", ...(await checkTierModels(root)) },
82
- { name: "image input", ...(await checkImageInput(root, undefined, undefined, undefined, probeImages, pathEnv)) },
83
- { name: "claude auth", ...(await checkClaudeAuth(root, pathEnv)) },
84
- { name: "claude api key", ...(await checkAnthroApiKey(root, undefined, pathEnv)) },
85
- { name: "claude version", ...(await checkClaudeVersion(root, pathEnv)) },
86
- { name: "opencode version", ...(await checkOpencodeVersion(root, pathEnv)) },
87
- { name: "opencode", ...(await checkOpencode(root, pathEnv)) },
88
- { name: "backend binary", ...checkAgentBinary(root, pathEnv) },
90
+ { name: "bootstrap", run: () => checkBootstrap(root, config) },
91
+ { name: "pause", run: () => checkPause(root) },
92
+ { name: "quiet hours", run: () => checkQuietHours(config) },
93
+ { name: "budget", run: () => checkBudget(root, config, (orchestratorAlive(root, info) && info?.budget) || null) },
94
+ { name: "claude window", run: () => checkSubscriptionWindow("claude", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
95
+ { name: "codex window", run: () => checkSubscriptionWindow("codex", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
96
+ { name: "provider waits", run: () => checkProviderHolds(root) },
97
+ { name: "maintenance allowance", run: () => checkMaintenanceQuota(root, config, orchestratorAlive(root, info)) },
98
+ { name: "provider concurrency", run: () => checkProviderConcurrency(config, root) },
99
+ { name: "provider contention", run: () => checkProviderContention(root, config) },
100
+ { name: "check contention", run: () => checkCheckContention(root, config) },
101
+ { name: "repeated failures", run: () => checkErrorStreaks(root, config) },
102
+ { name: "main's check", run: () => checkMainRed(root, config) },
103
+ { name: "over-cap model", run: () => checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
104
+ { name: "fallback chain", run: () => checkFallbackChain(root) },
105
+ { name: "models for each tier", run: () => checkTierModels(root) },
106
+ { name: "image input", run: () => checkImageInput(root, undefined, undefined, undefined, probeImages, pathEnv) },
107
+ { name: "claude auth", run: () => checkClaudeAuth(root, pathEnv) },
108
+ { name: "claude api key", run: () => checkAnthroApiKey(root, undefined, pathEnv) },
109
+ { name: "claude version", run: () => checkClaudeVersion(root, pathEnv) },
110
+ { name: "opencode version", run: () => checkOpencodeVersion(root, pathEnv) },
111
+ { name: "opencode", run: () => checkOpencode(root, pathEnv) },
112
+ { name: "codex version", run: () => checkCodexVersion(root, pathEnv) },
113
+ { name: "codex login", run: () => checkCodexAuth(root, pathEnv) },
114
+ { name: "codex home", run: () => checkCodexHome(root) },
115
+ { name: "codex models", run: () => checkCodexModels(root) },
116
+ { name: "codex account", run: () => checkCodexAccount(root) },
117
+ { name: "codex leaks", run: () => checkCodexLeaks(root) },
118
+ { name: "codex sessions", run: () => checkCodexSessions(root) },
119
+ { name: "codex write guard", run: () => checkCodexWriteGuard(root, pathEnv) },
120
+ { name: "backend binary", run: () => checkBackendBinary(root, pathEnv) },
121
+ { name: "feedback filing", run: () => checkFeedbackFiling(root, pathEnv) },
89
122
  // The transcript total is informational and only read when a seam routes to claude.
90
- { name: "claude transcripts", ...checkClaudeTranscripts(config) },
123
+ { name: "claude transcripts", run: () => checkClaudeTranscripts(config) },
91
124
  // The receive-side guards name what a running team skips, so an operator tool meant for the
92
125
  // team (a pi package, an extension file, an mcp.json server, or funes) does not vanish
93
126
  // silently. Each check returns null to omit its row: the pi row for a team with no pi seam,
94
- // the funes row when funes is not on PATH.
95
- ...(appendNamedOutcome("pi extensions", checkPiExtensions(config, undefined, pathEnv))),
96
- ...(appendNamedOutcome("funes", checkFunes(config, pathEnv))),
97
- { name: "state dir", ...checkStateDir(root) },
98
- { name: "event log", ...checkEventLog(root) },
99
- { name: "notify", ...checkNotify(root, config) },
100
- { name: "merge lock", ...checkMergeLock(root) },
101
- { name: "workspaces", ...checkWorktreePool(root) },
102
- { name: "parallel instances", ...checkWorkInstances(root, config) },
103
- { name: "leftover role branches", ...(await checkLeftoverRoleBranches(root, config)) },
127
+ // the funes row when funes is not on PATH, and the claude/codex window rows when that backend
128
+ // has no subscription account.
129
+ { name: "pi extensions", run: () => checkPiExtensions(config, undefined, pathEnv) },
130
+ { name: "funes", run: () => checkFunes(config, pathEnv) },
131
+ { name: "state dir", run: () => checkStateDir(root) },
132
+ { name: "event log", run: () => checkEventLog(root) },
133
+ { name: "notify", run: () => checkNotify(root, config) },
134
+ { name: "merge lock", run: () => checkMergeLock(root) },
135
+ { name: "workspaces", run: () => checkWorktreePool(root) },
136
+ { name: "parallel instances", run: () => checkWorkInstances(root, config) },
137
+ { name: "leftover role branches", run: () => checkLeftoverRoleBranches(root, config) },
138
+ { name: "tumwater pins", run: () => checkTumwaterPins(root) },
104
139
  // The disk floor (plans/disk-floor.md, part 1/4): a broken config stays undefined (checkInit
105
140
  // reports it), so the check falls back to the 10 GB default rather than skipping itself.
106
- { name: "disk space", ...checkDiskSpace(root, config?.diskHoldGB ?? 10, config?.diskReclaimGB ?? 40) },
107
- { name: "check", ...checkBuildCheck(root, config) },
108
- { name: "root install", ...checkRootInstall(root) },
141
+ { name: "disk space", run: () => checkDiskSpace(root, config?.diskHoldGB ?? 10, config?.diskReclaimGB ?? 40, diskSample) },
142
+ { name: "check", run: () => checkBuildCheck(root, config) },
143
+ { name: "root install", run: () => checkRootInstall(root) },
109
144
  // The review gate's flake memory, surfaced read-only: naming which checks the gate kept
110
145
  // excusing as flakes (ok/warn only, so a flaky suite never fails a scripted pre-flight).
111
- { name: "flaky checks", ...checkFlakyChecks(root) },
112
- { name: "claimed bugs", ...checkFixClaims(root) },
113
- { name: "validation gaps", ...checkValidationGaps(root) },
114
- { name: "plans stuck in Planned", ...checkStrandedPlans(root) },
115
- { name: "backlog headings", ...checkBacklogHeadings(root) },
116
- { name: "backlog archive", ...checkLiveFinished(root) },
117
- { name: "refusals", ...checkRefusedEntries(root) },
118
- { name: "stalled plans", ...checkStalledPlans(root) },
119
- { name: "plan dependencies", ...checkPlanDependencies(root) },
120
- { name: "stale questions", ...checkStaleQuestions(root) },
121
- { name: "stale bugs", ...checkStaleBugs(root) },
122
- { name: "waiting bugs", ...checkWaitingBugs(root) },
123
- { name: "regressed fixes", ...checkRegressedFixes(root) },
124
- { name: "stale merge-queue entries", ...checkStaleQueuedMerges(root, Date.now()) },
125
- { name: "stale prompts", ...checkStalePrompts(root, Date.now()) },
126
- { name: "uncommitted backlog", ...(await checkUncommittedBacklog(root, config)) },
146
+ { name: "flaky checks", run: () => checkFlakyChecks(root) },
147
+ { name: "claimed bugs", run: () => checkFixClaims(root) },
148
+ { name: "validation gaps", run: () => checkValidationGaps(root) },
149
+ { name: "plans stuck in Planned", run: () => checkStrandedPlans(root) },
150
+ { name: "backlog headings", run: () => checkBacklogHeadings(root) },
151
+ { name: "backlog archive", run: () => checkLiveFinished(root) },
152
+ { name: "refusals", run: () => checkRefusedEntries(root) },
153
+ { name: "stalled plans", run: () => checkStalledPlans(root) },
154
+ { name: "plan dependencies", run: () => checkPlanDependencies(root) },
155
+ { name: "stale questions", run: () => checkStaleQuestions(root) },
156
+ { name: "stale bugs", run: () => checkStaleBugs(root) },
157
+ { name: "waiting bugs", run: () => checkWaitingBugs(root) },
158
+ { name: "regressed fixes", run: () => checkRegressedFixes(root) },
159
+ { name: "stale merge-queue entries", run: () => checkStaleQueuedMerges(root, Date.now()) },
160
+ { name: "merge-queue files", run: () => checkMergeQueueFiles(root) },
161
+ { name: "stale prompts", run: () => checkStalePrompts(root, Date.now()) },
162
+ { name: "uncommitted backlog", run: () => checkUncommittedBacklog(root, config) },
127
163
  // The running team's own view of its build (staleness and what auto-restart made of it)
128
164
  // when there is one; without it the check still stands on its own git comparison.
129
- { name: "update", ...(await checkBuild(root, undefined, undefined, (orchestratorAlive(root, info) && info?.build) || null)) },
130
- { name: "orphans", ...(await checkOrphans(root, probe)) },
131
- { name: "macOS port leak", ...(await checkLaunchServicesPorts(probe)) },
165
+ { name: "update", run: () => checkBuild(root, undefined, undefined, (orchestratorAlive(root, info) && info?.build) || null) },
166
+ { name: "orphans", run: () => checkOrphans(root, probe) },
167
+ { name: "macOS port leak", run: () => checkLaunchServicesPorts(probe) },
132
168
  ];
169
+ const checks = [];
170
+ for (const step of steps) {
171
+ if (only && !only.includes(step.name))
172
+ continue;
173
+ const outcome = await runDoctorCheck(step.run);
174
+ if (outcome !== null)
175
+ checks.push({ name: step.name, ...outcome });
176
+ }
133
177
  const problems = checks.filter((c) => c.level === "fail").length;
134
178
  return { header, checks, routing, verdict: problems === 0 ? "ready to run" : plural(problems, "problem") };
135
179
  }