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
@@ -2,12 +2,14 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { worktreesDir } from "../paths.js";
4
4
  import { pidAlive } from "../process/process.js";
5
+ import { rootToken } from "../process/run-marker.js";
5
6
  import { GROUP_POLL_MS, KILL_GRACE_MS } from "../process/process-group.js";
6
7
  import { systemProcessProbe } from "../process/process-table.js";
7
8
  import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
8
9
  import { errorMessage, truncate } from "../text/text.js";
9
10
  import { plural } from "../text/phrases.js";
10
- import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
11
+ import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
12
+ import { distinctOrdered } from "../collections.js";
11
13
  /** The orphan-process check of `tumwater doctor`, split out of doctor.ts: it is the one
12
14
  * doctor check that inspects the operating system's process table (argv, cwd, parent-child
13
15
  * tree) rather than the repo's own files, so it carries a different shape of logic — path
@@ -92,6 +94,36 @@ function harnessGone(marker) {
92
94
  const m = /^(\d+)-/.exec(marker);
93
95
  return m ? !pidAlive(Number(m[1])) : null;
94
96
  }
97
+ /** The repo-identity token in a run marker's third dash segment — the team root tag
98
+ * `makeRunMarker` stamps when it knows one — or null for a rootless two-segment mark
99
+ * (`<pid>-<hex>`, or a torn or foreign value with no third segment): there is no evidence of
100
+ * which checkout's harness minted it. */
101
+ export function markRootToken(mark) {
102
+ const third = mark.split("-")[2];
103
+ return third === undefined ? null : third;
104
+ }
105
+ /** The repo-identity token `makeRunMarker` stamps into a team-rooted mark — the same `rootToken`
106
+ * run-marker.ts mints, re-exported here so the orphan scan can spell this root's side of the
107
+ * comparison without importing process/run-marker. */
108
+ export { rootToken };
109
+ /** ps's elapsed-time field (`[[D-]HH:]MM:SS`, the string the doctor line already prints) in
110
+ * milliseconds, the minimum-age gate the automatic reap uses so a just-leaked — still
111
+ * possibly owned — process is not killed. Any shape that does not parse, or that carries an
112
+ * out-of-range minute or second, reads 0: the conservative reading that keeps the reap from
113
+ * acting on uncertain evidence. */
114
+ export function parseEtimeMs(etime) {
115
+ const m = /^(?:\d+-)?(?:(\d+):)?(\d{1,2}):(\d{2})$/.exec(etime.trim());
116
+ if (!m)
117
+ return 0;
118
+ const hh = Number(m[1] ?? 0);
119
+ const mm = Number(m[2]);
120
+ const ss = Number(m[3]);
121
+ if (mm > 59 || ss > 59)
122
+ return 0;
123
+ const days = /^(\d+)-/.exec(etime.trim());
124
+ const d = days ? Number(days[1]) : 0;
125
+ return ((d * 24 + hh) * 3600 + mm * 60 + ss) * 1000;
126
+ }
95
127
  /** A parent pid → child pids map of the table. */
