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
@@ -6,16 +6,12 @@ import { abortSync, ensureWorktree, resetWorktreeToMain } from "../git/worktree.
6
6
  import { leaseSlot } from "../git/worktree-pool.js";
7
7
  import { slotCount } from "../config/config.js";
8
8
  import { useWorktree } from "../git/worktree-use.js";
9
- import { readPiListedModels } from "../pi/pi-listed-models.js";
10
9
  import { branchName, worktreePath, mergeRefName, rejectedRefName } from "../paths.js";
11
10
  import { logEventBestEffort } from "../events/events.js";
12
11
  import { buildConflictHandbackNote, buildSalvageRestoreNote } from "../prompt/gate-prompts.js";
13
12
  import { buildConflictDiscardNote, buildMainKnownRedNote, buildRevisionNote, buildRejectedReviewNote, } from "../prompt/gate-notes.js";
14
- import { modelSelectorField, resolvedBackendConfig, reviewRunConfig, reviewedAfterMerge, scheduleConfigForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
13
+ import { modelSelectorField, resolvedBackendConfig, reviewRunConfig, reviewedAfterMerge, scheduleConfigForRole, seamHoldKey, } from "../config/config-views.js";
15
14
  import { fallbackRoleConfig } from "../config/config-fallbacks.js";
16
- import { imageReroute, seamImageInput } from "../agent/image-input.js";
17
- import { piModelsPath, readPiProviders } from "../pi/pi-models.js";
18
- import { peekPrompt, peekRolePrompt } from "../inbox/inbox.js";
19
15
  import { planRunStart, planWithSalvagedWork, takesSalvagedWork } from "../run/run-resume.js";
20
16
  import { restoreSalvagedWork } from "../git/slot-salvage.js";
21
17
  import { DEFAULT_BACKEND } from "../agent/agent-backend.js";
@@ -30,7 +26,8 @@ import { applyRevision, applyWithConflicts } from "./revision.js";
30
26
  import { conflictedFileHunks, hasConflictMarkers, mainCommitsTouching } from "../merge/merge-git.js";
31
27
  import { changedFiles } from "../git/git-diff.js";
32
28
  import { handlePiResultPhase } from "./runner-outcome.js";
33
- import { mainRedGate, mainRedHealNote } from "../baseline/main-red.js";
29
+ import { mainRedHealNote, namedFailures } from "../baseline/main-red.js";
30
+ import { watchedBaseline } from "../baseline/main-baseline.js";
34
31
  import { extractCiResult, extractFlow, extractFiled } from "../verdict/reply-contract.js";
35
32
  /** The intent the author needs to resolve a handed-back change: the change's own commit message
36
33
  * and the main commits that touched its conflicted files since its merge-base. Mirrors what
@@ -90,60 +87,6 @@ export async function runPhase(ctx) {
90
87
  let cfg = onFallback && fallback !== null
91
88
  ? { ...fallback, minRunIntervalSeconds: primaryCfg.minRunIntervalSeconds }
92
89
  : primaryCfg;
93
- // Image-input reroute (plans/image-input.md part 4/5). A pending one-shot marker means
94
- // THIS run is the blind-read reroute: start directly on the image-capable target the
95
- // prior run recorded, over the kept worktree, in a fresh session. The marker is consumed
96
- // (cleared) here so it does not route a later run; imageRerouteActive stays true for the
97
- // whole run so the verdict's one-hop guard fails a second blind read instead of
98
- // rerouting again. A preflight (below) sets the same active flag without a marker.
99
- s.imageRerouteActive = false;
100
- let preflightBlocked = false;
101
- if (s.imageReroute) {
102
- const r = s.imageReroute;
103
- cfg = withModelOverrides(ctx.config, r.tier, {
104
- backend: r.backend,
105
- provider: r.provider,
106
- model: r.model,
107
- thinking: r.thinking,
108
- });
109
- // The marker is NOT cleared here: planRunStart reads its `from`/`path` when it builds
110
- // the handoff note (and clears it then), so a run skipped before the plan (an empty
111
- // inbox) leaves the reroute pending for the next eligible run.
112
- s.imageRerouteActive = true;
113
- }
114
- else {
115
- // Preflight (plans/image-input.md "Attachments"): a queued prompt carrying an
116
- // `[image attached: …]` line needs a model that can read images BEFORE any run starts,
117
- // so a text-only run hops straight to the target instead of spending a failed run
118
- // learning it; with no target the run errors without spawning pi.
119
- const nextPrompt = ctx.role === DIRECTOR_ROLE ? peekPrompt(ctx.root) : peekRolePrompt(ctx.root, ctx.role);
120
- const carriesImages = nextPrompt !== null && /^\[image attached: .+\]$/m.test(nextPrompt);
121
- if (carriesImages) {
122
- const providers = readPiProviders(piModelsPath());
123
- const listed = readPiListedModels(ctx.root);
124
- if (seamImageInput(cfg, providers, undefined, listed) !== true) {
125
- const target = imageReroute(ctx.config, ctx.role, ctx.rerouteRegime(now), undefined, null, undefined, null, listed);
126
- if (target !== null) {
127
- cfg = target.config;
128
- s.imageRerouteActive = true;
129
- logEventBestEffort(ctx.root, {
130
- source: ctx.role,
131
- type: "image_reroute",
132
- run: s.runs + 1,
133
- from: primaryCfg.model,
134
- to: target.config.model,
135
- tier: target.tier,
136
- preflight: true,
137
- });
138
- }
139
- else {
140
- preflightBlocked = true;
141
- s.lastError =
142
- `an inbox message for ${ctx.role} attaches an image, but no configured tier can read it`;
143
- }
144
- }
145
- }
146
- }
147
90
  // Capture what this run runs on (the orchestrator's budget handback matches it against
148
91
  // the fallback pair) and clear any stale handback flag: an abort request that lands while
149
92
  // the runner is idle must not name a later run's resume.
@@ -179,19 +122,11 @@ export async function runPhase(ctx) {
179
122
  source: ctx.role,
180
123
  type: "run_start",
181
124
  run: runNumber,
182
- // A run routed onto the image-capable target — a blind-read handoff or an attachment
183
- // preflight — is stamped so the feed shows the reroute and the model it landed on.
184
- ...(s.imageRerouteActive ? { reroute: "image" } : {}),
185
125
  ...modelSelectorField(cfg),
186
126
  });
187
127
  let outcome;
188
128
  try {
189
- // The preflight can end the run before any pi spawns: a text-only runner with an image
190
- // attachment and no image-capable target has nothing to run, so it errors without
191
- // spending a model call. Everything else runs the pipeline as usual.
192
- outcome = preflightBlocked
193
- ? { result: "error" }
194
- : await ctx.runOnce(cfg, { fallback, probe: probing, primary: primaryCfg });
129
+ outcome = await ctx.runOnce(cfg, { fallback, probe: probing, primary: primaryCfg });
195
130
  }
196
131
  catch (err) {
197
132
  outcome = { result: "error" };
@@ -257,17 +192,6 @@ export async function runOncePhase(ctx, cfg, fallbackCtx) {
257
192
  if (plan === null)
258
193
  return { result: "skipped" };
259
194
  if (ctx.role === DIRECTOR_ROLE) {
260
- // The image-reroute one-shot marker routes exactly ONE run onto the image-capable
261
- // target (plans/image-input.md part 4/5, INV-J2): for a role runner, planRunStart consumes
262
- // it after reading its handoff-note facts. The director never resumes, so planRunStart's
263
- // image-reroute branch never runs for it and the marker would otherwise linger and
264
- // re-route every later director run off its own tier. It is cleared here — after this
265
- // run is confirmed to run (an empty-inbox skip above returns before this, leaving the
266
- // reroute pending for the next eligible run) — so the rerouted director prompt is
267
- // delivered exactly once and the director is back on its own tier next.
268
- // A promptless recovery run delivers no prompt, so it leaves a pending reroute for the next.
269
- if (plan.prompt !== null)
270
- s.imageReroute = undefined;
271
195
  // The director keeps its dedicated worktree (it runs outside the permit, consumes its
272
196
  // config-request file at that exact path, and never resumes). Hold it from before
273
197
  // ensureWorktree — creating or resetting it is part of the use — to the run's end
@@ -391,7 +315,7 @@ leasedDir) {
391
315
  // recovery merge raced the slot's batches into `merge_blocked`). A salvaged leftover
392
316
  // ENDS the run: the role holds one merge ref, and the leftover owns it until its
393
317
  // merge resolves. With nothing to salvage the branch holds nothing either, so the reset
394
- // below leaves pristine main for the red-main gate.
318
+ // below leaves pristine main.
395
319
  // A permanent reviewer config error — held from re-queueing — clears once the resolved
396
320
  // reviewer selector changes, so fixing the model config recovers the pin automatically
397
321
  // (BUGS.md 2026-10-06).
@@ -452,33 +376,26 @@ leasedDir) {
452
376
  // nothing, so it ends here rather than reach the authoring run with a null prompt.
453
377
  if (plan.prompt === null)
454
378
  return { result: "skipped" };
455
- // Red-main baseline gate (src/baseline/main-red.ts): the worktree is pristine main right
456
- // now — verify main's own suite before spending an authoring run on top of it. Only roles
457
- // whose diff can carry code changes are blocked; resume runs skip this by construction
458
- // (their worktree is not pristine main). The `ci` healer is exempt because its fix is
459
- // the team's only way back to green, so instead of blocking it we hand it the failure
460
- //the same check runs, but a red main appends a <main-red>
461
- // note to this run's prompt so it reproduces and fixes that failure rather than hunting
462
- // blind.
379
+ // The `ci` healer's red-main hand-off (src/baseline/main-red.ts): the worktree is pristine
380
+ // main right now, so a red main appends a <main-red> note to this run's prompt and the
381
+ // healer reproduces and fixes that failure rather than hunting blind. Every other role
382
+ // authors on the red main unblocked — the red-main authoring block came off with the
383
+ // pre-merge blocks, parts 3g/3–3j/3 and 3d/3–3f/3.
463
384
  if (ctx.baseRole === "ci") {
464
385
  const note = await mainRedHealNote(ctx.root, ctx.role, wt);
465
386
  if (note)
466
387
  prompt = appendPromptNote(prompt, note);
467
388
  }
468
389
  else {
469
- const knownRed = [];
470
- const blocked = await mainRedGate(ctx.root, ctx.role, wt, undefined, (tests) => knownRed.push(tests));
471
- if (blocked) {
472
- // The dequeued prompt never ran: put it back in its queue before returning, so it is
473
- // not lost to a restart (the pending field is memory-only) or dropped by the next
474
- // run's outcome handling — the queue is the durable store, and once main is green
475
- // the next run dequeues it again (criterion b).
476
- ctx.pending.requeueUnfulfilled(userPrompt);
477
- ctx.pending.clear();
478
- return blocked;
479
- }
480
- if (knownRed[0])
481
- prompt = appendPromptNote(prompt, buildMainKnownRedNote(knownRed[0]));
390
+ // Every other role proceeds on the red main, told which tests already fail (the
391
+ // <main-known-red> note, restored 2026-10-25): a pure read of the main watch's persisted
392
+ // verdict (watchedBaseline runs no check), so the block appears only when a verdict names
393
+ // failures and a role's start pays nothing for it. `ci` owns the repair without
394
+ // monopolizing it — a fix from any role lands like any other change.
395
+ const baseline = await watchedBaseline(wt);
396
+ const tests = baseline?.status === "red" ? namedFailures(baseline) : [];
397
+ if (tests.length > 0)
398
+ prompt = appendPromptNote(prompt, buildMainKnownRedNote(tests));
482
399
  }
483
400
  }
484
401
  // A change handed back after its merges kept conflicting — or a persisted hand-back the
@@ -208,16 +208,6 @@ export function loadRunnerState(root, role) {
208
208
  v.probeAt = healNumber(v.probeAt, 0);
209
209
  v.cooldownMs = healNumber(v.cooldownMs, 0);
210
210
  });
211
- // The image-reroute marker is a one-shot nested record consumed by the next run; a torn
212
- // scalar is dropped (its consumers read/mutate fields), and the tier is repaired to a
213
- // known tier name so a corrupt tier cannot route a rerouted run to a phantom seam.
214
- if (s.imageReroute !== undefined) {
215
- const healed = healRecord(s.imageReroute, () => { });
216
- s.imageReroute =
217
- healed && (healed.tier === "small" || healed.tier === "default" || healed.tier === "strong")
218
- ? healed
219
- : undefined;
220
- }
221
211
  return s;
222
212
  }
223
213
  /** Per-role cache of the persisted live claim, keyed by the state file's stat — one stat per
@@ -6,7 +6,7 @@ import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
6
6
  import { promptPreview } from "../inbox/inbox.js";
7
7
  import { assembleRunPrompt } from "../run/run-prompt.js";
8
8
  import { RunnerPi } from "./runner-pi.js";
9
- import { configForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
9
+ import { configForRole, seamHoldKey, } from "../config/config-views.js";
10
10
  import { fallbackRoleConfig } from "../config/config-fallbacks.js";
11
11
  import { PendingPrompt } from "../inbox/pending-prompt.js";
12
12
  import { modelFallbackActive } from "./model-fallback.js";
@@ -73,13 +73,6 @@ export class RoleRunner {
73
73
  * resolve to, since different roles may run on different pairs.
74
74
  * Transient — never persisted — and captured fresh at every run start. */
75
75
  runPair;
76
- /** Whether the budget gate currently has the team on its free fallback pairs (src/gates/
77
- * budget-gates.ts onFallback, set alongside `config` by the orchestrator's poll): an
78
- * image reroute may then look only among the free fallback pairs, never a priced primary
79
- * (plans/image-input.md "Reroute target" "Regime"). Pure scheduler input, never whether
80
- * the runner itself is mid-fallback; defaults false, so standalone-runner tests stay on the
81
- * primary regime unless they set it. */
82
- budgetFallbackActive = false;
83
76
  /** The merge failure this run's leftover recovery is retrying, if any (a retriable merger
84
77
  * outcome that kept the pin, which recovery re-queued): set in finishRecoveryRun and
85
78
  * attached to the returned RunOutcome so applyRunOutcome can feed it into the error streak
@@ -185,30 +178,7 @@ export class RoleRunner {
185
178
  * active (a due probe runs the primary), else the role's own resolved config. The hold and
186
179
  * start passes read this so a role mid-episode is keyed by the provider it will actually use.
187
180
  * Pure read of config + state + the caller's clock. */
188
- /** Which pair set an image reroute may look among (plans/image-input.md "Reroute target"
189
- * "Regime"): the free fallback pairs while the budget gate is on fallback OR the role is
190
- * in a model-fallback episode; otherwise the primary tiers. One home for the rule, shared
191
- * by the target-resolution sites (the verdict's reroute arm, the attachment preflight). */
192
- rerouteRegime(now) {
193
- if (this.budgetFallbackActive)
194
- return "fallback";
195
- if (modelFallbackActive(this.state.modelFallback, now))
196
- return "fallback";
197
- return "primary";
198
- }
199
181
  runConfig(now) {
200
- // A pending image-input reroute (plans/image-input.md part 4/5) outranks the role's own
201
- // tier and its model fallback: the next run starts on the image-capable target the
202
- // blind-read run recorded, rebuilt from the stored seam over the live config.
203
- if (this.state.imageReroute) {
204
- const r = this.state.imageReroute;
205
- return withModelOverrides(this.config, r.tier, {
206
- backend: r.backend,
207
- provider: r.provider,
208
- model: r.model,
209
- thinking: r.thinking,
210
- });
211
- }
212
182
  const fallback = fallbackRoleConfig(this.config, this.role);
213
183
  if (fallback !== null && modelFallbackActive(this.state.modelFallback, now))
214
184
  return fallback;
@@ -323,7 +293,6 @@ export class RoleRunner {
323
293
  warn: (message) => this.warn(message),
324
294
  runSignal: () => this.runSignal(),
325
295
  runPrompt: (backend) => this.runPrompt(backend),
326
- rerouteRegime: (now) => this.rerouteRegime(now),
327
296
  get recoveryFailure() {
328
297
  return self.recoveryFailure;
329
298
  },
@@ -8,9 +8,9 @@
8
8
  import path from "node:path";
9
9
  import { DAY_MS } from "../text/datetime.js";
10
10
  import { hasOutstandingWork } from "../runner/runner-state.js";
11
- import { backlogHolds, eligibleEntries } from "../backlog/backlog-eligibility.js";
11
+ import { backlogHolds, eligibleEntries, roleSection } from "../backlog/backlog-eligibility.js";
12
12
  import { entryKey } from "../backlog/backlog-parts.js";
13
- import { actionableEntryRanges, actionableEntryRangesFor } from "../backlog/backlog-structure.js";
13
+ import { actionableEntryRanges, actionableEntryRangesFor } from "../backlog/backlog-index-data.js";
14
14
  import { readTextOrNull } from "../files/files.js";
15
15
  import { changeBaseRev, fileContentAt } from "../git/git.js";
16
16
  import { INSTANCE_ROLES, baseRoleOf, configuredInstances } from "../roles/runner-ids.js";
@@ -27,20 +27,13 @@ export function planBacklogTarget(config) {
27
27
  return configuredInstances(config, "feature") + 1;
28
28
  }
29
29
  /** Does PLANS.md already hold the plan role's target (planBacklogTarget eligible plans), with no
30
- * plan marked needs-review or needs-replan? The charter makes splitting or replanning a marked
30
+ * plan marked needs-review? The charter makes splitting or replanning a marked
31
31
  * plan outrank the stop, so a marked entry keeps the plan role running; otherwise a plan run can
32
32
  * only end in TUMWATER_NOTHING_TO_DO and the scheduler defers it (deferRunReason). */
33
33
  export function planBacklogFull(root, config) {
34
34
  if (eligibleEntries(root, "feature").length < planBacklogTarget(config))
35
35
  return false;
36
- return !backlogHolds(root).plans.some((h) => h === "needs-review" || h === "needs-replan");
37
- }
38
- /** The backlog file and section a role's entries live under: PLANS.md's `## Planned` for
39
- * feature, BUGS.md's `## Open` for bugfix. An instance id resolves through its base role. */
40
- function roleSection(role) {
41
- return baseRoleOf(role) === "bugfix"
42
- ? { file: "BUGS.md", section: "Open" }
43
- : { file: "PLANS.md", section: "Planned" };
36
+ return !backlogHolds(root).plans.some((h) => h === "needs-review");
44
37
  }
45
38
  /** Every entry key listed under a role's backlog section, held or not: the set that separates a
46
39
  * claim whose entry left the section (`left`) from one that is still listed but ineligible
@@ -9,11 +9,14 @@ import { pausedRoles, pausedRolesUntil, scheduledStopAt, scheduledWakes, standin
9
9
  import { orchestratorAlive, readOrchestratorInfo, } from "../team/orchestrator-info.js";
10
10
  import { bootstrapHoldsRole, bootstrapStatus } from "../gates/bootstrap-gates.js";
11
11
  import { readMergeQueue } from "../merge/merge-queue.js";
12
- import { collectImageProbeFailures, collectRoleRows } from "./status-roles.js";
12
+ import { collectRoleRows } from "./status-roles.js";
13
13
  import { collectInboxes } from "./status-inbox.js";
14
+ import { collectRoleNotesOverCap } from "./status-role-notes.js";
14
15
  import { collectBudget } from "./status-budget.js";
15
16
  import { collectRootInstallDrift } from "./status-install.js";
16
17
  import { detectBuildCheck } from "../build/build-check-detect.js";
18
+ import { briefBlockMissing, briefBlockOverCap } from "../brief/brief.js";
19
+ import { principlesBlockOverCap } from "../prompt/principles.js";
17
20
  /** The earlier of the two optional stop deadlines (`run --for`'s and a scheduled stop's). */
18
21
  function earliestStop(a, b) {
19
22
  return a === undefined ? b : b === undefined ? a : Math.min(a, b);
@@ -23,6 +26,128 @@ function earliestStop(a, b) {
23
26
  export function scopeSnapshotToRoles(snap, roles) {
24
27
  return { ...snap, roles: snap.roles.filter((s) => roleInScope(s.role, roles)) };
25
28
  }
29
+ /** The run-state header fields (running/pid/build) of the snapshot. */
30
+ function snapshotRunFields(running, info) {
31
+ return {
32
+ running,
33
+ pid: info?.pid,
34
+ build: running && info?.build ? info.build : null,
35
+ };
36
+ }
37
+ /** The director and per-role inbox fields and the open question count of the snapshot. The
38
+ * `collectInboxes` read stays in `snapshot`; the
39
+ * builder recomposes the destructured values and reads the open question count from `root`. */
40
+ function snapshotInboxFields(root, inboxes) {
41
+ return {
42
+ inbox: inboxes.inbox,
43
+ inboxPrompts: inboxes.inboxPrompts,
44
+ inboxFiles: inboxes.inboxFiles,
45
+ inboxQueuedAt: inboxes.inboxQueuedAt,
46
+ inboxNotBefore: inboxes.inboxNotBefore,
47
+ roleInbox: inboxes.roleInbox,
48
+ roleInboxPrompts: inboxes.roleInboxPrompts,
49
+ questions: openQuestions(root).length,
50
+ };
51
+ }
52
+ /** The spend/billing fields of the snapshot: the budget, the per-backend subscription windows
53
+ * (and their claude-window spelling) and the running-only chain/provider holds. */
54
+ function snapshotBudgetFields(budget, subscriptionWindows, subscriptionOnly, claudeBlock, running, info) {
55
+ return {
56
+ // Unconditional (never null): a disabled team still shows its spend and the badge is
57
+ // the affordance for SETTING a cap. models.json itself is stat-cached inside pi-models.ts,
58
+ // so an unchanged catalog costs one stat per poll, not a re-read plus parse.
59
+ budget,
60
+ // One window block per subscription backend in use, raw for `status --json` and the
61
+ // parts 3b/9–3e/9 displays; omitted when no subscription backend reported a window.
62
+ ...(subscriptionWindows.length > 0 ? { subscriptionWindows } : {}),
63
+ subscriptionOnly,
64
+ ...(claudeBlock
65
+ ? {
66
+ claudeWindow: {
67
+ windows: claudeBlock.windows,
68
+ primary: claudeBlock.primary,
69
+ only: subscriptionOnly,
70
+ heldUntil: claudeBlock.heldUntil,
71
+ },
72
+ }
73
+ : {}),
74
+ ...(running && info?.chain && Object.keys(info.chain).length > 0 ? { chain: info.chain } : {}),
75
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped
76
+ // team, and only while a hold stands (an empty list is the same as none).
77
+ ...(running && info?.providerHolds && info.providerHolds.length > 0 ? { providerHolds: info.providerHolds } : {}),
78
+ };
79
+ }
80
+ /** The pause/quiet-hours fields of the snapshot: the paused flags and their deadlines, the
81
+ * maintenance quota and bootstrap holds, and the per-role quiet windows. */
82
+ function snapshotPauseFields(teamPause, markerRoles, roles, markerSet, capPaused, maintenanceQuota, bootstrapNow, roleRows, budgetPausedRoles, cfg, root, wakes, quiet) {
83
+ return {
84
+ paused: teamPause !== null,
85
+ pausedRoles: markerRoles,
86
+ pausedRunners: roles.filter((id) => runnerPaused(markerSet, id)),
87
+ capPaused,
88
+ maintenanceQuota,
89
+ // The new-project bootstrap hold (plans/work-ratio.md, part 2/2): the same read-only
90
+ // verdict the gate computes (bootstrapStatus), so the dashboards and the scheduler agree.
91
+ // The held set is computed with the gate's own predicate over the same role list.
92
+ bootstrap: bootstrapNow?.active
93
+ ? { plansDone: bootstrapNow.plansDone, untilPlansDone: bootstrapNow.untilPlansDone }
94
+ : undefined,
95
+ bootstrapHeld: bootstrapNow?.active
96
+ ? roleRows.filter((l) => bootstrapHoldsRole(l.role, openBugs(root).length > 0)).map((l) => l.role)
97
+ : [],
98
+ budgetPausedRoles,
99
+ // The per-role quiet windows' held roles (PLANS.md quietHoursPerRole): keyed role →
100
+ // window, so the dashboards can render the same `quiet until <end>` wording the team
101
+ // badge uses, scoped to the role's own schedule. The window string comes back out of
102
+ // the same config the hold was decided from, so the label cannot name a window that is
103
+ // not holding (roleQuietHold true implies a parseable, non-empty string there).
104
+ roleQuietPaused: Object.fromEntries(roleRows
105
+ .filter((l) => l.role !== DIRECTOR_ROLE && roleQuietHold(cfg.quietHoursPerRole, baseRoleOf(l.role), new Date()))
106
+ .map((l) => [l.role, cfg.quietHoursPerRole?.[baseRoleOf(l.role)] ?? ""])),
107
+ pausedUntil: teamPause?.until,
108
+ pausedRolesUntil: pausedRolesUntil(root),
109
+ scheduledWake: wakes[0],
110
+ scheduledWakes: wakes.length > 0 ? wakes : undefined,
111
+ pauseReason: teamPause?.reason,
112
+ quietHours: quiet.window ?? undefined,
113
+ inQuietHours: quiet.inWindow,
114
+ };
115
+ }
116
+ /** The merge-queue and doctor health fields of the snapshot (main-check, ci-parity and the
117
+ * build-check/brief/principles flags, plus the role notes over cap). */
118
+ function snapshotHealthFields(root, cfg, mergeQueue, roleNotesOverCap) {
119
+ return {
120
+ mergeQueue,
121
+ mainCheck: mainCheckForPoll(root, cfg),
122
+ ciParity: ciParityForPoll(root, cfg),
123
+ noDeclaredCheck: detectBuildCheck(root, cfg) === null ? true : undefined,
124
+ noBrief: briefBlockMissing(root) ? true : undefined,
125
+ briefTruncated: briefBlockOverCap(root) ? true : undefined,
126
+ ...(roleNotesOverCap.length > 0 ? { roleNotesOverCap } : {}),
127
+ principlesTruncated: principlesBlockOverCap(root) ? true : undefined,
128
+ };
129
+ }
130
+ /** The running-only resource fields of the snapshot (disk floor, install drift, permits and
131
+ * the run stop deadlines). */
132
+ function snapshotResourceFields(root, running, info, rootDrift) {
133
+ return {
134
+ // The disk floor's published state (plans/disk-floor.md, part 4/4): only while the
135
+ // orchestrator runs — the exit removes the info file, so a stale file beside a dead pid
136
+ // must not speak for a stopped team — and only when the last poll could measure the
137
+ // volume. Absent otherwise, so the surfaces render exactly as before.
138
+ disk: running && info?.disk ? info.disk : undefined,
139
+ // Independent of the team running: a stopped team's worktrees still borrow the root install.
140
+ rootInstallDrift: rootDrift.length ? rootDrift : undefined,
141
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
142
+ permits: running && info?.permits ? info.permits : undefined,
143
+ slotPool: running && info?.slotPool ? info.slotPool : undefined,
144
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
145
+ checkPermits: running && info?.checkPermits ? info.checkPermits : undefined,
146
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
147
+ runStopsAt: running ? earliestStop(info?.stopsAt, scheduledStopAt(root)) : undefined,
148
+ scheduledStopAt: running ? scheduledStopAt(root) : undefined,
149
+ };
150
+ }
26
151
  /** One fresh team snapshot for observers. `modelsPath` overrides pi's model definitions
27
152
  * location (default ~/.pi/agent/models.json) — a test seam, like doctor's pathEnv. `now` pins
28
153
  * the poll's clock for the budget figures (the persisted day-stamp check and the burn-rate
@@ -51,9 +176,9 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
51
176
  // `disk` and the rows agree.
52
177
  const slots = readSlotsState(root).slots;
53
178
  const roleRows = collectRoleRows(root, cfg, now, slots, modelsPath, markerSet);
54
- // The doctor's cached image-probe failures (part 5/5): one read per snapshot, labelled, so
55
- // the amber probe alert and `status --json` both answer from the same collector.
56
- const imageProbeFailures = collectImageProbeFailures(root, now);
179
+ // Every role whose notes file runs past the read bound (status-role-notes.ts): the standing
180
+ // twin of the doctor `role notes` row, raised as the amber role-notes-truncated alert.
181
+ const roleNotesOverCap = collectRoleNotesOverCap(root);
57
182
  // One inbox pass per poll (status-inbox.ts) serves all four director fields and the per-role
58
183
  // counts/entries from the same queuedRolePromptEntries reads.
59
184
  const { inbox, inboxPrompts, inboxFiles, inboxQueuedAt, inboxNotBefore, roleInbox, roleInboxPrompts } = collectInboxes(root, roles, now);
@@ -104,94 +229,18 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
104
229
  // The one-release claude spelling (see StatusSnapshot.claudeWindow), derived from the claude
105
230
  // block so `subscriptionWindows` stays the single source of the window data.
106
231
  const claudeBlock = subscriptionWindows.find((b) => b.backend === "claude");
232
+ // The maintenance allowance (plans/work-ratio.md, part 1b/4): the same pure verdict the
233
+ // gate folds, over the same event log, so the dashboards show the hold the scheduler
234
+ // enforces. `merges` is the one merge queue snapshot read above, so in-flight maintenance
235
+ // merges count once here and in mergeQueue.
236
+ const maintenanceQuota = maintenanceQuotaForPoll(root, cfg.maintenancePerWorkMerge, roleRows, new Set(merges.map((e) => e.role)), now);
107
237
  return {
108
- running,
109
- pid: info?.pid,
110
- build: running && info?.build ? info.build : null,
111
- inbox,
112
- inboxPrompts,
113
- inboxFiles,
114
- inboxQueuedAt,
115
- inboxNotBefore,
116
- roleInbox,
117
- roleInboxPrompts,
118
- questions: openQuestions(root).length,
119
- imageProbeFailures,
238
+ ...snapshotRunFields(running, info),
239
+ ...snapshotInboxFields(root, { inbox, inboxPrompts, inboxFiles, inboxQueuedAt, inboxNotBefore, roleInbox, roleInboxPrompts }),
120
240
  roles: roleRows,
121
- // Unconditional (never null): a disabled team still shows its spend and the badge is
122
- // the affordance for SETTING a cap. models.json itself is stat-cached inside pi-models.ts,
123
- // so an unchanged catalog costs one stat per poll, not a re-read plus parse.
124
- budget,
125
- // One window block per subscription backend in use, raw for `status --json` and the
126
- // parts 3b/9–3e/9 displays; omitted when no subscription backend reported a window.
127
- ...(subscriptionWindows.length > 0 ? { subscriptionWindows } : {}),
128
- subscriptionOnly,
129
- ...(claudeBlock
130
- ? {
131
- claudeWindow: {
132
- windows: claudeBlock.windows,
133
- primary: claudeBlock.primary,
134
- only: subscriptionOnly,
135
- heldUntil: claudeBlock.heldUntil,
136
- },
137
- }
138
- : {}),
139
- ...(running && info?.chain && Object.keys(info.chain).length > 0 ? { chain: info.chain } : {}),
140
- // Only while running: a stale info file beside a dead pid must not speak for a stopped
141
- // team, and only while a hold stands (an empty list is the same as none).
142
- ...(running && info?.providerHolds && info.providerHolds.length > 0 ? { providerHolds: info.providerHolds } : {}),
143
- paused: teamPause !== null,
144
- pausedRoles: markerRoles,
145
- pausedRunners: roles.filter((id) => runnerPaused(markerSet, id)),
146
- capPaused,
147
- // The maintenance allowance (plans/work-ratio.md, part 1b/4): the same pure verdict the
148
- // gate folds, over the same event log, so the dashboards show the hold the scheduler
149
- // enforces. `merges` is the one merge queue snapshot read above, so in-flight maintenance
150
- // merges count once here and in mergeQueue.
151
- maintenanceQuota: maintenanceQuotaForPoll(root, cfg.maintenancePerWorkMerge, roleRows, new Set(merges.map((e) => e.role)), now),
152
- // The new-project bootstrap hold (plans/work-ratio.md, part 2/2): the same read-only
153
- // verdict the gate computes (bootstrapStatus), so the dashboards and the scheduler agree.
154
- // The held set is computed with the gate's own predicate over the same role list.
155
- bootstrap: bootstrapNow?.active
156
- ? { plansDone: bootstrapNow.plansDone, untilPlansDone: bootstrapNow.untilPlansDone }
157
- : undefined,
158
- bootstrapHeld: bootstrapNow?.active
159
- ? roleRows.filter((l) => bootstrapHoldsRole(l.role, openBugs(root).length > 0)).map((l) => l.role)
160
- : [],
161
- budgetPausedRoles,
162
- // The per-role quiet windows' held roles (PLANS.md quietHoursPerRole): keyed role →
163
- // window, so the dashboards can render the same `quiet until <end>` wording the team
164
- // badge uses, scoped to the role's own schedule. The window string comes back out of
165
- // the same config the hold was decided from, so the label cannot name a window that is
166
- // not holding (roleQuietHold true implies a parseable, non-empty string there).
167
- roleQuietPaused: Object.fromEntries(roleRows
168
- .filter((l) => l.role !== DIRECTOR_ROLE && roleQuietHold(cfg.quietHoursPerRole, baseRoleOf(l.role), new Date()))
169
- .map((l) => [l.role, cfg.quietHoursPerRole?.[baseRoleOf(l.role)] ?? ""])),
170
- pausedUntil: teamPause?.until,
171
- pausedRolesUntil: pausedRolesUntil(root),
172
- scheduledWake: wakes[0],
173
- scheduledWakes: wakes.length > 0 ? wakes : undefined,
174
- pauseReason: teamPause?.reason,
175
- quietHours: quiet.window ?? undefined,
176
- inQuietHours: quiet.inWindow,
177
- mergeQueue,
178
- mainCheck: mainCheckForPoll(root, cfg),
179
- ciParity: ciParityForPoll(root, cfg),
180
- noDeclaredCheck: detectBuildCheck(root, cfg) === null ? true : undefined,
181
- // The disk floor's published state (plans/disk-floor.md, part 4/4): only while the
182
- // orchestrator runs — the exit removes the info file, so a stale file beside a dead pid
183
- // must not speak for a stopped team — and only when the last poll could measure the
184
- // volume. Absent otherwise, so the surfaces render exactly as before.
185
- disk: running && info?.disk ? info.disk : undefined,
186
- // Independent of the team running: a stopped team's worktrees still borrow the root install.
187
- rootInstallDrift: rootDrift.length ? rootDrift : undefined,
188
- // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
189
- permits: running && info?.permits ? info.permits : undefined,
190
- slotPool: running && info?.slotPool ? info.slotPool : undefined,
191
- // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
192
- checkPermits: running && info?.checkPermits ? info.checkPermits : undefined,
193
- // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
194
- runStopsAt: running ? earliestStop(info?.stopsAt, scheduledStopAt(root)) : undefined,
195
- scheduledStopAt: running ? scheduledStopAt(root) : undefined,
241
+ ...snapshotBudgetFields(budget, subscriptionWindows, subscriptionOnly, claudeBlock, running, info),
242
+ ...snapshotPauseFields(teamPause, markerRoles, roles, markerSet, capPaused, maintenanceQuota, bootstrapNow, roleRows, budgetPausedRoles, cfg, root, wakes, quiet),
243
+ ...snapshotHealthFields(root, cfg, mergeQueue, roleNotesOverCap),
244
+ ...snapshotResourceFields(root, running, info, rootDrift),
196
245
  };
197
246
  }