tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { childEnv } from "../process/process.js";
4
4
  import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
5
5
  import { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
6
- import { opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
6
+ import { opencodeModelFree, opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
7
+ import { formatDate } from "../text/datetime.js";
7
8
  import { opencodeDbPath } from "../paths.js";
8
9
  import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes } from "../files/files.js";
9
10
  import { formatBytes, tokenWindowLabel } from "../text/format.js";
@@ -11,7 +12,7 @@ import { escapeRegExp } from "../text/text.js";
11
12
  import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
12
13
  import { loadConfigForCheck, opencodeSeamsInUse, seamedModels, } from "./doctor-model-checks.js";
13
14
  /** The doctor's opencode credential and leak readiness check, split out of doctor-model-checks.ts
14
- * (whose shared seam/model framework and the pi, tier-models and image-input checks stay there):
15
+ * (whose shared seam/model framework and the pi and tier-models checks stay there):
15
16
  * checkOpencode and its helpers (opencodeProviderOf, opencodeAuthLists, jsonKeyPresent,
16
17
  * opencodeConfigLeaks). This is the doctor's only check that spawns the `opencode` CLI and reads
17
18
  * its catalog and the operator's global opencode config — a distinct concern from the
@@ -107,6 +108,80 @@ function opencodeConfigLeaks(configHome) {
107
108
  warns.push(`${agentsMd} still loads into team runs`);
108
109
  return warns;
109
110
  }
111
+ /** A catalog model's price text — "free" when free, the billed-demotion phrase when `billedAt`
112
+ * is set, else the per-M prices — so opencodeModelRow can name the price without the nested
113
+ * ternary and its two `?? 0` fallbacks. */
114
+ function opencodePriceText(free, billedAt, inputPerM, outputPerM) {
115
+ if (free)
116
+ return "free";
117
+ if (billedAt !== undefined)
118
+ return `no longer free (billed ${formatDate(new Date(billedAt))})`;
119
+ return `$${inputPerM ?? 0}/$${outputPerM ?? 0} per M`;
120
+ }
121
+ /** One opencode catalog model's row: today's per-model body from the cache key through the
122
+ * dropped-effort warning, returning the catalog note and any warnings, or undefined when the
123
+ * model is missing from the cache (the caller owns that failure message). `warnings` holds the
124
+ * billed-at demotion line and the dropped-effort line in today's order. */
125
+ function opencodeModelRow(cache, model, thinking) {
126
+ const key = opencodeModelKey(model);
127
+ const entry = cache.models[key];
128
+ if (entry === undefined)
129
+ return undefined;
130
+ const warnings = [];
131
+ const free = opencodeModelFree(cache, key);
132
+ if (entry.billedAt !== undefined) {
133
+ warnings.push(`${model} was free in the catalog but billed once on ${formatDate(new Date(entry.billedAt))} — the budget gate now prices it`);
134
+ }
135
+ const window = entry.context !== undefined ? `, ${tokenWindowLabel(entry.context)}` : "";
136
+ const note = `${model} — ${opencodePriceText(free, entry.billedAt, entry.inputPerM, entry.outputPerM)}${window}`;
137
+ // A model with no effort list at all supports no thinking level, so a requested level is
138
+ // dropped just the same as one the list omits.
139
+ if (thinking !== undefined && thinking !== "" && !entry.efforts?.includes(thinking)) {
140
+ warnings.push(`${model} has no ${thinking} effort — that thinking level is dropped`);
141
+ }
142
+ return { note, warnings };
143
+ }
144
+ /** The team's opencode database and spill directory sizes as one information line (`db …,
145
+ * spill …`), reading the db, the spill dir and their byte formatting — today's storage tail. */
146
+ function opencodeStorageNote(root) {
147
+ const db = statOrNull(opencodeDbPath(root));
148
+ const spillDir = opencodeSpillDir(root);
149
+ const spill = spillDir !== "" && isDirectory(spillDir) ? treeBytes(spillDir) : null;
150
+ return `db ${formatBytes(db?.size ?? 0)}${spill !== null ? `, spill ${formatBytes(spill)}` : ", no spill dir"}`;
151
+ }
152
+ /** Walk the opencode-routed model pairs into today's per-model fails, warns and notes: the
153
+ * catalog-miss failure, the catalog note and its warnings from opencodeModelRow, and the
154
+ * non-gateway credential probe each provider. Returns the three arrays checkOpencode assembles. */
155
+ async function collectOpencodeModelRows(cache, models, config, pathEnv, timeoutMs) {
156
+ const fails = [];
157
+ const warns = [];
158
+ const notes = [];
159
+ for (const pair of models) {
160
+ const model = pair.model;
161
+ const row = opencodeModelRow(cache, model, pair.thinking);
162
+ if (row === undefined) {
163
+ fails.push(`${model} is not in the opencode catalog cache`);
164
+ continue;
165
+ }
166
+ notes.push(row.note);
167
+ warns.push(...row.warnings);
168
+ const provider = opencodeProviderOf(model);
169
+ if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
170
+ warns.push(`${model} is a non-gateway provider opencode does not list a credential for`);
171
+ }
172
+ }
173
+ return { fails, warns, notes };
174
+ }
175
+ /** The check's outcome from the accumulated fails/warns/notes and the storage tail: the joined
176
+ * base line ("opencode ready — …" when nothing is wrong, else the found lines) and the
177
+ * fail/warn/ok verdict by severity. */
178
+ function opencodeOutcome(fails, warns, notes, sizeNotes) {
179
+ const found = [...fails, ...warns];
180
+ const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
181
+ if (fails.length > 0)
182
+ return failOutcome(base);
183
+ return (warns.length > 0 ? warnOutcome : okOutcome)(base);
184
+ }
110
185
  /** OpenCode model, credential and leak readiness (plans/opencode-backend.md §7, part 7/7):
111
186
  * every model routed to opencode must be in the catalog cache — an unknown model fails, a
112
187
  * cached one reads "free" or its per-M prices and context window, and a thinking level with no
@@ -116,49 +191,23 @@ function opencodeConfigLeaks(configHome) {
116
191
  * team runs, else warns naming the file and key. Reports the team's opencode database and spill
117
192
  * sizes as information. Stands down (ok) when no seam routes to opencode, so a pi-only team
118
193
  * spawns nothing and reads nothing. `pathEnv` and `home` are injectable so tests need no real
119
- * PATH or HOME. The version half of the opencode pre-flight is the sibling checkOpencodeVersion
120
- * in doctor-checks.ts. */
194
+ * PATH or HOME. A model's freeness is read from opencodeModelFree — the same call the budget
195
+ * gate and status use — so a catalog-free model that an observed bill demoted to billed reads
196
+ * as billed here, not free, keeping the row from drifting from what the team is billed. The
197
+ * per-model catalog row lives in opencodeModelRow, the database-and-spill size line in
198
+ * opencodeStorageNote; both are the two extracted jobs the reader names by. The version half of
199
+ * the opencode pre-flight is the sibling checkOpencodeVersion in doctor-checks.ts. */
121
200
  export async function checkOpencode(root, pathEnv = envPath(), home = os.homedir(), xdgConfigHome = process.env.XDG_CONFIG_HOME ?? "", timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
122
201
  const config = loadConfigForCheck(root);
123
202
  if ("level" in config)
124
203
  return config;
125
204
  if (!opencodeSeamsInUse(config))
126
205
  return okOutcome("no model jobs run on opencode");
127
- const models = seamedModels(config, "opencode");
128
- const cache = readOpencodeCache(root);
129
- const fails = [];
130
- const warns = [];
131
- const notes = [];
132
- for (const pair of models) {
133
- const key = opencodeModelKey(pair.model);
134
- const entry = cache.models[key];
135
- if (entry === undefined) {
136
- fails.push(`${pair.model} is not in the opencode catalog cache`);
137
- continue;
138
- }
139
- const price = entry.free ? "free" : `$${entry.inputPerM ?? 0}/$${entry.outputPerM ?? 0} per M`;
140
- const window = entry.context !== undefined ? `, ${tokenWindowLabel(entry.context)}` : "";
141
- notes.push(`${pair.model} — ${price}${window}`);
142
- // A model with no effort list at all supports no thinking level, so a requested level is
143
- // dropped just the same as one the list omits.
144
- if (pair.thinking !== undefined &&
145
- pair.thinking !== "" &&
146
- !entry.efforts?.includes(pair.thinking)) {
147
- warns.push(`${pair.model} has no ${pair.thinking} effort — that thinking level is dropped`);
148
- }
149
- const provider = opencodeProviderOf(pair.model);
150
- if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
151
- warns.push(`${pair.model} is a non-gateway provider opencode does not list a credential for`);
152
- }
153
- }
154
- warns.push(...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")));
155
- const db = statOrNull(opencodeDbPath(root));
156
- const spillDir = opencodeSpillDir(root);
157
- const spill = spillDir !== "" && isDirectory(spillDir) ? treeBytes(spillDir) : null;
158
- const sizeNotes = `db ${formatBytes(db?.size ?? 0)}${spill !== null ? `, spill ${formatBytes(spill)}` : ", no spill dir"}`;
159
- const found = [...fails, ...warns];
160
- const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
161
- if (fails.length > 0)
162
- return failOutcome(base);
163
- return (warns.length > 0 ? warnOutcome : okOutcome)(base);
206
+ const rows = await collectOpencodeModelRows(readOpencodeCache(root), seamedModels(config, "opencode"), config, pathEnv, timeoutMs);
207
+ const warns = [
208
+ ...rows.warns,
209
+ ...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")),
210
+ ];
211
+ const sizeNotes = opencodeStorageNote(root);
212
+ return opencodeOutcome(rows.fails, warns, rows.notes, sizeNotes);
164
213
  }
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ import { readTextOrNull } from "../files/files.js";
3
+ import { PRINCIPLES_MAX_CHARS } from "../prompt/principles.js";
4
+ import { okOutcome, warnOutcome } from "./outcome.js";
5
+ /** Doctor check for PRINCIPLES.md's injection cap. readPrinciples caps the trimmed text at
6
+ * PRINCIPLES_MAX_CHARS for every prompt's <principles> block, and principleBullets keeps only
7
+ * its leading bullets; a file over the cap loses its tail (and a long header eats bullet
8
+ * budget) with nothing telling the operator who edits it. This check warns when the file would
9
+ * truncate, naming the trimmed count, the cap, the marker, and the fix. Read-only, like every
10
+ * doctor check — it never edits PRINCIPLES.md. */
11
+ export function checkPrinciples(root) {
12
+ const raw = readTextOrNull(path.join(root, "PRINCIPLES.md"));
13
+ if (raw === null)
14
+ return okOutcome("no PRINCIPLES.md");
15
+ const length = raw.trim().length;
16
+ if (length <= PRINCIPLES_MAX_CHARS) {
17
+ return okOutcome(`PRINCIPLES.md is within the ${PRINCIPLES_MAX_CHARS}-character prompt cap`);
18
+ }
19
+ return warnOutcome("PRINCIPLES.md has " +
20
+ `${length} trimmed characters, over the ${PRINCIPLES_MAX_CHARS}-character ` +
21
+ "prompt cap; every run's <principles> block carries only the head plus " +
22
+ "`…[PRINCIPLES.md truncated at " +
23
+ `${PRINCIPLES_MAX_CHARS} chars]` +
24
+ " and only the leading bullets survive — keep it to its list bullets and " +
25
+ "move long-form rationale into docs/ (not injected)");
26
+ }
@@ -26,6 +26,7 @@ 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
28
  import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
29
+ import { andMore } from "./doctor-backlog.js";
29
30
  import { KNOWN_FLAKE_WINDOW_MS, loadQuarantine, repeatedFlakeCounts } from "../review/known-flakes.js";
30
31
  import { POST_MERGE_REJECT_WINDOW_MS, postMergeChurn } from "../review/post-merge-review.js";
31
32
  import { roleLabel } from "../roles/role-labels.js";
@@ -216,7 +217,7 @@ export function checkDurationPhrase(events) {
216
217
  export function checkBuildCheck(root, config) {
217
218
  const check = detectBuildCheck(root, config ?? undefined);
218
219
  if (!check)
219
- return warnOutcome("none declared — the review gate's build pre-check, the red-main baseline, and redeploy's green check are all off (set `check.command` in tumwater.json for a non-npm repo)");
220
+ return warnOutcome("none declared — the review gate's build pre-check, the red-main baseline, and redeploy's green check are all off (set `check.command` in tumwater.json for a non-npm repo); the plan role will plan this project's check and the feature role declare it, a test/typecheck/build npm script or check.command");
220
221
  // The in-lock command from mergeCommandOf (absent mergeCommand falls back to the gate
221
222
  // command): the row names which command the merge lock runs and where the full check lands,
222
223
  // facts a reader cannot get from detectBuildCheck alone. The after-merge roles are not named
@@ -266,13 +267,10 @@ export function checkFlakyChecks(root, now = Date.now()) {
266
267
  return warnOutcome(phrases.join("; "));
267
268
  }
268
269
  /** A trailing day of at least `POST_MERGE_REJECT_WARN` post-merge `review_rejected` events — the
269
- * audit twin of the always-on `post-merge-churn` amber alert (src/ui/status-payload.ts
270
- * `postMergeChurnAlert`). Each rejection is an already-landed commit the reviewer turned back, so
271
- * it stays on main with the reviewer's reasons, so a churn day must not read as a clean
272
- * main. Shares the alert's
273
- * window, threshold and
274
- * event selection through `postMergeChurn`, so the doctor row and the alert cannot disagree on
275
- * when churn fires. ok/warn only — a churn burst does not stop the team. */
270
+ * audit of a day that must not read as a clean main: each rejection is an already-landed commit
271
+ * the reviewer turned back, so it stays on main with the reviewer's reasons. Reads the
272
+ * `POST_MERGE_REJECT_WINDOW_MS` window and
273
+ * threshold through `postMergeChurn`. ok/warn only — a churn burst does not stop the team. */
276
274
  export function checkPostMergeChurn(root) {
277
275
  const { events } = readEventsSince(root, POST_MERGE_REJECT_WINDOW_MS);
278
276
  const churn = postMergeChurn(events);
@@ -283,15 +281,15 @@ export function checkPostMergeChurn(root) {
283
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`);
284
282
  }
285
283
  /** The one home of the doctor's `flaky checks` sentence: names each check that flapped more than
286
- * once, most frequent first, with how many times it flapped — the same wording the always-on
287
- * flapped-checks alert carries, so the doctor row and the alert cannot drift. null when `flapped`
284
+ * once, most frequent first, with how many times it flapped — the one sentence the doctor's
285
+ * `flaky checks` row shows. null when `flapped`
288
286
  * is empty (nothing crossed the bar). */
289
- export function flappedChecksPhrase(flapped) {
287
+ function flappedChecksPhrase(flapped) {
290
288
  if (flapped.length === 0)
291
289
  return null;
292
290
  const render = ([k, n]) => `"${k}" ×${n}`;
293
291
  const [first, ...rest] = flapped;
294
- const more = rest.length > 0 ? ` (and ${rest.length} more: ${rest.map(render).join(", ")})` : "";
292
+ const more = andMore(rest, render);
295
293
  return `${agree(flapped.length, "check", "checks")} flapped more than once in the last 24h: ${render(first)}${more}`;
296
294
  }
297
295
  /** Build provenance — is the harness about to run (this process's dist/) the code main
@@ -324,7 +322,7 @@ running = null) {
324
322
  : running?.restartPending
325
323
  ? "; auto-restart is under way"
326
324
  : "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
327
- return warnOutcome(`dist/ from ${sha} is stale — main has ${stale.aheadCommits} later commit(s) touching ${STALE_INPUTS_LABEL}${next}`);
325
+ return warnOutcome(`dist/ from ${sha} is stale — main has ${plural(stale.aheadCommits, "later commit")} touching ${STALE_INPUTS_LABEL}${next}`);
328
326
  }
329
327
  return okOutcome(`dist/ from ${sha}, matches main`);
330
328
  }
@@ -343,7 +341,7 @@ function currentHead(root) {
343
341
  * on every merge and the ci role runs on its own clock, so a post-merge gap is normal and must
344
342
  * not turn the Health card or the exit code red. A stale verdict that stays stale past
345
343
  * `CI_PARITY_STALE_MS` (ciParityStalled) is a stall — the full suite has silently stopped
346
- * covering new tips — and warns, alongside the standing `ci-stale` alert. Read-only against
344
+ * covering new tips — and warns. Read-only against
347
345
  * .tumwater/. */
348
346
  export function checkCiParity(root, config, now = Date.now()) {
349
347
  const entry = readCiParity(root);
@@ -359,9 +357,9 @@ export function checkCiParity(root, config, now = Date.now()) {
359
357
  }
360
358
  // A stale verdict is normal right after a merge (main moves and the ci role runs on its own
361
359
  // clock), but one that stays stale past CI_PARITY_STALE_MS is a stall: the full suite has
362
- // silently stopped covering new tips. The doctor warn and the standing `ci-stale` alert share
363
- // this one predicate (ciParityStalled), so the row and the alert cannot disagree on when a
364
- // gap is a problem. Inside the window the existing friendly stale wording stands.
360
+ // silently stopped covering new tips. This warn is the one place ciParityStalled is applied —
361
+ // the row is the standing signal. Inside the window the existing friendly stale wording
362
+ // stands.
365
363
  if (ciParityStalled({ state: "stale", lastRunAt: entry.lastRunAt }, now)) {
366
364
  return warnOutcome(`stale and stalled — the last run covered ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} ${age} ago (${entry.result}); main is now ${shortSha(tip)} @ lock ${shortSha(lockHash)} — no full-suite run has covered a tip in over ${CI_PARITY_STALE_MS / DAY_MS} days, so the ci role is disabled, paused, or unable to record`);
367
365
  }
@@ -374,8 +372,8 @@ export function checkCiParity(root, config, now = Date.now()) {
374
372
  * is not a stall); a fresh ledger reads ok naming the newest exercised flow and its age. A
375
373
  * ledger whose newest exercise is older than `QA_COVERAGE_STALE_MS` (qaCoverageStalled) is a
376
374
  * `warn` naming that flow and its age — the role is disabled, paused, or unable to record —
377
- * alongside the standing `qa-stale` alert, both through the one `qaCoverageStalled` predicate
378
- * so the row and the alert cannot disagree. Read-only against .tumwater/. */
375
+ * via the one `qaCoverageStalled` predicate, this row the standing signal. Read-only against
376
+ * .tumwater/. */
379
377
  export function checkQaCoverage(root, now = Date.now()) {
380
378
  const coverage = readQaCoverage(root);
381
379
  const entries = Object.entries(coverage);
@@ -395,3 +393,55 @@ export function checkQaCoverage(root, now = Date.now()) {
395
393
  }
396
394
  return okOutcome(`newest flow coverage: ${newestFlow} exercised ${age} ago`);
397
395
  }
396
+ /** How far back the `main diff rules` row looks — one day, the same 24 h trailing-day convention
397
+ * every event-reading doctor row uses, so a diff-rule failure the main watch logged stays a
398
+ * standing warn until a day passes. Owned here (the check that reads it). */
399
+ const MAIN_RULE_FAILURE_WINDOW_MS = 24 * 60 * 60_000;
400
+ /** The exact substring src/baseline/main-watch.ts's `watchRules` logs in its warning, so this
401
+ * check matches a real rule failure and cannot drift from the wording the watch writes. */
402
+ const MAIN_RULE_FAILURE_MARKER = "fails the diff rules";
403
+ /** The doctor's `main diff rules` row: a `warning` the main watch logged when the review gate's
404
+ * diff rules rejected main's tip in the trailing day. The gate judges one change; the watch
405
+ * judges each first-parent commit's own diff, so a conflict marker, a protected edit, a leaked
406
+ * secret, a phantom fix claim or a broken backlog structure that reached main through a gate
407
+ * blind spot sits on main with only this event log to say so. Reads only the `warning` event
408
+ * the watch already writes, so the row cannot drift from the watch's wording; the most recent
409
+ * hit names the tip and the rule. ok/warn only — it never fails a scripted pre-flight. */
410
+ export function checkMainDiffRules(root) {
411
+ 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));
413
+ if (hits.length === 0)
414
+ return okOutcome("no diff-rule failure on main in the last day");
415
+ const latest = hits[hits.length - 1];
416
+ return warnOutcome(`${plural(hits.length, "diff-rule failure on main", "diff-rule failures on main")} in the last day — most recently ${String(latest.message)} — the gate approved a diff the watch's rules reject; repair the tip or revert it with \`tumwater revert <sha>\``);
417
+ }
418
+ /** The exact prefix src/merge/merge-post-review-drain.ts logs when it drops a post-merge
419
+ * review at POST_REVIEW_FAILURE_LIMIT. The drain drops in two ways and logs each with its own
420
+ * tail — "it stays on main unreviewed" after failed runs (the commit never got a review) and
421
+ * "the fix stays unreviewed" after failed fix merges (the fix ref is deleted, main unmoved) —
422
+ * so this check matches the shared "dropping the post-merge review of" prefix both share, not
423
+ * either tail. The drain also files the drop in BUGS.md as best-effort ordinary backlog work
424
+ * (fileDroppedReview), but a skipped filing (off-base checkout or a dirty BUGS.md) leaves only
425
+ * this event log standing, so this check matches the drain's own wording. */
426
+ const DROPPED_REVIEW_MARKER = "dropping the post-merge review of";
427
+ /** How far back the `dropped post-merge reviews` row looks — one day, the same 24 h trailing-day
428
+ * convention every event-reading doctor row uses. Owned here (the check that reads it). */
429
+ const DROPPED_REVIEW_WINDOW_MS = 24 * 60 * 60_000;
430
+ /** The doctor's `dropped post-merge reviews` row: a `warning` the post-merge-review drain
431
+ * (src/merge/merge-post-review-drain.ts) wrote when it dropped a review at
432
+ * POST_REVIEW_FAILURE_LIMIT — after failed runs its commit stays on main unreviewed, and
433
+ * after failed fix merges the fix ref is deleted (main unmoved) so the commit stays
434
+ * unreviewed; either way the review is gone and no longer in the queue the `post-merge
435
+ * reviews` row reads. The drain files the dropped review in BUGS.md as best-effort ordinary
436
+ * backlog work, so when that filing is skipped this row is the standing signal naming the
437
+ * dropped review. Reads only the `warning` event the drain already writes, so it cannot drift
438
+ * from the drain's wording; the most recent hit names the commit. ok/warn only — an
439
+ * unreviewed commit is operator signal, not a broken environment. */
440
+ export function checkDroppedPostMergeReviews(root) {
441
+ 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));
443
+ if (hits.length === 0)
444
+ return okOutcome("no post-merge review was dropped in the last day");
445
+ const latest = hits[hits.length - 1];
446
+ return warnOutcome(`${plural(hits.length, "post-merge review was dropped", "post-merge reviews were dropped")} in the last day — the dropped review is gone and its commit stays unreviewed; most recently ${String(latest.message)}`);
447
+ }
@@ -0,0 +1,33 @@
1
+ import { readRoleNotesOverCap } from "../status/status-role-notes.js";
2
+ import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
3
+ import { agree, plural } from "../text/phrases.js";
4
+ import { okOutcome, warnOutcome } from "./outcome.js";
5
+ /** How many oversized notes the `role notes` row names before appending "and N more", the
6
+ * cap the other multi-role observer rows (e.g. workspaces) use so a long tail does not drown
7
+ * the report. */
8
+ const ROLE_NOTES_NAMED_CAP = 3;
9
+ /** The `role notes` check: whether any role's notes file would be truncated when read into
10
+ * that role's run prompt. `readRoleNote` (src/run/run-prompt.ts) cuts every notes file at
11
+ * ROLE_NOTES_MAX_BYTES *characters* on read (truncateWithNote's text.length bound), but every
12
+ * operator surface reads the file whole — so a hand edit or old build can leave a note the
13
+ * operator sees and the role never does. This row warns on that gap without touching the file.
14
+ * The over-cap walk is shared with the status snapshot's collectRoleNotesOverCap
15
+ * (src/status/status-role-notes.ts), so the row and the status alert cannot disagree about
16
+ * which notes are over the bound; null means the notes dir is missing or unreadable.
17
+ * Read-only against .tumwater/, like every check. Never throws. */
18
+ export function checkRoleNotes(root) {
19
+ const over = readRoleNotesOverCap(root);
20
+ if (over === null) {
21
+ // Missing or unreadable notes dir: nothing recorded yet (the write path creates it).
22
+ return okOutcome("no role notes recorded yet");
23
+ }
24
+ if (over.length === 0) {
25
+ return okOutcome(`every role note is within the ${ROLE_NOTES_MAX_BYTES}-character read bound`);
26
+ }
27
+ const first = over
28
+ .slice(0, ROLE_NOTES_NAMED_CAP)
29
+ .map((o) => `${o.role} (${o.chars})`)
30
+ .join(", ");
31
+ const more = over.length > ROLE_NOTES_NAMED_CAP ? ` and ${over.length - ROLE_NOTES_NAMED_CAP} more` : "";
32
+ return warnOutcome(`${plural(over.length, "note", "notes")} across ${plural(over.length, "role", "roles")} ${agree(over.length, "exceeds", "exceed")} the ${ROLE_NOTES_MAX_BYTES}-character read bound: ${first}${more} — each role's prompt shows only the truncated head, so rewrite the note under the cap (tumwater role <id> --notes-file <path> or the tumwater-notes shim)`);
33
+ }
@@ -6,16 +6,18 @@ import { checkOrphans } from "./doctor-orphans.js";
6
6
  import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours, checkSubscriptionWindow } from "./doctor-holds.js";
