tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -1,13 +1,13 @@
1
1
  import os from "node:os";
2
2
  import { childEnv } from "../process/process.js";
3
- import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
3
+ import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
4
4
  import { claudeBackend, claudeConfigDir } from "../agent/claude/claude-backend.js";
5
5
  import { claudeEnv } from "../agent/claude/claude-env.js";
6
6
  import { claudeAuthUsesSubscription, parseClaudeAuthStatus } from "../agent/claude/claude-account.js";
7
7
  import { resolvedBackendConfig } from "../config/config-views.js";
8
8
  import { envPath } from "../files/files.js";
9
9
  import { belowNodeFloor } from "../version.js";
10
- import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
10
+ import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
11
11
  import { claudeSeamsInUse, loadConfigForCheck, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
12
12
  /** The doctor's claude credential and version readiness checks, split out of
13
13
  * doctor-model-checks.ts (whose shared seam/model framework and the pi and tier-models
@@ -16,9 +16,24 @@ import { claudeSeamsInUse, loadConfigForCheck, versionFromProbe, seamedModels, }
16
16
  * distinct concern from the filesystem/git checks, kept together so a change to how doctor
17
17
  * spawns or scrubs claude does not touch the other model checks. The shared report contract
18
18
  * (CheckOutcome) and the seam/model framework (loadConfigForCheck, claudeSeamsInUse,
19
- * seamPairs) come from doctor-model-checks.ts, and the AUTH_CHECK_* probe budgets from probe.ts,
20
- * with the shared report contract (CheckOutcome) from outcome.ts, exactly as the other doctor-*
21
- * modules share them. */
19
+ * seamPairs) come from doctor-model-checks.ts, and the AUTH_CHECK_* probe budgets
20
+ * from doctor-probe.ts, with the shared report contract (CheckOutcome) from
21
+ * doctor-outcome.ts, exactly as the other doctor-* modules share them. */
22
+ /** The config a claude readiness row reads, or the warn outcome a broken `tumwater.json`
23
+ * earns, or the ok stand-down when no seam routes to claude. The claude rows repeat the same
24
+ * three-line guard and the same seam stand-down (`loadConfigForCheck` + `claudeSeamsInUse`);
25
+ * folding them here mirrors codexCheckBase so a change to how doctor guards a claude check
26
+ * touches one spot instead of three. Each caller still checks the seams it needs — the auth
27
+ * rows use `claudeSeamsInUse` directly (folded here), the version row goes on to
28
+ * `seamedModels` for the floors. */
29
+ function claudeCheckBase(root) {
30
+ const config = loadConfigForCheck(root);
31
+ if ("level" in config)
32
+ return config;
33
+ if (!claudeSeamsInUse(config))
34
+ return okOutcome("no model jobs run on claude");
35
+ return config;
36
+ }
22
37
  /** One bounded probe of the resolved `claude` binary under the scrubbed run environment a real
23
38
  * run gets: resolve the binary through the claude adapter that would spawn it and run `args`
24
39
  * with a short deadline. Null when the probe could not run, the child errored, or the deadline
@@ -47,6 +62,13 @@ async function claudeAuthProbe(config, pathEnv, timeoutMs) {
47
62
  return null;
48
63
  return parseClaudeAuthStatus(result.stdout);
49
64
  }
65
+ /** The warn verdict both auth-driven claude rows return when `claude auth status` could not
66
+ * run or its reply was unreadable — the one home of the wording so checkClaudeAuth and
67
+ * checkAnthroApiKey cannot drift on the unreadable-probe message. Each caller makes its own
68
+ * solitary `claudeAuthProbe` call and branches on null through it. */
69
+ function claudeAuthUnreadable() {
70
+ return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
71
+ }
50
72
  /** claude credential readiness (plans/agent-backends.md "Every seam, checked"): a claude seam
51
73
  * whose login is gone fails every run before it can work, so doctor runs `claude auth status`
52
74
  * under the scrubbed run environment and fails when it reports `loggedIn: false`. An ok detail
@@ -55,14 +77,12 @@ async function claudeAuthProbe(config, pathEnv, timeoutMs) {
55
77
  * probe that could not run or a reply this cannot read warns rather than guesses. Stands down
56
78
  * (ok) when no seam routes to claude, so a pi-only team spawns nothing. */
