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
@@ -121,62 +121,45 @@ export const VALIDATION_GAP_TALLY = `One query counts both the verbatim line and
121
121
  export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${VALIDATION_GAP_TAGS.filter((t) => t !== "none").join("|")})' BUGS.md BUGS-FIXED.md | head -20`;
122
122
  /** The literal prefix every Needs-review note starts with. Exported so a reader that only has
123
123
  * to recognize one (the backlog index's ` [blocked: needs review]` mark) can match on it instead of
124
- * parsing the date or the wording out of the full template — the same split as
125
- * NEEDS_REPLAN_PREFIX. */
124
+ * parsing the date or the wording out of the full template. The rejected-final-round note the
125
+ * feature role appends (gate-notes.ts) also begins with this prefix, so it reads as a Needs-review
126
+ * note and is classified by entryHold accordingly. */
126
127
  export const NEEDS_REVIEW_PREFIX = `**Needs review `;
127
- /** The tag text before the held count, shared by the note templates and the parser
128
- * (backlog-eligibility's heldCount) so the two cannot drift: the live count on a Needs-review
129
- * or Needs-replan note is written inline as `(held N)`, with `N` one past the surviving prior
130
- * count (the feature role appends it, the plan role's split carries it via PRIOR_HELD_NOTE). */
128
+ /** The tag text before a held count, read by the backlog (backlog-eligibility's heldCount) so
129
+ * the note form and the parser cannot drift. No writer emits `(held N)` anymore — the feature
130
+ * note is flat — but heldCount still parses the tag from a Needs-review note line already on
131
+ * disk until part 2f/6 removes the reader. */
131
132
  export const HELD_TAG_PREFIX = `(held `;