96
128
  function childrenOf(rows) {
97
129
  const children = new Map();
@@ -111,8 +143,10 @@ function shownCommand(command, root) {
111
143
  return truncate(cut.reduce((c, rt) => c.split(`${rt}/`).join(""), command), ORPHAN_COMMAND_MAX);
112
144
  }
113
145
  /** The detection half of checkOrphans, shared with killOrphans so the check and the kill cannot
114
- * disagree about the set. */
115
- async function findOrphans(root, probe) {
146
+ * disagree about the set. Exported so the always-on orphan alert feed (src/ui/alert-feeds.ts)
147
+ * runs the exact same scan the doctor row reads — the alert and the row cannot drift on the
148
+ * orphan set. */
149
+ export async function findOrphans(root, probe) {
116
150
  const roots = rootSpellings(root);
117
151
  let rows;
118
152
  try {
@@ -148,12 +182,24 @@ async function findOrphans(root, probe) {
148
182
  // Best-effort per the probe contract: argv and cwd still carry the check alone.
149
183
  }
150
184
  }
185
+ const ours = rootToken(root);
151
186
  const byMark = new Set();
152
187
  for (const r of parentless) {
153
188
  if (byArgv.has(r.pid))
154
189
  continue;
155
- const verdicts = (marks.get(r.pid) ?? []).map(harnessGone);
156
- if (verdicts.length > 0 && verdicts.every((v) => v === true))
190
+ const rMarks = marks.get(r.pid) ?? [];
191
+ const verdicts = rMarks.map(harnessGone);
192
+ // A dead-marked process is this checkout's only when no mark names a different root: a
193
+ // token that is present and not `ours` proves another checkout's harness stamped that
194
+ // process, and signalling it would break the "never signal a process not attributable"
195
+ // rule (BUGS.md, "Reap the doctor's orphans, part 1/3"). A rootless mark keeps today's
196
+ // report — whatever started it, nothing else will reap it.
197
+ if (verdicts.length > 0 &&
198
+ verdicts.every((v) => v === true) &&
199
+ rMarks.every((m) => {
200
+ const t = markRootToken(m);
201
+ return t === null || t === ours;
202
+ }))
157
203
  byMark.add(r.pid);
158
204
  }
159
205
  const orphans = parentless.filter((r) => {
@@ -199,16 +245,17 @@ async function findOrphans(root, probe) {
199
245
  * never a crashed doctor; unreadable cwds degrade to the argv match and say so. Any orphan is
200
246
  * a fail — the exit code is the point: a scripted doctor must notice. `probe` is injectable so
201
247
  * tests run against a fake table. */
202
- export async function checkOrphans(root, probe = systemProcessProbe) {
203
- const scan = await findOrphans(root, probe);
248
+ /** The orphan-problem sentence for the standing alert feed, shared with checkOrphans so the
249
+ * alert and the doctor row cannot drift on the wording: the count, each listed orphan's pid,
250
+ * age, cpu and command (with its descendant count and the run mark that caught it), and the
251
+ * nothing-reaps-these remediation. null when the scan errored or found no orphans. Public so
252
+ * the feed (src/ui/alert-feeds.ts) can stage a scan and get exactly the row's message. */
253
+ export function orphanFailDetail(root, scan) {
204
254
  if ("error" in scan)
205
- return warnOutcome(`could not scan the process table — ${scan.error}`);
255
+ return null;
206
256
  const { rows, orphans, marks, cwdProblem } = scan;
207
- if (orphans.length === 0) {
208
- if (cwdProblem !== null)
209
- return warnOutcome(`none named in argv, but process cwds are unreadable (${cwdProblem}) — an orphan started inside a worktree, like a leaked test runner, would be missed`);
210
- return okOutcome("none — no process reparented to PID 1 runs from .tumwater/worktrees/ or carries only dead runs' marks");
211
- }
257
+ if (orphans.length === 0)
258
+ return null;
212
259
  const children = childrenOf(rows);
213
260
  const listed = orphans.slice(0, ORPHANS_LISTED_MAX).map((r) => {
214
261
  const n = descendantPids(children, r.pid).length;
@@ -221,9 +268,22 @@ export async function checkOrphans(root, probe = systemProcessProbe) {
221
268
  });
222
269
  const more = orphans.length - listed.length;
223
270
  const count = `${plural(orphans.length, "orphaned process", "orphaned processes")} (PPID 1)`;
224
- return failOutcome(`${count}: ${listed.join("; ")}${more > 0 ? `; and ${more} more` : ""}` +
271
+ return `${count}: ${listed.join("; ")}${more > 0 ? `; and ${more} more` : ""}` +
225
272
  " — nothing reaps these; `tumwater doctor --kill-orphans` (or the dashboard Health card's Kill orphans, or the TUI Health tab's Ctrl+K pressed twice) kills each with its descendants" +
226
- (cwdProblem !== null ? ` (process cwds unreadable — ${cwdProblem}; argv matched only)` : ""));
273
+ (cwdProblem !== null ? ` (process cwds unreadable — ${cwdProblem}; argv matched only)` : "");
274
+ }
275
+ /** The doctor's `orphans` row. */
276
+ export async function checkOrphans(root, probe = systemProcessProbe) {
277
+ const scan = await findOrphans(root, probe);
278
+ if ("error" in scan)
279
+ return warnOutcome(`could not scan the process table — ${scan.error}`);
280
+ const { orphans, cwdProblem } = scan;
281
+ if (orphans.length === 0) {
282
+ if (cwdProblem !== null)
283
+ return warnOutcome(`none named in argv, but process cwds are unreadable (${cwdProblem}) — an orphan started inside a worktree, like a leaked test runner, would be missed`);
284
+ return okOutcome("none — no process reparented to PID 1 runs from .tumwater/worktrees/ or carries only dead runs' marks");
285
+ }
286
+ return failOutcome(orphanFailDetail(root, scan));
227
287
  }
228
288
  const systemSignals = {
229
289
  kill: (pid, signal) => void process.kill(pid, signal),
@@ -271,7 +331,7 @@ async function killTrees(root, rows, chosen, signals) {
271
331
  }
272
332
  };
273
333
  const trees = chosen.map((r) => ({ r, pids: [r.pid, ...descendantPids(children, r.pid)] }));
274
- const all = [...new Set(trees.flatMap((t) => t.pids))];
334
+ const all = distinctOrdered(trees.flatMap((t) => t.pids));
275
335
  for (const pid of all)
276
336
  send(pid, "SIGTERM");
277
337
  const stillAlive = () => all.filter((pid) => !refused.has(pid) && signals.alive(pid));
@@ -291,6 +351,20 @@ async function killTrees(root, rows, chosen, signals) {
291
351
  }
292
352
  return { killed, failed };
293
353
  }
354
+ /** The `pid N (+N descendant) command` line per killed tree and the `could not kill` line per
355
+ * refused tree, in the read the reapers' best-effort warning logs — the one shape both
356
+ * reapPreviousTeamOrphans and reapAttributableOrphans report, so the two cannot drift on the
357
+ * itemized wording. Callers join `[...items, ...refused]` with "; " behind their own verb
358
+ * phrase. */
359
+ function orphanKillReportLines(killed, failed) {
360
+ return {
361
+ items: killed.map((k) => {
362
+ const tree = k.descendants > 0 ? ` (+${plural(k.descendants, "descendant")})` : "";
363
+ return `pid ${k.pid}${tree} ${k.command}`;
364
+ }),
365
+ refused: failed.map((f) => `could not kill pid ${f.pid}: ${f.reason}`),
366
+ };
367
+ }
294
368
  /** Team start's reap of a crashed predecessor's leaks: kill the parentless processes whose run
295
369
  * marks all name dead harnesses and at least one names `previousPid`, the orchestrator the
296
370
  * info file recorded. Safe without an operator's judgment: a mark proves a tumwater start
@@ -314,11 +388,7 @@ export async function reapPreviousTeamOrphans(root, previousPid, probe = systemP
314
388
  if (chosen.length === 0)
315
389
  return 0;
316
390
  const { killed, failed } = await killTrees(root, scan.rows, chosen, signals);
317
- const items = killed.map((k) => {
318
- const tree = k.descendants > 0 ? ` (+${plural(k.descendants, "descendant")})` : "";
319
- return `pid ${k.pid}${tree} ${k.command}`;
320
- });
321
- const refused = failed.map((f) => `could not kill pid ${f.pid}: ${f.reason}`);
391
+ const { items, refused } = orphanKillReportLines(killed, failed);
322
392
  warnEventBestEffort(root, TUMWATER_SOURCE, `killed ${plural(killed.length, "orphaned process", "orphaned processes")} left by tumwater ` +
323
393
  `${previousPid}'s runs at team start: ${[...items, ...refused].join("; ")}`);
324
394
  return killed.length;
@@ -327,3 +397,34 @@ export async function reapPreviousTeamOrphans(root, previousPid, probe = systemP
327
397
  return 0;
328
398
  }
329
399
  }
400
+ /** How long an orphan must have been running before the automatic reap may kill it: a young
401
+ * orphan may still be a live run's process that findOrphans read as parentless mid-hand-off,
402
+ * and ten minutes is far longer than any such attribution would hold. The operator's
403
+ * `--kill-orphans`/dashboard/TUI click bypasses this — doctors judge with their eyes. */
404
+ const ORPHAN_REAP_MIN_AGE_MS = 10 * 60_000;
405
+ /** The orchestrator's once-per-interval sweep of the orphans `checkOrphans` already reports:
406
+ * kill the same set the operator's `--kill-orphans` kills, once each is old enough that no
407
+ * live run can still own it. Reuses findOrphans' attribution and killTrees' signal-and-wait,
408
+ * so the automatic reap cannot drift from the doctor row or the operator kill and never
409
+ * signals a process findOrphans does not list. An unreadable table, or no old-enough orphan,
410
+ * kills nothing and logs nothing; it never throws. Returns the number of orphan trees killed. */
411
+ export async function reapAttributableOrphans(root, probe = systemProcessProbe, signals = systemSignals) {
412
+ try {
413
+ const scan = await findOrphans(root, probe);
414
+ if ("error" in scan)
415
+ return 0;
416
+ const chosen = scan.orphans.filter((r) => parseEtimeMs(r.etime) >= ORPHAN_REAP_MIN_AGE_MS);
417
+ if (chosen.length === 0)
418
+ return 0;
419
+ const { killed, failed } = await killTrees(root, scan.rows, chosen, signals);
420
+ if (killed.length === 0)
421
+ return 0;
422
+ const { items, refused } = orphanKillReportLines(killed, failed);
423
+ warnEventBestEffort(root, TUMWATER_SOURCE, `reaped ${plural(killed.length, "orphaned process", "orphaned processes")}: ` +
424
+ [...items, ...refused].join("; "));
425
+ return killed.length;
426
+ }
427
+ catch {
428
+ return 0;
429
+ }
430
+ }
@@ -9,6 +9,13 @@
9
9
  export function okOutcome(detail) {
10
10
  return { level: "ok", detail };
11
11
  }
12
+ /** The doctor's stand-down on a missing or unreadable tumwater.json: an ok outcome saying
13
+ * there is nothing to verify, because the config check reports the broken file itself. Shared
14
+ * by the four checks that skip their walk when config is unavailable, so the message reads
15
+ * identically across them. */
16
+ export function noConfigOutcome() {
17
+ return okOutcome("no readable tumwater.json — nothing to verify");
18
+ }
12
19
  export function warnOutcome(detail) {
13
20
  return { level: "warn", detail };
14
21
  }
@@ -1,5 +1,5 @@
1
1
  import { childEnv } from "../process/process.js";
2
- import { boundedProbe, AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS } from "./probe.js";
2
+ import { boundedProbe, AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS } from "./doctor-probe.js";
3
3
  import { parseJsonObject } from "../files/json-object.js";
4
4
  import { envPath } from "../files/files.js";
5
5
  import { resolveBackendBin } from "../pi/pi-bin.js";
@@ -12,7 +12,7 @@ import { parsePiModelImages, parsePiModelTable, writePiListedModels } from "../p
12
12
  * a crash, so readiness is read from the body alone and a reply that did not run or does not
13
13
  * parse is the only "unknown" (never not-ready, which would blame credentials). Never throws;
14
14
  * every failure mode ends on "unknown". The bounded subprocess is the shared boundedProbe
15
- * (probe.ts), also used by the claude probes. */
15
+ * (doctor-probe.ts), also used by the claude probes. */
16
16
  export async function piProviderAuth(config, provider, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
17
17
  const resolved = resolveBackendBin(config);
18
18
  const result = await boundedProbe(resolved.bin, ["auth", "check", "--provider", provider, "--json"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
- import { readTextOrNull } from "../files/files.js";
2
+ import { readRegularTextOrNull } from "../files/files.js";
3
3
  import { PRINCIPLES_MAX_CHARS } from "../prompt/principles.js";
4
- import { okOutcome, warnOutcome } from "./outcome.js";
4
+ import { okOutcome, warnOutcome } from "./doctor-outcome.js";
5
5
  /** Doctor check for PRINCIPLES.md's injection cap. readPrinciples caps the trimmed text at
6
6
  * PRINCIPLES_MAX_CHARS for every prompt's <principles> block, and principleBullets keeps only
7
7
  * its leading bullets; a file over the cap loses its tail (and a long header eats bullet
@@ -9,7 +9,7 @@ import { okOutcome, warnOutcome } from "./outcome.js";
9
9
  * truncate, naming the trimmed count, the cap, the marker, and the fix. Read-only, like every
10
10
  * doctor check — it never edits PRINCIPLES.md. */
11
11
  export function checkPrinciples(root) {
12
- const raw = readTextOrNull(path.join(root, "PRINCIPLES.md"));
12
+ const raw = readRegularTextOrNull(path.join(root, "PRINCIPLES.md"));
13
13
  if (raw === null)
14
14
  return okOutcome("no PRINCIPLES.md");
15
15
  const length = raw.trim().length;
@@ -17,7 +17,7 @@ import { buildStaleness, isSelfHosted, readBuildInfo, STALE_INPUTS_LABEL } from
17
17
  import { dirEntryNames, statOrNull } from "../files/files.js";
18
18
  import { refSha } from "../git/git.js";
19
19
  import { STATE_DIR, eventsArchivePath, eventsLogPath } from "../paths.js";
20
- import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gate.js";
20
+ import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gates.js";
21
21
  import { DAY_MS, dayKey } from "../text/datetime.js";
22
22
  import { errorMessage } from "../text/text.js";
23
23
  import { lowerMedian } from "../collections.js";
@@ -25,7 +25,7 @@ import { formatGB, shortSha } from "../text/format.js";
25
25
  import { eventLogState, readEventsSince } from "../events/event-window.js";
26
26
  import { EVENTS_KEEP } from "../events/events.js";
27
27
  import { finiteNumber } from "../files/json-object.js";
28
- import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
28
+ import { agree, plural, shortSpanPhrase, trailingDayMedianPhrase } from "../text/phrases.js";
29
29
  import { andMore } from "./doctor-backlog.js";
30
30
  import { KNOWN_FLAKE_WINDOW_MS, loadQuarantine, repeatedFlakeCounts } from "../review/known-flakes.js";
31
31
  import { POST_MERGE_REJECT_WINDOW_MS, postMergeChurn } from "../review/post-merge-review.js";
@@ -33,7 +33,7 @@ import { roleLabel } from "../roles/role-labels.js";
33
33
  import { quarantineWarning } from "../baseline/main-guardrails.js";
34
34
  import { readCiParity, currentCiParityState, parityIsCurrent, ciParityStalled, CI_PARITY_STALE_MS } from "../run/ci-parity.js";
35
35
  import { readQaCoverage, qaCoverageStalled, QA_COVERAGE_STALE_MS } from "../run/qa-coverage.js";
36
- import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
36
+ import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
37
37
  /** .tumwater writable — absent is fine (created on first run); present, prove it by writing
38
38
  * and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
39
39
  * The write is the property under test: only a write that fails is "not writable", so the
@@ -204,7 +204,7 @@ export function checkDurationPhrase(events) {
204
204
  return undefined;
205
205
  const median = lowerMedian(durations);
206
206
  const slowest = Math.max(...durations);
207
- return `passing merge checks in the last 24h took ${shortSpanPhrase(median)} median, ${shortSpanPhrase(slowest)} slowest (${plural(durations.length, "run")})`;
207
+ return `passing merge checks in the last 24h took ${trailingDayMedianPhrase(median, slowest, "slowest", durations.length, "run")}`;
208
208
  }
209
209
  /** Declared project check — names what the review gate's deterministic pre-check, the
210
210
  * red-main baseline, and redeploy's green check will run, without running it: a configured
@@ -251,7 +251,7 @@ export function checkBuildCheck(root, config) {
251
251
  * once, so a routine one-off flake stays quiet. ok/warn only — never a fail — so a flaky suite
252
252
  * cannot fail a scripted doctor pre-flight (the checkStalledEntries precedent; the exit-code
253
253
  * rule is
254
- * outcome.ts's). */
254
+ * doctor-outcome.ts's). */
255
255
  export function checkFlakyChecks(root, now = Date.now()) {
256
256
  const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
257
257
  const flapped = repeatedFlakeCounts(events);
@@ -278,7 +278,7 @@ export function checkPostMergeChurn(root) {
278
278
  return okOutcome("no post-merge rejection burst in the last day");
279
279
  return warnOutcome(`${plural(churn.count, "post-merge review rejection")} in the last day (${churn.roles
280
280
  .map((r) => `${roleLabel(r.role)} ×${r.count}`)
281
- .join(", ")}) — each commit had already reached main and stays there with the reviewer's reasons; narrow \`review.afterMerge\` for the noisy role or raise the authoring bar`);
281
+ .join(", ")}) — each commit had already reached main and stays there with the reviewer's reasons; raise the authoring bar`);
282
282
  }
283
283
  /** The one home of the doctor's `flaky checks` sentence: names each check that flapped more than
284
284
  * once, most frequent first, with how many times it flapped — the one sentence the doctor's
@@ -321,7 +321,10 @@ running = null) {
321
321
  ? `; auto-restart is BLOCKED (${running.restartBlocked}) and will not retry until main moves or the block clears`
322
322
  : running?.restartPending
323
323
  ? "; auto-restart is under way"
324
- : "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
324
+ : running?.waitingForCheck
325
+ ? `; auto-restart waits for a commit that passes main's check — none newer than ` +
326
+ `${shortSha(running.checkedHead ?? info.sha)} has yet (the main's check row says why)`
327
+ : "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
325
328
  return warnOutcome(`dist/ from ${sha} is stale — main has ${plural(stale.aheadCommits, "later commit")} touching ${STALE_INPUTS_LABEL}${next}`);
326
329
  }
327
330
  return okOutcome(`dist/ from ${sha}, matches main`);
@@ -396,7 +399,7 @@ export function checkQaCoverage(root, now = Date.now()) {
396
399
  /** How far back the `main diff rules` row looks — one day, the same 24 h trailing-day convention
397
400
  * every event-reading doctor row uses, so a diff-rule failure the main watch logged stays a
398
401
  * standing warn until a day passes. Owned here (the check that reads it). */
399
- const MAIN_RULE_FAILURE_WINDOW_MS = 24 * 60 * 60_000;
402
+ const MAIN_RULE_FAILURE_WINDOW_MS = DAY_MS;
400
403
  /** The exact substring src/baseline/main-watch.ts's `watchRules` logs in its warning, so this
401
404
  * check matches a real rule failure and cannot drift from the wording the watch writes. */
402
405
  const MAIN_RULE_FAILURE_MARKER = "fails the diff rules";
@@ -407,9 +410,19 @@ const MAIN_RULE_FAILURE_MARKER = "fails the diff rules";
407
410
  * blind spot sits on main with only this event log to say so. Reads only the `warning` event
408
411
  * the watch already writes, so the row cannot drift from the watch's wording; the most recent
409
412
  * hit names the tip and the rule. ok/warn only — it never fails a scripted pre-flight. */
413
+ /** The `warning` events whose message contains `marker` — the read both marker-driven
414
+ * project rows (checkMainDiffRules and checkDroppedPostMergeReviews) ask for, so their
415
+ * filter predicate cannot drift: a warning whose message is absent (an older event) is
416
+ * never a hit, and the marker match is a plain substring like the watch's wording.
417
+ * `e.type === "warning"` gates on the literal type, mirroring the warn shape warnEvent
418
+ * writes. Since every hit is a `warning`, `String` on its message downstream reads safely
419
+ * for the events this returns. */
420
+ function warningEventsWithMarker(events, marker) {
421
+ return events.filter((e) => e.type === "warning" && typeof e.message === "string" && e.message.includes(marker));
422
+ }
410
423
  export function checkMainDiffRules(root) {
411
424
  const { events } = readEventsSince(root, MAIN_RULE_FAILURE_WINDOW_MS);
412
- const hits = events.filter((e) => e.type === "warning" && typeof e.message === "string" && e.message.includes(MAIN_RULE_FAILURE_MARKER));
425
+ const hits = warningEventsWithMarker(events, MAIN_RULE_FAILURE_MARKER);
413
426
  if (hits.length === 0)
414
427
  return okOutcome("no diff-rule failure on main in the last day");
415
428
  const latest = hits[hits.length - 1];
@@ -426,7 +439,7 @@ export function checkMainDiffRules(root) {
426
439
  const DROPPED_REVIEW_MARKER = "dropping the post-merge review of";
427
440
  /** How far back the `dropped post-merge reviews` row looks — one day, the same 24 h trailing-day
428
441
  * convention every event-reading doctor row uses. Owned here (the check that reads it). */
429
- const DROPPED_REVIEW_WINDOW_MS = 24 * 60 * 60_000;
442
+ const DROPPED_REVIEW_WINDOW_MS = DAY_MS;
430
443
  /** The doctor's `dropped post-merge reviews` row: a `warning` the post-merge-review drain
431
444
  * (src/merge/merge-post-review-drain.ts) wrote when it dropped a review at
432
445
  * POST_REVIEW_FAILURE_LIMIT — after failed runs its commit stays on main unreviewed, and
@@ -439,7 +452,7 @@ const DROPPED_REVIEW_WINDOW_MS = 24 * 60 * 60_000;
439
452
  * unreviewed commit is operator signal, not a broken environment. */
440
453
  export function checkDroppedPostMergeReviews(root) {
441
454
  const { events } = readEventsSince(root, DROPPED_REVIEW_WINDOW_MS);
442
- const hits = events.filter((e) => e.type === "warning" && typeof e.message === "string" && e.message.includes(DROPPED_REVIEW_MARKER));
455
+ const hits = warningEventsWithMarker(events, DROPPED_REVIEW_MARKER);
443
456
  if (hits.length === 0)
444
457
  return okOutcome("no post-merge review was dropped in the last day");
445
458
  const latest = hits[hits.length - 1];
@@ -1,7 +1,7 @@
1
1
  import { readRoleNotesOverCap } from "../status/status-role-notes.js";
2
2
  import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
3
3
  import { agree, plural } from "../text/phrases.js";
4
- import { okOutcome, warnOutcome } from "./outcome.js";
4
+ import { okOutcome, warnOutcome } from "./doctor-outcome.js";
5
5
  /** How many oversized notes the `role notes` row names before appending "and N more", the
6
6
  * cap the other multi-role observer rows (e.g. workspaces) use so a long tail does not drown
7
7
  * the report. */
@@ -1,23 +1,23 @@
1
1
  import { loadConfigSafe } from "../config/config.js";
2
2
  import { envPath } from "../files/files.js";
3
- import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
3
+ import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
4
4
  import { systemProcessProbe } from "../process/process-table.js";
5
5
  import { checkOrphans } from "./doctor-orphans.js";
6
- import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours, checkSubscriptionWindow } from "./doctor-holds.js";
6
+ import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours } from "./doctor-holds.js";
7
7
  import { checkRootInstall } from "./doctor-install.js";
8
8
  import { checkNotify } from "./doctor-notify.js";
9
9
  import { checkRoleNotes } from "./doctor-role-notes.js";
10
10
  import { checkLaunchServicesPorts } from "./doctor-launch-services.js";
11
11
  import { plural } from "../text/phrases.js";
12
12
  import { shortSha } from "../text/format.js";
13
- import { failOutcome } from "./outcome.js";
13
+ import { failOutcome } from "./doctor-outcome.js";
14
14
  import { errorMessage } from "../text/text.js";
15
15
  import { checkBackendBinary, checkBrief, checkBootstrap, checkClaudeTranscripts, checkExecPath, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkOpencodeVersion, checkNodeVersion, checkPiExtensions, checkRepo, } from "./doctor-checks.js";
16
16
  import { checkPrinciples } from "./doctor-principles.js";
17
17
  import { checkBuild, checkBuildCheck, checkCiParity, checkDiskSpace, checkDroppedPostMergeReviews, checkEventLog, checkFlakyChecks, checkMainDiffRules, checkPostMergeChurn, checkQaCoverage, checkStateDir, } from "./doctor-project-checks.js";
18
- import { sampleFreeBytes } from "../gates/disk-gate.js";
19
- import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderConcurrency, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
20
- import { checkFeedbackFiling, checkTierModels } from "./doctor-model-checks.js";
18
+ import { sampleFreeBytes } from "../gates/disk-gates.js";
19
+ import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
20
+ import { checkFeedbackFiling, checkImageInput, checkTierModels } from "./doctor-model-checks.js";
21
21
  import { checkFallbackChain, checkFallbackModel } from "./doctor-fallback-checks.js";
22
22
  import { checkAnthroApiKey, checkClaudeAuth, checkClaudeVersion } from "./doctor-claude-checks.js";
23
23
  import { checkOpencode } from "./doctor-opencode-checks.js";
@@ -95,11 +95,8 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
95
95
  { name: "pause", run: () => checkPause(root) },
96
96
  { name: "quiet hours", run: () => checkQuietHours(config) },
97
97
  { name: "budget", run: () => checkBudget(root, config, (orchestratorAlive(root, info) && info?.budget) || null) },
98
- { name: "claude window", run: () => checkSubscriptionWindow("claude", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
99
- { name: "codex window", run: () => checkSubscriptionWindow("codex", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
100
98
  { name: "provider waits", run: () => checkProviderHolds(root) },
101
99
  { name: "maintenance allowance", run: () => checkMaintenanceQuota(root, config, orchestratorAlive(root, info)) },
102
- { name: "provider concurrency", run: () => checkProviderConcurrency(config, root) },
103
100
  { name: "provider contention", run: () => checkProviderContention(root, config) },
104
101
  { name: "check contention", run: () => checkCheckContention(root, config) },
105
102
  { name: "repeated failures", run: () => checkErrorStreaks(root, config) },
@@ -107,6 +104,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
107
104
  { name: "over-cap model", run: () => checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
108
105
  { name: "fallback chain", run: () => checkFallbackChain(root) },
109
106
  { name: "models for each tier", run: () => checkTierModels(root) },
107
+ { name: "image input", run: () => checkImageInput(root) },
110
108
  { name: "claude auth", run: () => checkClaudeAuth(root, pathEnv) },
111
109
  { name: "claude api key", run: () => checkAnthroApiKey(root, undefined, pathEnv) },
112
110
  { name: "claude version", run: () => checkClaudeVersion(root, pathEnv) },
@@ -127,8 +125,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
127
125
  // The receive-side guards name what a running team skips, so an operator tool meant for the
128
126
  // team (a pi package, an extension file, an mcp.json server, or funes) does not vanish
129
127
  // silently. Each check returns null to omit its row: the pi row for a team with no pi seam,
130
- // the funes row when funes is not on PATH, and the claude/codex window rows when that backend
131
- // has no subscription account.
128
+ // the funes row when funes is not on PATH.
132
129
  { name: "pi extensions", run: () => checkPiExtensions(config, undefined, pathEnv) },
133
130
  { name: "funes", run: () => checkFunes(config, pathEnv) },
134
131
  { name: "state dir", run: () => checkStateDir(root) },