57
79
  export async function checkClaudeAuth(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
58
- const config = loadConfigForCheck(root);
80
+ const config = claudeCheckBase(root);
59
81
  if ("level" in config)
60
82
  return config;
61
- if (!claudeSeamsInUse(config))
62
- return okOutcome("no model jobs run on claude");
63
83
  const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
64
84
  if (auth === null)
65
- return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
85
+ return claudeAuthUnreadable();
66
86
  if (!auth.loggedIn)
67
87
  return failOutcome("claude reports logged in: false — run `claude auth login`");
68
88
  const subscription = claudeAuthUsesSubscription(auth);
@@ -80,54 +100,40 @@ export async function checkClaudeAuth(root, pathEnv = envPath(), timeoutMs = AUT
80
100
  * an API-key login (then there is no subscription to lose). `env` is injectable so the check
81
101
  * stays hermetic; a probe that cannot run warns rather than guesses like checkClaudeAuth. */
82
102
  export async function checkAnthroApiKey(root, env = process.env, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
83
- const config = loadConfigForCheck(root);
103
+ const config = claudeCheckBase(root);
84
104
  if ("level" in config)
85
105
  return config;
86
- if (!claudeSeamsInUse(config))
87
- return okOutcome("no model jobs run on claude");
88
106
  const apiKey = env.ANTHROPIC_API_KEY;
89
107
  if (apiKey === undefined || apiKey === "")
90
108
  return okOutcome("no ANTHROPIC_API_KEY set");
91
109
  const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
92
110
  if (auth === null)
93
- return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
111
+ return claudeAuthUnreadable();
94
112
  if (!auth.loggedIn || !claudeAuthUsesSubscription(auth))
95
113
  return okOutcome("ANTHROPIC_API_KEY is set, but claude reports no claude.ai login");
96
114
  return warnOutcome("ANTHROPIC_API_KEY is set while claude reports a claude.ai login — model jobs on claude will bill the API key, not the subscription; unset it or route every anthropic model job on pi explicitly");
97
115
  }
98
- /** claude CLI-version readiness (plans/agent-backends.md finding 9): an installed CLI below a
99
- * configured model's minimum fails or misprices that model, and team runs disable the
100
- * auto-updater, so the version an operator has is the version agent runs use. Fails below the
101
- * highest floor among the claude-routed models, naming which model needs it; an ok detail
102
- * prints the installed version and the floor it satisfied (or no floor when none applies).
103
- * The `off`/`minimal` thinking clamp is noted here too, since this check already has the
104
- * claude seams in hand. Stands down when no seam routes to claude. */
105
- export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
106
- const config = loadConfigForCheck(root);
107
- if ("level" in config)
108
- return config;
109
- const seams = seamedModels(config, "claude");
110
- if (seams.length === 0)
111
- return okOutcome("no model jobs run on claude");
112
- // The floor each configured model needs; a model the adapter does not gate contributes none.
116
+ /** The floor each configured model needs, keyed by model; a model the adapter does not gate
117
+ * contributes none. */
118
+ function claudeVersionFloors(seams) {
113
119
  const floors = new Map();
114
120
  for (const seam of seams) {
115
121
  const min = claudeBackend.minVersion?.(seam.model);
116
122
  if (min !== undefined)
117
123
  floors.set(seam.model, min);
118
124
  }
119
- const result = await claudeProbe(config, ["--version"], pathEnv, timeoutMs);
120
- const found = versionFromProbe(result);
121
- if (found === undefined)
122
- return warnOutcome("could not check — `claude --version` did not run or its output was unreadable");
125
+ return floors;
126
+ }
127
+ /** The clamp note when any claude seam clamps thinking, else empty. */
128
+ function claudeVersionClampNote(seams) {
123
129
  const clamped = seams.some((seam) => seam.thinking === "off" || seam.thinking === "minimal");
124
130
  const clampNote = clamped
125
131
  ? "; `off`/`minimal` thinking clamps to `--effort low` (claude has no lower setting)"
126
132
  : "";
127
- if (floors.size === 0)
128
- return okOutcome(`claude ${found}${clampNote}`);
129
- // The highest floor decides: whichever model needs the newest CLI. Below it, fail and name
130
- // that model; at or above, print the version against the floor.
133
+ return clampNote;
134
+ }
135
+ /** The highest floor decides: whichever model needs the newest CLI. */
136
+ function claudeVersionNeededBy(floors) {
131
137
  let required = "";
132
138
  let neededBy = "";
133
139
  for (const [model, min] of floors) {
@@ -136,7 +142,35 @@ export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs =
136
142
  neededBy = model;
137
143
  }
138
144
  }
145
+ return { required, neededBy };
146
+ }
147
+ /** Verdict on the found version against the claude seams' floors, carrying the clamp note. */
148
+ function claudeVersionOutcome(found, floors, clampNote) {
149
+ if (floors.size === 0)
150
+ return okOutcome(`claude ${found}${clampNote}`);
151
+ const { required, neededBy } = claudeVersionNeededBy(floors);
139
152
  if (belowNodeFloor(found, `>=${required}`))
140
153
  return failOutcome(`claude ${found} is older than ${required} required by ${neededBy} — run \`claude update\``);
141
154
  return okOutcome(`claude ${found} (>= ${required} for ${neededBy})${clampNote}`);
142
155
  }
156
+ /** claude CLI-version readiness (plans/agent-backends.md finding 9): an installed CLI below a
157
+ * configured model's minimum fails or misprices that model, and team runs disable the
158
+ * auto-updater, so the version an operator has is the version agent runs use. Fails below the
159
+ * highest floor among the claude-routed models, naming which model needs it; an ok detail
160
+ * prints the installed version and the floor it satisfied (or no floor when none applies).
161
+ * The `off`/`minimal` thinking clamp is noted here too, since this check already has the
162
+ * claude seams in hand. Stands down when no seam routes to claude. */
163
+ export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
164
+ const config = claudeCheckBase(root);
165
+ if ("level" in config)
166
+ return config;
167
+ const seams = seamedModels(config, "claude");
168
+ if (seams.length === 0)
169
+ return okOutcome("no model jobs run on claude");
170
+ const floors = claudeVersionFloors(seams);
171
+ const result = await claudeProbe(config, ["--version"], pathEnv, timeoutMs);
172
+ const found = versionFromProbe(result);
173
+ if (found === undefined)
174
+ return warnOutcome("could not check — `claude --version` did not run or its output was unreadable");
175
+ return claudeVersionOutcome(found, floors, claudeVersionClampNote(seams));
176
+ }
@@ -1,19 +1,19 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { childEnv } from "../process/process.js";
4
- import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
4
+ import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
5
5
  import { codexBackend, CODEX_MIN_VERSION } from "../agent/codex/codex-backend.js";