132
133
  /** The markdown note the feature role appends to a plan it cannot finish in one run, so the plan
133
134
  * role can split it. Mirrors the **Refused …** note convention: a note the next fresh run reads,
134
135
  * with no code parsing it. Defined once so the feature and plan texts and the director's routing
135
- * clause cannot drift. The trailing `(held <N>)` tag (HELD_TAG_PREFIX) is the live held count,
136
- * written as one past the count surviving on the entry; it sits inside the line, so
137
- * NEEDS_REVIEW_PREFIX still recognizes the note. */
138
- export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run ${HELD_TAG_PREFIX}<N>)**`;
136
+ * clause cannot drift. No writer carries a `(held <N>)` count anymore: a too-large note stays
137
+ * flat, so a new hold never escalates to the operator's desk. */
138
+ export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
139
139
  /** The literal reason the automatic convergence hold writes into a held plan's note
140
140
  * (holdEntryAfterExhaustion, src/runner/revision.ts, builds the note as
141
141
  * `**Needs review … by <role>: could not converge — <surviving objections>**`, INV-BH4).
142
142
  * Exported so the writer and the reader share one literal: it sits between holdEntryAfterExhaustion
143
143
  * (which writes the note) and the plan role's charter (role-catalog.ts `find`) which reads it, so
144
144
  * reason to recognize the automatic hold as distinct from the feature role's too-large
145
- * ${NEEDS_REVIEW_NOTE} note and the ${NEEDS_REPLAN_PREFIX} note — both carry no
146
- * `could not converge` text, so the charter routes each hold to its own action without matching
147
- * on a second literal. Add no second spelling of this text. */
145
+ * ${NEEDS_REVIEW_NOTE} note — it carries no `could not converge` text, so the charter routes
146
+ * each hold to its own action without matching on a second literal. Add no second spelling of
147
+ * this text. */
148
148
  export const CONVERGENCE_HOLD_MARKER = "could not converge";
149
- /** The marker a lifted hold leaves behind, so a split or rewrite carries the count instead of
150
- * letting it reset: `**Previously held N**` on the note's line. The live count is `(held N)` on
151
- * the note; the surviving marker after a lift is `**Previously held N**` — heldCount reads the
152
- * first as the priority, then the second. */
149
+ /** The marker a lifted hold leaves behind, so a split or rewrite carried the count instead of
150
+ * letting it reset: `**Previously held N**` on the note's line. No writer emits this anymore (the
151
+ * plan role's split no longer carries a count) and heldCount no longer reads it, so it is inert;
152
+ * withoutHeldForOperatorNotes still drops a surviving `**Previously held N−1**` line beside a
153
+ * released at-threshold note until part 2f/6 retires the count machinery. */
153
154
  export const PRIOR_HELD_PREFIX = `**Previously held `;
154
- /** The literal marker a lift leaves on a split or rewrite so the held count survives instead of
155
- * resetting: a `**Previously held <N>**` line at the same count the lifted note carried (the
156
- * <N> here is the value the note's `(held N)` tag held). heldCount reads this as the fallback
157
- * when no live note is present, so the count round-trips a split. */
158
- export const PRIOR_HELD_NOTE = `${PRIOR_HELD_PREFIX}<N>**`;
159
- /** The literal prefix every Needs-replan note starts with. Exported so a reader that only has to
160
- * recognize one (the backlog index's ` [blocked: needs replan]` mark) can match on it instead of
161
- * parsing the date or the round count out of the full template. */
162
- export const NEEDS_REPLAN_PREFIX = `**Needs replan `;
163
- /** The markdown note the feature role appends under a plan whose change was rejected after its
164
- * final revision round, carrying the reviewer's objections back to the strong-tier plan role
165
- * instead of letting feature re-author the same plan. Same convention as NEEDS_REVIEW_NOTE: a
166
- * note a later fresh run reads, with no code parsing it. The trailing `(held <H>)` tag
167
- * (HELD_TAG_PREFIX) is the live held count, one past the count surviving on the entry; the
168
- * placeholder is `<H>` (for held) so it reads apart from the `<N>` review-round count earlier
169
- * in the same line. It sits inside the line, so NEEDS_REPLAN_PREFIX still recognizes the note. */
170
- export const NEEDS_REPLAN_NOTE = `${NEEDS_REPLAN_PREFIX}<YYYY-MM-DD> by feature: rejected after <N> review rounds ${HELD_TAG_PREFIX}<H>)**`;
171
- /** The literal prefix every Waiting-for-the-next-failure note starts with. Exported so a reader
172
- * that only has to recognize one (the backlog index's ` [blocked: waiting for the next failure]`
173
- * mark) can match on it instead of parsing the date or the wording out of the full template. */
174
- export const WAITING_FOR_FAILURE_PREFIX = `**Waiting for the next failure `;
175
- /** The markdown note the bugfix role writes under an open bug it cannot reproduce once it has made
176
- * the failure explain itself, so bugfix stops spending runs without new evidence. Same convention
177
- * as NEEDS_REPLAN_NOTE: a note a later fresh run reads. It lifts when whoever records the next
178
- * occurrence deletes the line, or by `tumwater bug --release <n>`. */
179
- export const WAITING_FOR_FAILURE_NOTE = `${WAITING_FOR_FAILURE_PREFIX}<YYYY-MM-DD> by bugfix: <what the next failure's message will show>**`;
155
+ /** The literal marker the plan role writes as its own line under the PLANS.md `## Planned` entry
156
+ * that declares this project's deterministic check, and the feature role reads as that entry's
157
+ * identity. It is the one decision point between the two no-check notes (gate-notes.ts
158
+ * buildNoCheckNote): no marker → the plan note (write the declaration entry); marker → the
159
+ * feature note (implement it). Defined once so the marker the plan note names, the one
160
+ * declarationPlanEntry recognizes (src/backlog/backlog.ts), and the catalog texts interpolate stay
161
+ * identical. */
162
+ export const CHECK_DECLARATION_MARKER = `**Declares the project check**`;
180
163
  /** How a role with no backlog (organize, clean, dedupe, perf, security, robustness, improve) finds
181
164
  * its one task, as numbered steps. Written against the observed failure: with nothing to point
182
165
  * at, a local model reads the codebase file by file — thirty whole-file reads, ~300 KB of tool
@@ -74,6 +74,23 @@ function inspectBodyLines(p, now) {
74
74
  }
75
75
  lines.push("");
76
76
  }
77
+ // The feedback observer's per-install filing ledger (role-view.ts's filings,
78
+ // feedback-filings.ts's filingRows): each issue this install filed, newest-first, its number,
79
+ // title, and age — the operator twin of the ledger that is otherwise the observer's only
80
+ // memory. Present only for feedback, so every other role's render stays byte-identical; an
81
+ // enabled-but-quiet observer reads as "none filed yet" rather than a truncated report.
82
+ if (p.filings) {
83
+ lines.push("## Filings", "");
84
+ if (p.filings.length === 0) {
85
+ lines.push("- none filed yet");
86
+ }
87
+ else {
88
+ for (const f of p.filings) {
89
+ lines.push(`- #${f.issue} ${f.title} — ${formatAge(now - f.filedAt)} ago`);
90
+ }
91
+ }
92
+ lines.push("");
93
+ }
77
94
  lines.push(...fencedOrNone("Next run's prompt", p.nextPrompt, "nothing to do this run"));
78
95
  // run-prompt.ts dequeues exactly one queued prompt per run, so with more than one
79
96
  // waiting the block above shows only the oldest — say so, so the count line and the
@@ -306,6 +323,8 @@ export function renderRoleMarkdown(p, now = Date.now()) {
306
323
  let cap = `- Daily cap: ${usd(dc.spentUsd)} of ${usd(dc.capUsd)} spent today (${dc.over ? "holding now — no new runs until local midnight" : "not holding"})`;
307
324
  if (dc.roleIds.length > 1)
308
325
  cap += ` · shared by ${dc.roleIds.join(", ")}`;
326
+ if (p.dailyCapHitAt !== undefined)
327
+ cap += ` · ~cap at ${clockHm(p.dailyCapHitAt)}`;
309
328
  lines.push(cap);
310
329
  }
311
330
  // The role's schedule (role-view.ts's nextRunAt/backoffSeconds/lastRunEndedAt, read from
@@ -15,22 +15,23 @@ import { pausedRoles } from "../team/team-state.js";
15
15
  import { loadRunnerState } from "../runner/runner-state-load.js";
16
16
  import { baseRoleOf, runnerIds, runnerPaused } from "./runner-ids.js";
17
17
  import { roleCapVerdicts } from "../gates/role-cap-gates.js";
18
+ import { projectCapHit } from "../budget/budget.js";
18
19
  import { yieldMultiplierFor } from "../scheduling/backoff.js";
19
20
  import { quietHoursStatus } from "../scheduling/quiet-hours.js";
20
21
  import { assembleRunPrompt } from "../run/run-prompt.js";
21
22
  import { configForRole, resolvedModelFields, reviewedAfterMerge, reviewRunConfig, roleSeamTier, seamConcurrencyKey } from "../config/config-views.js";
22
23
  import { fallbackPair, modelFallbackView } from "../config/config-fallbacks.js";
23
- import { fallbackModelPair, piModelsPath, seamContextWindow } from "../pi/pi-models.js";
24
+ import { fallbackModelPair, piModelsPath, seamContextWindowForRole } from "../pi/pi-models.js";
24
25
  import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
25
26
  import { formatModelSelector } from "../config/model-selector.js";
26
27
  import { roleImageInfo } from "../agent/image-input.js";
27
28
  import { readPiListedModels } from "../pi/pi-listed-models.js";
28
- import { readImageProbeCache } from "../doctor/image-probe.js";
29
29
  import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
30
30
  import { readClaudeAccount } from "../agent/claude/claude-account.js";
31
31
  import { readAccount, windowEntries } from "../agent/subscription-account.js";
32
32
  import { CONFIG_BASENAME, configPath, roleNotesPath } from "../paths.js";
33
33
  import { coverageRows, readQaCoverage } from "../run/qa-coverage.js";
34
+ import { filingRows, readFeedbackFilings } from "../run/feedback-filings.js";
34
35
  import { readTextOrNull, statOrNull } from "../files/files.js";
35
36
  import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
36
37
  import { finiteNumber, objectOrEmpty } from "../files/json-object.js";
@@ -210,14 +211,19 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
210
211
  const backendOverride = typeof backendOverrideRaw === "string" ? backendOverrideRaw : undefined;
211
212
  const capUsd = cfg.maxDailyCostUsdPerRole?.[base];
212
213
  let dailyCap;
214
+ let dailyCapHitAt;
213
215
  if (role !== DIRECTOR_ROLE && typeof capUsd === "number" && Number.isFinite(capUsd) && capUsd > 0) {
214
216
  const ids = runnerIds(cfg).filter((id) => baseRoleOf(id) === base);
215
217
  if (!ids.includes(role))
216
218
  ids.push(role);
217
219
  const runners = ids.map((id) => ({ role: id, state: id === role ? state : loadRunnerState(root, id) }));
218
220
  const verdict = roleCapVerdicts(runners, cfg.maxDailyCostUsdPerRole, now.getTime()).get(base);
219
- if (verdict)
221
+ if (verdict) {
220
222
  dailyCap = { capUsd, spentUsd: verdict.spentUsd, over: verdict.over, roleIds: verdict.roleIds };
223
+ const hitAt = projectCapHit({ spentUsd: verdict.spentUsd, capUsd }, now.getTime());
224
+ if (hitAt !== null)
225
+ dailyCapHitAt = hitAt;
226
+ }
221
227
  }
222
228
  // The role's notes, read for display. The same degradation as every other read here: a
223
229
  // missing, empty, or unreadable file is simply "no note yet". The director has none.
@@ -245,7 +251,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
245
251
  }
246
252
  // The resolved primary seam's declared context window. A run currently on the model
247
253
  // fallback may use a different window, which is the fallback's own documented state.
248
- const contextWindow = seamContextWindow(configForRole(cfg, role), root, modelsPath);
254
+ const contextWindow = seamContextWindowForRole(cfg, role, root, modelsPath);
249
255
  return {
250
256
  id: role,
251
257
  title: resolved.title,
@@ -272,7 +278,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
272
278
  ...(account !== null ? { billing: account.billing, billingBackend } : {}),
273
279
  ...(billingWindow !== undefined ? { billingWindow } : {}),
274
280
  chainStep,
275
- ...roleImageInfo(cfg, role, modelsPath, readImageProbeCache(root), readPiListedModels(root)),
281
+ ...roleImageInfo(cfg, role, modelsPath, readPiListedModels(root)),
276
282
  minRunIntervalSeconds: effective.minRunIntervalSeconds,
277
283
  ...(minRunIntervalOverride !== undefined ? { minRunIntervalOverride } : {}),
278
284
  ...(providerOverride !== undefined ? { providerOverride } : {}),
@@ -306,6 +312,13 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
306
312
  // payload and Markdown render stay byte-identical. The ledger read degrades to empty
307
313
  // coverage on a missing or torn file exactly as readQaCoverage already does.
308
314
  ...(role === "qa" ? { flowCoverage: coverageRows(readQaCoverage(root), now.getTime()) } : {}),
315
+ // The feedback observer's per-install filing ledger (plans/feedback-role.md 2/2), the twin
316
+ // of qa's flowCoverage block above: each issue this install filed, newest-first via the one
317
+ // filingRows ordering the prompt block and the operator read share. Present only for the
318
+ // canonical feedback id, so every other role's --json payload and Markdown render stay
319
+ // byte-identical. The ledger read degrades to an empty list on a missing or torn file
320
+ // exactly as readFeedbackFilings already does.
321
+ ...(role === "feedback" ? { filings: filingRows(readFeedbackFilings(root)) } : {}),
309
322
  inboxCount: queuedRolePromptCount(root, role),
310
323
  // The resolved primary seam's declared context window in tokens, or null when it cannot
311
324
  // be known (a non-pi seam, an unresolvable provider/model pair, or a model with no
@@ -332,5 +345,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
332
345
  ...(recentOutcomes.length > 0 ? { recentOutcomes } : {}),
333
346
  ...(usage.tokens !== 0 || usage.costUsd !== 0 || usage.notionalCostUsd !== 0 ? { usage } : {}),
334
347
  ...(dailyCap ? { dailyCap } : {}),
348
+ ...(dailyCapHitAt !== undefined ? { dailyCapHitAt } : {}),
335
349
  };
336
350
  }
@@ -30,9 +30,8 @@ const WORK_ROLES = new Set(["feature", "bugfix", "plan"]);
30
30
  * event log for `selfcheck` and `feedback`) is not a function of whether main moved. */