7
7
  import { checkRootInstall } from "./doctor-install.js";
8
8
  import { checkNotify } from "./doctor-notify.js";
9
+ import { checkRoleNotes } from "./doctor-role-notes.js";
9
10
  import { checkLaunchServicesPorts } from "./doctor-launch-services.js";
10
11
  import { plural } from "../text/phrases.js";
11
12
  import { shortSha } from "../text/format.js";
12
13
  import { failOutcome } from "./outcome.js";
13
14
  import { errorMessage } from "../text/text.js";
14
15
  import { checkBackendBinary, checkBrief, checkBootstrap, checkClaudeTranscripts, checkExecPath, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkOpencodeVersion, checkNodeVersion, checkPiExtensions, checkRepo, } from "./doctor-checks.js";
15
- import { checkBuild, checkBuildCheck, checkCiParity, checkDiskSpace, checkEventLog, checkFlakyChecks, checkPostMergeChurn, checkQaCoverage, checkStateDir, } from "./doctor-project-checks.js";
16
+ import { checkPrinciples } from "./doctor-principles.js";
17
+ import { checkBuild, checkBuildCheck, checkCiParity, checkDiskSpace, checkDroppedPostMergeReviews, checkEventLog, checkFlakyChecks, checkMainDiffRules, checkPostMergeChurn, checkQaCoverage, checkStateDir, } from "./doctor-project-checks.js";
16
18
  import { sampleFreeBytes } from "../gates/disk-gate.js";