6
6
  import { codexEffort, EFFORT_BY_THINKING } from "../agent/codex/codex-args.js";
7
7
  import { codexEnv } from "../agent/codex/codex-env.js";
8
8
  import { codexHome, readCodexCatalog } from "../agent/codex/codex-catalog.js";
9
9
  import { codexHookTrust } from "../agent/codex/codex-hook-trust.js";
10
- import { parseCodexLoginStatus } from "../agent/codex/codex-account.js";
10
+ import { codexApiKeyRefusalMessage, parseCodexLoginStatus } from "../agent/codex/codex-account.js";
11
11
  import { codexWriteGuardDefinition } from "../agent/codex/codex-write-guard.js";
12
12
  import { resolvedBackendConfig } from "../config/config-views.js";
13
13
  import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes, walkFiles } from "../files/files.js";
14
14
  import { formatBytes, tokenWindowLabel } from "../text/format.js";
15
15
  import { errorMessage } from "../text/text.js";
16
- import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
16
+ import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
17
17
  import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
18
18
  /** The doctor's codex rows (part 9a/9, plans/codex-backend.md §4, §7, "Hand steps"): one
19
19
  * exported check per row — install/version, login, home, models, account, leaks, sessions
@@ -25,8 +25,8 @@ import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, versionFromProb
25
25
  * pi-only team shows no codex row and spawns nothing and reads nothing. The shared
26
26
  * report contract (CheckOutcome) and the seam/model framework (loadConfigForCheck,
27
27
  * codexSeamsInUse, seamPairs) come from doctor-model-checks.ts, the AUTH_CHECK_* probe budgets
28
- * from probe.ts, and the shared report contract (CheckOutcome) from outcome.ts, as the other
29
- * doctor-* modules share them. */
28
+ * from doctor-probe.ts, and the shared report contract (CheckOutcome) from
29
+ * doctor-outcome.ts, as the other doctor-* modules share them. */
30
30
  /** The config and codex home a codex readiness row reads, or the warn outcome a broken
31
31
  * `tumwater.json` earns, or null when no seam routes to codex (the row's stand-down). All
32
32
  * eight rows repeat the same three-line guard and the same home lookup
@@ -83,7 +83,7 @@ export async function checkCodexAuth(root, pathEnv = envPath(), timeoutMs = AUTH
83
83
  if (status === "subscription")
84
84
  return okOutcome("logged in using ChatGPT — billing: subscription");
85
85
  if (status === "api")
86
- return failOutcome(`Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run \`CODEX_HOME=${home} codex login --device-auth\``);
86
+ return failOutcome(codexApiKeyRefusalMessage(home));
87
87
  if (/Not logged in/i.test(result.stdout ?? ""))
88
88
  return failOutcome(`codex is not logged in — run \`CODEX_HOME=${home} codex login --device-auth\``);
89
89
  return warnOutcome("could not check — `codex login status` reply was unreadable");
@@ -18,24 +18,20 @@ import { finiteNumber, stringList } from "../files/json-object.js";
18
18
  import { commitExists, deleteRefIfAt, listBranches, refSha, refsUnder } from "../git/git.js";
19
19
  import { readSlotsState } from "../git/slots-state.js";
20
20
  import { eventsSince } from "../events/event-window.js";
21
- import { DAY_MS } from "../text/datetime.js";
21
+ import { DAY_MS, HOUR_MS } from "../text/datetime.js";
22
22
  import { shortSha } from "../text/format.js";
23
23
  import { errorMessage, joinParts, textOr } from "../text/text.js";
24
24
  import { rankCountEntries } from "../collections.js";
25
25
  import { DIRECTOR_ROLE } from "../roles/roles.js";
26
+ import { sampleHostLoad } from "../build/host-sleep.js";
26
27
  import { INSTANCE_ROLES, runnerEnabled, runnerIds } from "../roles/runner-ids.js";
27
28
  import { staleClaimDetail, staleClaimEntries } from "../scheduling/claim-entries.js";
28
- import { piModelsPath } from "../pi/pi-models.js";
29
- import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
30
- import { seamConcurrencyKey } from "../config/config-views.js";
31
- import { seamsInUse } from "../config/config-fallbacks.js";
32
- import { enforcedKeyCaps } from "../concurrency/semaphore.js";
33
29
  import { classifyLock, readLockPid } from "../concurrency/lock.js";
34
30
  import { branchName, mergeLockDir, worktreesDir } from "../paths.js";
35
- import { artifactPhrase, retireRole } from "../operator/retire.js";
31
+ import { artifactPhrase, retireRole } from "../operator/operator-retire.js";
36
32
  import { loadRunnerState } from "../runner/runner-state-load.js";
37
33
  import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
38
- import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
34
+ import { failOutcome, noConfigOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
39
35
  /** Merge lock — read-only classification via classifyLock (the same three cases the breaker
40
36
  * uses). A stale lock is a warning, not a failure: it self-heals on the next merge. */