31
31
  export const OBSERVER_ROLES = new Set(["qa", "selfcheck", "feedback", "ci"]);
32
32
  /** The built-in maintenance roles that author code on their own cadence — the eight members
33
- * DEFERRABLE_ROLES and BASELINE_BLOCKED_ROLES share. Both sets list them because both charter
34
- * the same roles (defer-until-needed scheduling, red-main blocking), so the list lives once
35
- * here and a new maintenance role cannot be added to one set and missed in the other. */
33
+ * DEFERRABLE_ROLES charts. The list lives once here so a new maintenance role cannot be added
34
+ * to one list and missed in the other. */
36
35
  const CODE_MAINTENANCE_ROLES = [
37
36
  "organize",
38
37
  "coverage",
@@ -74,25 +73,6 @@ export function yieldScaledRole(role) {
74
73
  export function roleTier(role) {
75
74
  return WORK_ROLES.has(baseRoleOf(role)) ? 0 : 1;
76
75
  }
77
- /** The roles blocked from starting an authoring run while main's own build/test suite is known
78
- * red with no failing test named (the red-main baseline check; a red that names its failing tests
79
- * blocks nothing, and these roles get a `<main-known-red>` note instead): every role whose diff
80
- * can carry non-exempt (code) changes — on a red main such a diff is rejected deterministically
81
- * by the gate's pre-check, so spending an authoring run on it is pure waste. Exempt: director
82
- * (human prompts outrank autonomous gates, like the budget gate), ci (the designated healer
83
- * — its run executes the suite per "leave the project working" and can fix a red main through the
84
- * existing pre-merge gate; blocking it would leave only the ci role able to unblock the team), and
85
- * plan/readme/steward/qa/selfcheck/feedback (markdown-only charter — their diffs are exempt
86
- * from the build pre-check via review.exemptPaths, so a red main does not block them). */
87
- export const BASELINE_BLOCKED_ROLES = new Set([
88
- "feature",
89
- ...CODE_MAINTENANCE_ROLES,
90
- ]);
91
- /** Is this role id blocked from authoring on a red main? True when its base role is in
92
- * BASELINE_BLOCKED_ROLES, so `feature-2` is blocked exactly as `feature` is. */
93
- export function baselineBlocked(role) {
94
- return BASELINE_BLOCKED_ROLES.has(baseRoleOf(role));
95
- }
96
76
  /** Work ratio 4/4: which commit tier a role's base role merges into, for the work vs maintenance
97
77
  * split the report and the "Merged today" tile show and the maintenance allowance (Work ratio
98
78
  * 1a/4, src/gates/maintenance-quota.ts) counts. The one home of the split. `work` is the
@@ -8,8 +8,8 @@
8
8
  * under .tumwater/state/ and degrades to "no data" on any missing or malformed file: an
9
9
  * observer must never fail a run on bookkeeping (the same policy as qa-coverage.ts). A stale
10
10
  * verdict that stays stale past `CI_PARITY_STALE_MS` is a stall — the full suite has silently
11
- * stopped covering new tips — surfaced as a doctor warn and a standing `ci-stale` alert through
12
- * the one `ciParityStalled` predicate. */
11
+ * stopped covering new tips — surfaced as the doctor `ci parity` warn through the one
12
+ * `ciParityStalled` predicate. */
13
13
  import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
14
14
  import { ciParityPath } from "../paths.js";
15
15
  import { isJsonObject } from "../files/json-object.js";
@@ -20,14 +20,13 @@ import * as fs from "node:fs";
20
20
  import path from "node:path";
21
21
  /** A stale verdict that stays stale past this window is a stall: the `ci` role — now the
22
22
  * full suite's own runner — has silently stopped covering new tips (disabled, paused, wedged,
23
- * or failing before it records). The doctor `ci parity` warn and the standing `ci-stale` alert
24
- * share this one threshold, so the surfaces cannot disagree on when stale is a problem. */
23
+ * or failing before it records). The doctor `ci parity` warn reads this one threshold, so the
24
+ * row and the ledger cannot disagree on when stale is a problem. */
25
25
  export const CI_PARITY_STALE_MS = 3 * DAY_MS;
26
26
  /** True only when a parity record exists, reads `stale` (a `failed` record already warns and
27
27
  * a `current` one is verified for the tip), and has not covered a tip within the stall
28
28
  * window. Parameter is structural — not `CiParityStatus` — so the status-polls → ci-parity
29
- * import edge stays one-way. This is the single stall predicate shared by the doctor row and
30
- * the alert. */
29
+ * import edge stays one-way. This is the single stall predicate the doctor row reads. */
31
30
  export function ciParityStalled(parity, now) {
32
31
  return !!parity && parity.state === "stale" && now - parity.lastRunAt > CI_PARITY_STALE_MS;
33
32
  }
@@ -14,6 +14,13 @@ import { formatAge } from "../text/datetime.js";
14
14
  * caps the block's growth and the prompt's size; older filings are dropped (the charter still
15
15
  * dedupes against the live `gh issue list`). */
16
16
  export const FEEDBACK_FILINGS_MAX = 100;
17
+ /** Collect the ledger into `FilingRow`s newest-first by `filedAt` — a stable sort, so two
18
+ * filings sharing a timestamp keep their ledger (insertion) order. A ledger stored by
19
+ * `recordFiled` is already newest-first, so this reorders nothing on the normal path; it
20
+ * guarantees a hand-edited or torn file that is not already ordered still reads consistently. */
21
+ export function filingRows(filings) {
22
+ return [...filings].sort((a, b) => b.filedAt - a.filedAt);
23
+ }
17
24
  function isFiling(value) {
18
25
  if (!isJsonObject(value))
19
26
  return false;
@@ -46,6 +53,8 @@ export function recordFiled(root, issue, title, now = Date.now()) {
46
53
  export function renderFilingsBlock(filings, now = Date.now()) {
47
54
  if (filings.length === 0)
48
55
  return undefined;
49
- const lines = filings.map((f) => ` #${f.issue} ${f.title} (${formatAge(now - f.filedAt)})`);
56
+ // Render through filingRows, the one ordering the prompt block and the operator read share,
57
+ // so a ledger not already newest-first still reads that way in both places.
58
+ const lines = filingRows(filings).map((f) => ` #${f.issue} ${f.title} (${formatAge(now - f.filedAt)})`);
50
59
  return ["Previously filed from this install:", ...lines].join("\n");
51
60
  }
@@ -26,15 +26,15 @@ export const QA_FLOWS = [
26
26
  /** A coverage ledger whose newest exercise is older than this window is a stall: the `qa`
27
27
  * role — the team's only flow-level observer, the one role that exercises the product's own
28
28
  * flows like a user — has silently stopped recording any exercise (disabled, paused, wedged,
29
- * or failing before it records). The doctor `qa coverage` warn and the standing `qa-stale`
30
- * alert share this one threshold, mirroring `ciParityStalled`'s `CI_PARITY_STALE_MS`. */
29
+ * or failing before it records). The doctor `qa coverage` warn fires through this one
30
+ * threshold, mirroring `ciParityStalled`'s `CI_PARITY_STALE_MS`. */
31
31
  export const QA_COVERAGE_STALE_MS = 3 * DAY_MS;
32
32
  /** True only when the ledger has at least one well-formed entry and every entry's `lastRunAt`
33
33
  * is older than the stall window (`now - max(lastRunAt) > QA_COVERAGE_STALE_MS`). An empty
34
34
  * ledger — a missing file, or a project that never ran qa — is not a stall: "no record yet" is
35
35
  * not "stopped recording", the same rule `ciParityStalled` applies to a missing parity record.
36
- * This is the one stall predicate the doctor row (`checkQaCoverage`) and the alert
37
- * (`qaCoverageAlert`) share, so the surfaces cannot disagree on when a quiet qa is a problem. */
36
+ * This is the one stall predicate the doctor's `qa coverage` row (`checkQaCoverage`) reads, so
37
+ * the row and the ledger cannot disagree on when a quiet qa is a problem. */
38
38
  export function qaCoverageStalled(coverage, now) {
39
39
  let newest = 0;
40
40
  let has = false;
@@ -36,6 +36,16 @@ export function clearSupersededRejection(s) {
36
36
  * quiet team). Small on purpose — at 3 the error ladder has already doubled twice and
37
37
  * the failure is clearly not transient. */
38
38
  export const ERROR_STREAK_WARN = 3;
39
+ /** Whether a role's consecutive-error count is at or past the warn bar (ERROR_STREAK_WARN) —
40
+ * the "failing" predicate the status phase cell (status-model.ts's onErrorStreak), the status
41
+ * poll's slot-demand read (status-roles.ts), and the doctor's error-streaks check
42
+ * (doctor-holds.ts) each test, so the threshold comparison cannot drift between them. The
43
+ * `=== ERROR_STREAK_WARN` crossing edges (run-finalize.ts, merge-slot.ts) are a different
44
+ * comparison (the single run that reached the bar, not the at-or-past state) and stay
45
+ * inline. */
46
+ export function errorStreakAtWarn(s) {
47
+ return (s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN;
48
+ }
39
49
  /** Consecutive failed runs after which the error-streak circuit breaker (src/gates/streak-gate.ts)
40
50
  * auto-pauses the role through the per-role pause marker — the same act-on-it escalation the
41
51
  * budget gate and the pause gates perform, applied to a role failing on its own cause. Ten,
@@ -47,8 +57,10 @@ export const ERROR_STREAK_BREAKER = 10;
47
57
  /** Resumes granted to one context-ceiling cut-off streak before the role stops resuming the
48
58
  * task and falls back to a fresh run: a task that outruns the ceiling on every attempt (even
49
59
  * from a freshly compacted context) is too big to converge, and each cycle costs an hour-plus
50
- * of model time on local hardware. The streak itself keeps counting (RunnerState.cutOffStreak). */
51
- const CUT_OFF_RESUME_LIMIT = 3;
60
+ * of model time on local hardware. The streak itself keeps counting (RunnerState.cutOffStreak)
61
+ * so one warning per episode can be raised (run-finalize.ts) and the resting cell can read
62
+ * ` · cut off ×N` (status-model.ts rolePhase). */
63
+ export const CUT_OFF_RESUME_LIMIT = 3;
52
64
  /** Resumes granted to one quiet-kill streak before the role abandons the starved pi session
53
65
  * and takes a fresh run on the idle ladder. A session the backend defers past the quiet
54
66
  * watchdog every attempt would otherwise re-send the same input immediately and forever,
@@ -14,6 +14,9 @@ import { bucketMergeEvents, resolveQueuedResult } from "../history/history-queue
14
14
  import { eventUsage, readEvents, runSpanMs, runStartMap } from "../events/event-read.js";
15
15
  import { outcomeText, usageText } from "../events/event-format.js";
16
16
  import { baseRoleOf } from "../roles/runner-ids.js";
17
+ import { loadConfigSafe, defaultConfig } from "../config/config.js";
18
+ import { seamContextWindowForRole } from "../pi/pi-models.js";
19
+ import { nonNegativeNumber } from "../files/json-object.js";
17
20
  /** The commit a run pinned: the `commit` of the first `merge_queued`/`merge_done` event in its
18
21
  * block carrying a non-empty one, else null. The one rule both the header's sha and --stat use. */
19
22
  export function runCommitSha(d) {
@@ -58,7 +61,7 @@ export function readRunDetail(root, role, runNumber) {
58
61
  for (let i = events.length - 1; i >= 0; i--) {
59
62
  const e = events[i];
60
63
  if (e.type === "run_end" && e.run === runNumber)
61
- return detailFrom(role, runNumber, all, events, [e], e, null);
64
+ return detailFrom(root, role, runNumber, all, events, [e], e, null);
62
65
  }
63
66
  return null;
64
67
  }
@@ -89,13 +92,19 @@ export function readRunDetail(root, role, runNumber) {
89
92
  break;
90
93
  block.push(e);
91
94
  }
92
- return detailFrom(role, runNumber, all, events, block, end, start);
95
+ return detailFrom(root, role, runNumber, all, events, block, end, start);
93
96
  }
94
97
  /** Fill the payload around a collected block: usage from the run_end via eventUsage/usageText,
95
98
  * duration via runSpanMs against the role's own start map (the shared pairing helper, so this
96
99
  * view and the history rows cannot drift on what a span is), events oldest-first. */
97
- function detailFrom(role, runNumber, all, scoped, block, end, start) {
100
+ function detailFrom(root, role, runNumber, all, scoped, block, end, start) {
98
101
  const usageNums = end === null ? { tokens: 0, costUsd: 0 } : eventUsage(end);
102
+ // The run_end's prompt-token total and its cache-read subset, corrupt-field-guarded reads
103
+ // (nonNegativeNumber, the same guard peakContextTokens applies) with the cache subset
104
+ // clamped to the total so the rendered share never exceeds 100%; 0 on an in-flight run.
105
+ const promptTokens = end === null ? 0 : nonNegativeNumber(end.promptTokens, 0);
106
+ const cacheReadTokens = end === null ? 0 : Math.min(nonNegativeNumber(end.cacheReadTokens, 0), promptTokens);
107
+ const preEditPromptTokens = end === null ? 0 : Math.min(nonNegativeNumber(end.preEditPromptTokens, 0), promptTokens);
99
108
  let result = end === null ? null : outcomeText(end);
100
109
  let merge = null;
101
110
  if (end !== null && result === "queued") {
@@ -108,6 +117,7 @@ function detailFrom(role, runNumber, all, scoped, block, end, start) {
108
117
  merge = resolved.outcome;
109
118
  }
110
119
  }
120
+ const mergedEvent = mergedEventOf(result, merge, scoped, block);
111
121
  const detail = {
112
122
  role,
113
123
  run: runNumber,
@@ -119,9 +129,30 @@ function detailFrom(role, runNumber, all, scoped, block, end, start) {
119
129
  tokens: usageNums.tokens,
120
130
  costUsd: usageNums.costUsd,
121
131
  usage: end === null ? "" : usageText(end),
132
+ // The peak is a corrupt-field-guarded read of the run_end (nonNegativeNumber, the same
133
+ // guard promptStatsFor applies); the window comes from the shared seamContextWindowForRole —
134
+ // same seam the role view and status table print — with loadConfigSafe falling back to
135
+ // defaults, so a broken tumwater.json yields a null window rather than failing this read-only
136
+ // view.
137
+ peakContextTokens: end === null ? 0 : nonNegativeNumber(end.peakContextTokens, 0),
138
+ // The run's prompt-token total and its cache-read subset, spread so a prompt-free or
139
+ // in-flight run keeps today's exact payload (no prompt/cache keys) and a cache subset > 0.
140
+ ...(promptTokens > 0 ? { promptTokens } : {}),
141
+ ...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
142
+ ...(preEditPromptTokens > 0 ? { preEditPromptTokens } : {}),
143
+ // The run_end's ceiling-truncation flag, spread so an in-flight, older, or normal run
144
+ // keeps today's exact payload (no cutOff key).
145
+ ...(end !== null && end.cutOff === true ? { cutOff: true } : {}),
146
+ contextWindow: seamContextWindowForRole(loadConfigSafe(root).config ?? defaultConfig(), role, root),
122
147
  events: block,
123
148
  merge,
124
149
  mergedSha: mergedShaOf(result, merge, scoped, block),
150
+ // The end-of-run → on-main landing latency (the landing `merged` event's ts minus the run
151
+ // end's), spread so a run without both an end and a landing event — or a landing before its
152
+ // end, or a negative difference — keeps today's exact payload (no key).
153
+ ...(end !== null && mergedEvent !== null && mergedEvent.ts - end.ts >= 0
154
+ ? { mergeAfterRunMs: mergedEvent.ts - end.ts }
155
+ : {}),
125
156
  reverted: null,
126
157
  };
127
158
  detail.reverted = revertedOf(detail, all);
@@ -153,23 +184,32 @@ function revertedOf(d, all) {
153
184
  }
154
185
  return null;
155
186
  }
156
- /** The commit of the newest `merged` event in `pool` (oldest-first) inside [from, to]. */
157
- function newestMergedCommit(pool, from, to) {
187
+ /** The newest `merged` event in `pool` (oldest-first) inside [from, to], or null when none. */
188
+ function newestMergedEvent(pool, from, to) {
158
189
  for (let i = pool.length - 1; i >= 0; i--) {
159
190
  const e = pool[i];
160
191
  if (e.type === "merged" && e.ts >= from && e.ts <= to && typeof e.commit === "string" && e.commit !== "") {
161
- return e.commit;
192
+ return e;
162
193
  }
163
194
  }
164
195
  return null;
165
196
  }
166
- /** The landed sha for a detail: see RunDetail.mergedSha. */
167
- function mergedShaOf(result, merge, scoped, block) {
197
+ /** The landing `merged` event for a detail, the one behind its `mergedSha` and its
198
+ * `mergeAfterRunMs` span: the newest in the block for a run that merged directly (result
199
+ * `changed`, no queued resolve) or the newest inside the merge's own window (`merge_done` ts
200
+ * minus its `durationMs`, up to its ts) for a queued run; null for every other run and when
201
+ * the event rotated out or the merge was a no-op fast-forward (which logs none). */
202
+ function mergedEventOf(result, merge, scoped, block) {
168
203
  if (merge === null) {
169
- return result === "changed" ? newestMergedCommit(block, -Infinity, Infinity) : null;
204
+ return result === "changed" ? newestMergedEvent(block, -Infinity, Infinity) : null;
170
205
  }
171
206
  if (merge.type !== "merge_done" || typeof merge.durationMs !== "number" || !Number.isFinite(merge.durationMs)) {
172
207
  return null;
173
208
  }
174
- return newestMergedCommit(scoped, merge.ts - merge.durationMs, merge.ts);
209
+ return newestMergedEvent(scoped, merge.ts - merge.durationMs, merge.ts);
210
+ }
211
+ /** The landed sha for a detail: see RunDetail.mergedSha. */
212
+ function mergedShaOf(result, merge, scoped, block) {
213
+ const e = mergedEventOf(result, merge, scoped, block);
214
+ return e === null ? null : e.commit;
175
215
  }
@@ -3,7 +3,7 @@ import { formatEvent } from "../events/event-format.js";
3
3
  import { eventTypeLabel } from "../events/event-labels.js";
4
4
  import { roleLabel } from "../roles/role-labels.js";
5
5
  import { shortSpanPhrase } from "../text/phrases.js";
6
- import { shortSha } from "../text/format.js";
6
+ import { shortSha, contextShareLabel, compactTokens } from "../text/format.js";
7
7
  import { piLogPath } from "../paths.js";
8
8
  import { readRunTranscript } from "../ui/transcript-run.js";
9
9
  import { gitTry } from "../git/git-run.js";
@@ -47,10 +47,35 @@ export function renderRunDetail(d) {
47
47
  summary.push(d.startTs === null ? "unpaired" : "in flight");
48
48
  else
49
49
  summary.push(d.result ? eventTypeLabel(d.result) : "?");
50
+ // The ceiling-truncation marker, straight after the result cell. It only rides a
51
+ // completed end (cutOff is set from the run_end), so it can never follow in flight/unpaired.
52
+ if (d.cutOff === true)
53
+ summary.push("cut off at the context ceiling");
50
54
  if (d.durationMs !== null)
51
55
  summary.push(shortSpanPhrase(d.durationMs));
52
56
  if (d.usage !== "")
53
57
  summary.push(d.usage);
58
+ // The run's prompt-token total and the share the backend served from cache, the input
59
+ // side a cost-only header hides. The `${n}% cached` wording and rounding are the usage
60
+ // report's `Prompt tokens by role` share; a run with prompt tokens but no cache reads
61
+ // shows no parenthetical, and a run that reported neither keeps today's header
62
+ // byte-identical (the omit-when-zero convention).
63
+ const prompt = d.promptTokens ?? 0;
64
+ const cached = Math.min(d.cacheReadTokens ?? 0, prompt);
65
+ if (prompt > 0)
66
+ summary.push(cached > 0 ? `prompt ${compactTokens(prompt)} tok (${Math.round((100 * cached) / prompt)}% cached)` : `prompt ${compactTokens(prompt)} tok`);
67
+ // The run's pre-edit prompt-token share, the `… before first edit` signal a cost-only
68
+ // header hides: how much of the prompt budget went out before the run made its first
69
+ // edit. Clamped to the total (run-detail-data.ts), written after the prompt cell and
70
+ // before the peak-context cell; a run that reported no pre-edit reading — and an
71
+ // in-flight run — keeps today's header byte-identical.
72
+ const preEdit = Math.min(d.preEditPromptTokens ?? 0, prompt);
73
+ if (preEdit > 0)
74
+ summary.push(`pre-edit ${compactTokens(preEdit)} tok`);
75
+ // The run's peak context against the seam's declared window, the one header every surface
76
+ // renders. Omitted when the run reported no peak (byte-identical to today's header).
77
+ if ((d.peakContextTokens ?? 0) > 0)
78
+ summary.push(`peak ctx ${contextShareLabel(d.peakContextTokens ?? 0, d.contextWindow)}`);
54
79
  if (d.model !== null)
55
80
  summary.push(d.model);
56
81
  const sha = runCommitSha(d);
@@ -59,6 +84,12 @@ export function renderRunDetail(d) {
59
84
  header += ` · commit ${shortSha(sha)}`;
60
85
  if (d.mergedSha !== null && d.mergedSha !== sha)
61
86
  header += ` · merged as ${shortSha(d.mergedSha)}`;
87
+ // The end-of-run → on-main landing latency, the per-change twin of the day report's `Merge
88
+ // wait median`. shortSpanPhrase is the duration cell's renderer, so both spans read alike; a
89
+ // run without the field (rejected/failed merge, in flight, landing rotated out, no-op
90
+ // fast-forward) keeps today's header byte-identical.
91
+ if (d.mergeAfterRunMs !== undefined)
92
+ header += ` · merged ${shortSpanPhrase(d.mergeAfterRunMs)} after the run`;
62
93
  if (d.reverted !== null) {
63
94
  header += ` · reverted ${shortSha(d.reverted.sha)} (by operator)`; // vocabulary-ok: the deliberate-revert writer this header names
64
95
  }