17
19
  import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderConcurrency, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
18
- import { checkFeedbackFiling, checkImageInput, checkTierModels } from "./doctor-model-checks.js";
20
+ import { checkFeedbackFiling, checkTierModels } from "./doctor-model-checks.js";
19
21
  import { checkFallbackChain, checkFallbackModel } from "./doctor-fallback-checks.js";
20
22
  import { checkAnthroApiKey, checkClaudeAuth, checkClaudeVersion } from "./doctor-claude-checks.js";
21
23
  import { checkOpencode } from "./doctor-opencode-checks.js";
@@ -23,7 +25,7 @@ import { checkCodexAccount, checkCodexAuth, checkCodexHome, checkCodexLeaks, che
23
25
  import { backendQualifiedSelector } from "../config/config-views.js";
24
26
  import { seamsInUse } from "../config/config-fallbacks.js";
25
27
  import { checkBacklogHeadings, checkFixClaims, checkLiveFinished, checkPlanDependencies, checkStalledEntries, checkStrandedPlans, checkValidationGaps } from "./doctor-backlog-plans.js";
26
- import { checkRefusedEntries, checkRegressedFixes, checkStaleBugs, checkStaleQuestions, checkWaitingBugs } from "./doctor-backlog-bugs.js";
28
+ import { checkRefusedEntries, checkRegressedFixes, checkStaleBugs, checkStaleQuestions } from "./doctor-backlog-bugs.js";
27
29
  import { checkMergeQueueFiles, checkPostReviews, checkStaleQueuedMerges, checkStalePrompts, checkUncommittedBacklog } from "./doctor-backlog.js";
28
30
  /** Pre-flight environment check (`tumwater doctor`). The harness's preconditions are
29
31
  * scattered across fail-fast checks that each command re-runs on its own (requireReadyRepo in
@@ -59,7 +61,7 @@ export async function runDoctorCheck(run) {
59
61
  * just the named checks, in report order; a skipped check is never evaluated, and a named
60
62
  * conditional check that returns null leaves no row. A check that throws becomes its own fail
61
63
  * row, and every other check still runs. */
62
- export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe, probeImages = "never", only, diskSample = sampleFreeBytes) {
64
+ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe, only, diskSample = sampleFreeBytes) {
63
65
  // Loaded once for the checks that read config (repo's baseBranch); a broken file stays
64
66
  // undefined — checkInit reports it verbatim — so doctor still runs every other check.
65
67
  const { config } = loadConfigSafe(root);
@@ -86,6 +88,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
86
88
  { name: "init", run: () => checkInit(root) },
87
89
  { name: "config key names", run: () => checkConfigKeyNames(root) },
88
90
  { name: "brief", run: () => checkBrief(root) },
91
+ { name: "principles", run: () => checkPrinciples(root) },
89
92
  // New-project bootstrap (plans/work-ratio.md, part 2/2): an active hold reports as ok
90
93
  // information, never a warning — a young project is not misconfigured.
91
94
  { name: "bootstrap", run: () => checkBootstrap(root, config) },
@@ -104,7 +107,6 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
104
107
  { name: "over-cap model", run: () => checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
105
108
  { name: "fallback chain", run: () => checkFallbackChain(root) },
106
109
  { name: "models for each tier", run: () => checkTierModels(root) },
107
- { name: "image input", run: () => checkImageInput(root, undefined, undefined, undefined, probeImages, pathEnv) },
108
110
  { name: "claude auth", run: () => checkClaudeAuth(root, pathEnv) },
109
111
  { name: "claude api key", run: () => checkAnthroApiKey(root, undefined, pathEnv) },
110
112
  { name: "claude version", run: () => checkClaudeVersion(root, pathEnv) },
@@ -143,6 +145,10 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
143
145
  { name: "check", run: () => checkBuildCheck(root, config) },
144
146
  { name: "ci parity", run: () => checkCiParity(root, config, Date.now()) },
145
147
  { name: "qa coverage", run: () => checkQaCoverage(root, Date.now()) },
148
+ // The role-notes read gap (plans/role-notes): every operator surface reads a role's notes
149
+ // file whole while the run prompt truncates it at ROLE_NOTES_MAX_BYTES; this row surfaces the
150
+ // gap read-only, beside the other per-role observer rows (feedback filing, workspaces).
151
+ { name: "role notes", run: () => checkRoleNotes(root) },
146
152
  { name: "root install", run: () => checkRootInstall(root) },
147
153
  // The review gate's flake memory, surfaced read-only: naming which checks the gate kept
148
154
  // excusing as flakes (ok/warn only, so a flaky suite never fails a scripted pre-flight).
@@ -158,11 +164,12 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
158
164
  { name: "plan dependencies", run: () => checkPlanDependencies(root) },
159
165
  { name: "stale questions", run: () => checkStaleQuestions(root) },
160
166
  { name: "stale bugs", run: () => checkStaleBugs(root) },
161
- { name: "waiting bugs", run: () => checkWaitingBugs(root) },
162
167
  { name: "regressed fixes", run: () => checkRegressedFixes(root) },
163
168
  { name: "stale merge-queue entries", run: () => checkStaleQueuedMerges(root, Date.now()) },
164
169
  { name: "merge-queue files", run: () => checkMergeQueueFiles(root) },
165
170
  { name: "post-merge reviews", run: () => checkPostReviews(root) },
171
+ { name: "main diff rules", run: () => checkMainDiffRules(root) },
172
+ { name: "dropped post-merge reviews", run: () => checkDroppedPostMergeReviews(root) },
166
173
  { name: "stale prompts", run: () => checkStalePrompts(root, Date.now()) },
167
174
  { name: "uncommitted backlog", run: () => checkUncommittedBacklog(root, config) },
168
175
  // The running team's own view of its build (staleness and what auto-restart made of it)
@@ -20,7 +20,7 @@ export const AUTH_CHECK_MAX_OUTPUT_BYTES = 1024 * 1024;
20
20
  * spawn closes for git). stderr is ignored rather than piped, since nothing reads it and a
21
21
  * full stderr pipe would block the child. `timeoutMs` and `maxOutputBytes` are test seams;
22
22
  * production callers pass their own named constants. One home of the shape, shared by the
23
- * model checks' credential/version probes, the tier-model probes, and the image-input probe —
23
+ * model checks' credential/version probes, and the tier-model probes —
24
24
  * before the split each doctor module carried its own byte-for-byte copy. The credential probes'
25
25
  * named budgets (AUTH_CHECK_TIMEOUT_MS, AUTH_CHECK_MAX_OUTPUT_BYTES) live beside it here, so the
26
26
  * backend check modules share them without importing them from one another. */