41
37
  export function checkMergeLock(root) {
@@ -75,8 +71,8 @@ export function staleWorkspacePins(root, now = Date.now()) {
75
71
  * the pin's whole-ms age). `role` is a non-optional string on StaleWorkspacePin, but the
76
72
  * clause renders it through textOr like the row always did, so a malformed pin can never
77
73
  * print nothing where the phrase expects a role. */
78
- export function stalePinPhrase(pin) {
79
- return `${textOr(pin.role)} pinned on ${pin.dir} for ${Math.round(pin.ageMs / 3_600_000)}h`;
74
+ function stalePinPhrase(pin) {
75
+ return `${textOr(pin.role)} pinned on ${pin.dir} for ${Math.round(pin.ageMs / HOUR_MS)}h`;
80
76
  }
81
77
  /** Directory names under the worktrees root that the pool no longer owns: a legacy per-role
82
78
  * `<role>` checkout (the pre-pool layout) or a `_land-<role>` checkout from the merge queue. The
@@ -86,11 +82,11 @@ function leftoverWorktreeDirs(root) {
86
82
  return subdirectoryNames(worktreesDir(root), (name) => name !== DIRECTOR_ROLE && !(name.startsWith("_") && !name.startsWith("_land-")));
87
83
  }
88
84
  /** The window of `slot_wait` events the pool row sums, and the total wait inside it that warns. */
89
- export const SLOT_WAIT_CHECK_WINDOW_MS = DAY_MS;
85
+ const SLOT_WAIT_CHECK_WINDOW_MS = DAY_MS;
90
86
  const SLOT_WAIT_WARN_TOTAL_MS = 10 * 60_000;
91
- export const PROVIDER_WAIT_CHECK_WINDOW_MS = DAY_MS;
87
+ const PROVIDER_WAIT_CHECK_WINDOW_MS = DAY_MS;
92
88
  const PROVIDER_WAIT_WARN_TOTAL_MS = 10 * 60_000;
93
- export const CHECK_WAIT_CHECK_WINDOW_MS = DAY_MS;
89
+ const CHECK_WAIT_CHECK_WINDOW_MS = DAY_MS;
94
90
  const CHECK_WAIT_WARN_TOTAL_MS = 10 * 60_000;
95
91
  /** Fold a window's `*_wait` events into the three numbers every contention check reads: how
96
92
  * many waits there were (each event counts once), the total ms they waited, and the longest
@@ -116,26 +112,34 @@ function sumWaits(waits) {
116
112
  }
117
113
  return { count, totalMs, longestMs, longest };
118
114
  }
115
+ /** The leading wait-summary clause every `*_wait` contention row shares — the wait count, the
116
+ * total and the longest span over the trailing day. Only the verb naming the waited-for thing
117
+ * differs between the three rows ("to start", "for a provider slot", "for a check slot"), so
118
+ * this single home keeps the shared wording — the same `plural("wait")`/`shortSpanPhrase`
119
+ * bucketing — from drifting between them. Each row appends its own bucket/knob tail after it. */
120
+ function waitDetail(count, verb, totalMs, longestMs) {
121
+ return `${plural(count, "wait")} ${verb} in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)}`;
122
+ }
119
123
  /** The `slot_wait` half of checkWorktreePool's rule: whether the window's waits cross
120
124
  * SLOT_WAIT_WARN_TOTAL_MS and the pool row's exact detail clause — the wait count, total and
121
125
  * longest spans (naming the role and whether it was a check or a run), and, on a warn, the
122
126
  * knob to raise past `slotCount`. Shared by the doctor's `workspaces` row and the always-on
123
127
  * contention alert, so the trigger and the advice cannot drift. */
124
- export function slotWaitContention(waits, slotCount) {
128
+ function slotWaitContention(waits, slotCount) {
125
129
  const { count, totalMs, longestMs, longest } = sumWaits(waits);
126
130
  const contended = totalMs >= SLOT_WAIT_WARN_TOTAL_MS;
127
131
  const purpose = longest.purpose === "verify" ? "check" : "run";
128
- const detail = `${plural(count, "wait")} to start in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)} (${textOr(longest.role)} ${purpose})${contended ? ` — raise worktreeSlots above ${slotCount}` : ""}`;
132
+ const detail = `${waitDetail(count, "to start", totalMs, longestMs)} (${textOr(longest.role)} ${purpose})${contended ? ` — raise worktreeSlots above ${slotCount}` : ""}`;
129
133
  return { warn: contended, detail };
130
134
  }
131
135
  /** Provider-slot contention's rule (the body of checkProviderContention from `sumWaits` on):
132
136
  * sums the waits, ranks the per-key buckets busiest first (the key-less bucket, which also
133
137
  * takes every team-cap wait whatever its key, reads "the run cap") and, on a warn past
134
- * PROVIDER_WAIT_WARN_TOTAL_MS, names each key's maxConcurrentPerProvider cap to raise plus
135
- * maxConcurrent for the run cap. Shared by the doctor's `provider contention` row and the
136
- * always-on contention alert, so the trigger and the advice cannot drift. The shared rule
137
- * leaves the count-zero and read-error arms to checkProviderContention. */
138
- export function providerWaitContention(waits, config) {
138
+ * PROVIDER_WAIT_WARN_TOTAL_MS, names `maxConcurrent` to raise. Shared by the doctor's
139
+ * `provider contention` row and the always-on contention alert, so the trigger and the advice
140
+ * cannot drift. The shared rule leaves the count-zero and read-error arms to
141
+ * checkProviderContention. */
142
+ function providerWaitContention(waits, config) {
139
143
  const perKey = new Map();
140
144
  const { count, totalMs, longestMs } = sumWaits(waits);
141
145
  for (const ev of waits) {
@@ -154,17 +158,13 @@ export function providerWaitContention(waits, config) {
154
158
  const parenthetical = ranked.length === 1
155
159
  ? `(${bucketLabel(ranked[0].key)})`
156
160
  : `(${cells[0]}, also ${cells.slice(1).join(", ")})`;
157
- let detail = `${plural(count, "wait")} for a provider slot in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)} ${parenthetical}`;
161
+ let detail = `${waitDetail(count, "for a provider slot", totalMs, longestMs)} ${parenthetical}`;
158
162
  const contended = totalMs >= PROVIDER_WAIT_WARN_TOTAL_MS;
159
- if (contended) {
160
- const advice = ranked
161
- .filter((b) => b.key !== "" && config?.maxConcurrentPerProvider?.[b.key] !== undefined)
162
- .map((b) => `raise maxConcurrentPerProvider.${b.key} above ${config?.maxConcurrentPerProvider?.[b.key]}`)
163
- .join(" and ");
164
- const teamAdvice = perKey.has("") && config !== undefined ? [`raise maxConcurrent above ${config.maxConcurrent}`] : [];
165
- const all = [...(advice ? [advice] : []), ...teamAdvice].join(" and ");
166
- if (all)
167
- detail += ` — ${all}`;
163
+ // The one configurable bound is the team total, so a warn names `maxConcurrent` to raise —
164
+ // but only for a team-cap wait, the only kind that bound can affect (a keyed-only wait from
165
+ // an old event shape has no knob to name).
166
+ if (contended && config !== undefined && perKey.has("")) {
167
+ detail += ` — raise maxConcurrent above ${config.maxConcurrent}`;
168
168
  }
169
169
  return { warn: contended, detail };
170
170
  }
@@ -174,7 +174,7 @@ export function providerWaitContention(waits, config) {
174
174
  * busiest first. Shared by the doctor's `check contention` row and the always-on contention
175
175
  * alert, so the trigger and the advice cannot drift. The shared rule leaves the count-zero
176
176
  * and read-error arms to checkCheckContention. */
177
- export function checkWaitContention(waits, config) {
177
+ function checkWaitContention(waits, config, load) {
178
178
  const perKind = new Map();
179
179
  const { count, totalMs, longestMs } = sumWaits(waits);
180
180
  for (const ev of waits) {
@@ -183,21 +183,31 @@ export function checkWaitContention(waits, config) {
183
183
  perKind.set(kind, (perKind.get(kind) ?? 0) + waited);
184
184
  }
185
185
  }
186
- let detail = `${plural(count, "wait")} for a check slot in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)}`;
186
+ let detail = waitDetail(count, "for a check slot", totalMs, longestMs);
187
187
  if (perKind.size > 0) {
188
188
  const ranked = rankCountEntries(perKind);
189
189
  detail += ` (behind ${ranked.map(([kind, ms]) => `${kind} ${shortSpanPhrase(ms)}`).join(", ")})`;
190
190
  }
191
191
  if (totalMs < CHECK_WAIT_WARN_TOTAL_MS)
192
192
  return { warn: false, detail };
193
- detail += ` — raise maxConcurrentChecks above ${config?.maxConcurrentChecks ?? defaultConfig().maxConcurrentChecks}`;
193
+ // The cap is only the right lever while the host can actually run more work: once the
194
+ // one-minute load average exceeds the core count (the same "starved" bar build-check-events
195
+ // uses for an on-time deadline kill), the wait is the host's own load, not an under-sized
196
+ // pool — so name the load and suppress the raise advice rather than telling an overloaded
197
+ // host to admit still more concurrent checks. The check still warns: the waits are real.
198
+ if (load.load1 > load.cpus) {
199
+ detail += ` — host load average ${Math.round(load.load1)} on ${load.cpus} cores: saturated, so raising maxConcurrentChecks is not the lever`;
200
+ }
201
+ else {
202
+ detail += ` — raise maxConcurrentChecks above ${config?.maxConcurrentChecks ?? defaultConfig().maxConcurrentChecks}`;
203
+ }
194
204
  return { warn: true, detail };
195
205
  }
196
206
  /** Worktree pool (plans/worktree-pool.md, part 5/5): report the pooled slots and warn on the
197
207
  * states an operator should act on — a legacy `<role>` or `_land-*` checkout the pool migration
198
208
  * left behind, a pin older than PIN_STALE_MS (a paused role holding a checkout), and the last
199
- * 24 h of `slot_wait` events totalling SLOT_WAIT_WARN_TOTAL_MS (the pool is too small). The stale
200
- * pin is also the standing `workspace-pins` alert's selection (staleWorkspacePins). Read-only,
209
+ * 24 h of `slot_wait` events totalling SLOT_WAIT_WARN_TOTAL_MS (the pool is too small). A stale
210
+ * pin is named by this row's stale-pins clause. Read-only,
201
211
  * like every doctor check; `now` is a test seam for the pin-age arithmetic. */
202
212
  export function checkWorktreePool(root, now = Date.now()) {
203
213
  const slots = readSlotsState(root).slots;
@@ -209,8 +219,8 @@ export function checkWorktreePool(root, now = Date.now()) {
209
219
  if (leftover.length > 0)
210
220
  parts.push(`legacy worktrees still present: ${leftover.join(", ")}`);
211
221
  if (stale.length > 0) {
212
- // The stale-pin clause renders the shared staleWorkspacePins selection (the same list the
213
- // standing `workspace-pins` alert carries), so the row's wording and the alert's selection
222
+ // The stale-pin clause renders the shared staleWorkspacePins selection (the same list this
223
+ // row's detail words), so the row's wording and the shared selection cannot drift.
214
224
  // cannot disagree on what a stale pin is or how old it is.
215
225
  parts.push(stale.map(stalePinPhrase).join("; "));
216
226
  }
@@ -236,13 +246,13 @@ export function checkWorktreePool(root, now = Date.now()) {
236
246
  * ok-fallback arms the two siblings repeated, so they cannot drift on which inputs read ok;
237
247
  * the row-specific rule (`providerWaitContention` / `checkWaitContention`) is `rule`. `noun`
238
248
  * names the waits in the ok message. */
239
- function waitContentionRow(root, windowMs, type, noun, rule, config, now) {
249
+ function waitContentionRow(root, windowMs, type, noun, rule, config, now, load = sampleHostLoad) {
240
250
  let count = 0;
241
251
  let verdict;
242
252
  try {
243
253
  const waits = eventsSince(root, windowMs, now, (ev) => ev.type === type);
244
254
  count = sumWaits(waits).count;
245
- verdict = rule(waits, config);
255
+ verdict = rule(waits, config, load());
246
256
  }
247
257
  catch (err) {
248
258
  // A read error leaves the level ok, exactly like the slot-wait branch's own read: the
@@ -258,7 +268,7 @@ function waitContentionRow(root, windowMs, type, noun, rule, config, now) {
258
268
  * orchestrator-launch.ts) and warns when the total reaches PROVIDER_WAIT_WARN_TOTAL_MS — the
259
269
  * provider cap is too small. Names every key that waited, busiest first (the key-less
260
270
  * bucket, which also takes every team-cap wait whatever its key, reads "the run cap") and, on
261
- * a warn, each key's maxConcurrentPerProvider cap to raise, plus maxConcurrent for the run cap.
271
+ * a warn, names `maxConcurrent` for the run cap.
262
272
  * A counterpoint to checkWorktreePool's slot-wait half; read-only, like every doctor check.
263
273
  * `now` is a test seam for the window. */
264
274
  export function checkProviderContention(root, config, now = Date.now()) {
@@ -279,8 +289,8 @@ function checkHolderKind(label) {
279
289
  * detail gains `(behind <kind> <span>, …)` after the longest clause, kinds busiest first: a
280
290
  * wait counts once toward each distinct kind that held a slot. `now` is a test seam for the
281
291
  * window. */
282
- export function checkCheckContention(root, config, now = Date.now()) {
283
- return waitContentionRow(root, CHECK_WAIT_CHECK_WINDOW_MS, "check_wait", "check waits", checkWaitContention, config, now);
292
+ export function checkCheckContention(root, config, now = Date.now(), load = sampleHostLoad) {
293
+ return waitContentionRow(root, CHECK_WAIT_CHECK_WINDOW_MS, "check_wait", "check waits", checkWaitContention, config, now, load);
284
294
  }
285
295
  /** The ids `leftover role branches` calls retirable and the branches it leaves for manual
286
296
  * deletion — the one walk the row and the reap share, so the two can never disagree on what a
@@ -317,7 +327,7 @@ export async function retirableLeftoverRoles(root, config) {
317
327
  * an unreadable tumwater.json skips it. */
318
328
  export async function checkLeftoverRoleBranches(root, config) {
319
329
  if (!config)
320
- return okOutcome("no readable tumwater.json — nothing to verify");
330
+ return noConfigOutcome();
321
331
  const { retirable, unknown } = await retirableLeftoverRoles(root, config);
322
332
  const parts = [];
323
333
  if (retirable.length > 0) {
@@ -440,7 +450,7 @@ function claimCounts(claims) {
440
450
  * tumwater.json (config undefined) skips the walk — checkInit reports the config itself. */
441
451
  export function checkWorkInstances(root, config, now = Date.now()) {
442
452
  if (!config)
443
- return okOutcome("no readable tumwater.json — nothing to verify");
453
+ return noConfigOutcome();
444
454
  const claims = runnerIds(config).map((id) => loadRunnerState(root, id).claim).filter((c) => c !== undefined);
445
455
  const entries = staleClaimEntries(root, runnerIds(config), now);
446
456
  const gone = entries.filter((e) => e.kind === "gone");
@@ -453,13 +463,11 @@ export function checkWorkInstances(root, config, now = Date.now()) {
453
463
  return warnOutcome(parts.join("; "));
454
464
  }
455
465
  /** An enabled multi-instance role contending for a worktree pool smaller than the run cap —
456
- * the pool-mismatch clause of the doctor's `parallel instances` row (checkWorkInstances) and
457
- * the always-on `instance-pool` alert (alert-feeds.ts instancePoolProblems). Returns the
458
- * `INSTANCE_ROLES` ids with more than one configured instance while `slotCount(config)`
466
+ * the pool-mismatch clause of the doctor's `parallel instances` row (checkWorkInstances):
467
+ * the `INSTANCE_ROLES` ids with more than one configured instance while `slotCount(config)`
459
468
  * (worktreeSlots, else maxConcurrent + 1) is below `config.maxConcurrent`, phrased as the
460
469
  * row's one clause; `[]` when no such role or the pool is at least `maxConcurrent`. Shared by
461
- * the doctor row and the alert so they cannot drift on the trigger, the role list, or the
462
- * wording. */
470
+ * the row and its clause so they cannot drift on the trigger, the role list, or the wording. */
463
471
  export function smallPoolProblems(config) {
464
472
  const multi = Object.entries(config.roles)
465
473
  .filter(([id, role]) => INSTANCE_ROLES.has(id) && role.enabled && (role.instances ?? 1) > 1)
@@ -469,71 +477,3 @@ export function smallPoolProblems(config) {
469
477
  return [];
470
478
  return [`${multi.join(", ")} run instances but worktreeSlots ${slots} is below maxConcurrent ${config.maxConcurrent}`];
471
479
  }
472
- /** Provider concurrency caps (plans/provider-concurrency.md §8): audit the
473
- * `maxConcurrentPerProvider` map against the seams the config can actually spawn — each role,
474
- * the review gate, the resolver, the director, and each tier's fallback — so the operator
475
- * sees whether every cap names a provider in use and whether any cap actually binds. Warns on
476
- * a cap key no seam resolves to (a typo, or a provider not in use), a cap at or above
477
- * `maxConcurrent` (which never binds — the team total is the real ceiling), and a capped
478
- * team whose seams include one with no key (pi's default provider is unreadable, so that
479
- * seam cannot be capped). A config with no per-provider map or no caps is fine, and so is a
480
- * well-formed map; a broken config is reported by the init check. Takes `root` to read pi's
481
- * own default model (piSettingsPath), the same seam the orchestrator's admission uses, so a
482
- * bare no-model pi seam keys exactly as the team would key it. */
483
- /** The warnings behind checkProviderConcurrency, as a pure function of the config and pi's
484
- * default model name: a cap key no seam resolves to (a typo, or a provider not in use), a cap
485
- * at or above `maxConcurrent` (which never binds), and a capped team whose seams include one
486
- * with no key (pi's default provider is unreadable, so that seam cannot be capped). The doctor's
487
- * `provider concurrency` row and the always-on `provider-caps` team alert both read this one
488
- * computation, so the row and the alert cannot drift on the trigger or the wording. `[]` for no
489
- * config, a map with no enforcedKeyCaps entries, or a map with no problem. `piDefault` is the
490
- * bare default model the orchestrator's admission seam carries, read once by the callers
491
- * through readPiDefaultModel(piSettingsPath(piModelsPath()), root). */
492
- export function providerConcurrencyProblems(config, piDefault) {
493
- if (!config)
494
- return [];
495
- const caps = config.maxConcurrentPerProvider;
496
- const capEntries = enforcedKeyCaps(caps);
497
- if (capEntries.length === 0)
498
- return [];
499
- const seams = seamsInUse(config);
500
- const keyed = [];
501
- for (const s of seams) {
502
- const k = seamConcurrencyKey(s, piDefault);
503
- if (k !== undefined && !keyed.includes(k))
504
- keyed.push(k);
505
- }
506
- const keySet = new Set(keyed);
507
- const capKeys = capEntries.map(([k]) => k);
508
- const warnings = [];
509
- for (const key of capKeys) {
510
- if (!keySet.has(key))
511
- warnings.push(`cap key "${key}" resolves to no model job (a typo, or a provider not in use)`);
512
- }
513
- for (const [key, cap] of capEntries) {
514
- if (cap >= config.maxConcurrent)
515
- warnings.push(`cap "${key}" ${cap} is at or above maxConcurrent ${config.maxConcurrent} — it never binds`);
516
- }
517
- const capSet = new Set(capKeys);
518
- const keyless = seams.filter((s) => seamConcurrencyKey(s, piDefault) === undefined);
519
- if (capSet.size > 0 && keyless.length > 0) {
520
- warnings.push(`${plural(keyless.length, "model job")} ${agree(keyless.length, "resolves", "resolve")} to no provider key (pi's default provider is unreadable, so they cannot be capped): ${keyless.map((s) => s.label).join(", ")}`);
521
- }
522
- return warnings;
523
- }
524
- export function checkProviderConcurrency(config, root) {
525
- if (!config)
526
- return okOutcome("no config to audit");
527
- const caps = config.maxConcurrentPerProvider;
528
- const capEntries = enforcedKeyCaps(caps);
529
- if (capEntries.length === 0)
530
- return okOutcome("no per-provider caps configured");
531
- // The pi default model the orchestrator's admission seam carries: a bare no-model pi seam
532
- // resolves its key only through it, so a team whose default is readable has keyed seams and
533
- // only an unreadable default leaves a key-less seam to warn about.
534
- const piDefault = readPiDefaultModel(piSettingsPath(piModelsPath()), root);
535
- const problems = providerConcurrencyProblems(config, piDefault);
536
- if (problems.length === 0)
537
- return okOutcome(`${plural(capEntries.length, "provider cap")} — all model jobs resolve`);
538
- return warnOutcome(problems.join("; "));
539
- }