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
@@ -57,24 +57,15 @@ export function newestRunRow(root, role, classes, models) {
57
57
  function newestRunNumber(root, role, classes, models) {
58
58
  return newestRunRow(root, role, classes, models)?.run ?? null;
59
59
  }
60
- /** `tumwater history <role> [<n>] [--last [--result <class>[,...]]] [--stat] [--patch]
61
- * [--transcript [--prompt] [--grep <text>]] [--json]`: print one completed
62
- * run's full event trail. Read-only: stdout only, no state file created — a run the
63
- * scan cannot find prints a not-found line and exits 0 (history's empty-output convention:
64
- * an absent record is an answer, not a failure), and under --json it prints the JSON document
65
- * `null` instead — the report --json precedent
66
- * that every exit-0 output is parseable, never prose. `tumwater history <role> --last` resolves
67
- * the role's newest completed run in the scanned window and prints its trail exactly as the
68
- * numbered form would — the summary header and event rendering are shared, only the number's
69
- * provenance differs (readRunRows' newest-first scan, not the user's argument). --last and
70
- * a numeric <n> are rivals: exactly one positional under --last, exactly two without. With
71
- * --last, `--result <class>[,...]` picks the newest run in the listed outcome classes instead
72
- * (the listing's classes, scanning up to HISTORY_MAX_RUNS runs); it needs --last. `--model
73
- * <selector>[,...]` with --last keeps only runs that started on a listed model (the listing's
74
- * exact match) and composes with --result; it needs --last too. With --last and no role, the
75
- * newest run of any role is opened (newestRunRow with a null role) and renders as its
76
- * role's form. */
77
- export async function cmdRunDetail(root, positionals, json, { last = false, stat = false, patch = false, transcript = false, prompt = false, grep = null, result = null, model = null } = {}) {
60
+ /** The invocation half of cmdRunDetail: rule out the arity and flag mistakes before any run is
61
+ * resolved, so the shared unknown-role wording and the usage string are the whole validation
62
+ * surface, found by this one name. Succeeds only for a well-formed call; returns the parsed
63
+ * bare/role/classes/models for the resolver. The role is checked first, so a typo is named
64
+ * whatever the count (knownRoleIdsCached: the built-ins plus user-defined roles, read through
65
+ * the never-throwing cached loader so a transiently broken tumwater.json cannot take a
66
+ * read-only view down). The arity, unknown-role and flag-dependency failures all `fail` with
67
+ * the usage. */
68
+ function parseRunDetailInvocation(root, positionals, { last, transcript, prompt, grep, result, model }) {
78
69
  // Positional arity by form: cli.ts has already peeled the flags off and gated them (only
79
70
  // --json and --last are admitted), so anything left over that does not fit the chosen form
80
71
  // is a mistake. The role is checked first, so a typo is named whatever the count; then
@@ -110,29 +101,60 @@ export async function cmdRunDetail(root, positionals, json, { last = false, stat
110
101
  fail(`history --model needs --last on a one-run trail (usage: ${RUN_DETAIL_USAGE})`);
111
102
  const classes = result !== null ? parseHistoryResultList(result) : null;
112
103
  const models = model !== null ? parseHistoryModelList(model) : null;
104
+ return { bare, role, classes, models };
105
+ }
106
+ /** The resolution half of cmdRunDetail: pick the run to open and read its detail. Bare `--last`
107
+ * takes the newest run of any role (its role from the row); `--last` with a role the newest run
108
+ * by number; the numbered path the user's positive integer. An empty or unreadable scan is null
109
+ * (the detail stays null, and the not-found branch of cmdRunDetail words it), so the resolver
110
+ * never reports a miss itself. */
111
+ function resolveRunDetailTarget(root, parsed, positionals, last) {
112
+ let role = parsed.role;
113
113
  // The run number: either the user's positive integer (the numbered path, through the
114
114
  // shared count parser — a non-positive or non-numeric n fails naming the shape; there is
115
115
  // no cap — the number selects, it does not size the scan) or the newest completed run's
116
116
  // number, read from readRunRows' newest-first scan for the role. An empty scan is not
117
117
  // found by construction, so the not-found path below takes it with the last-form wording.
118
118
  let runNumber;
119
- if (bare) {
119
+ if (parsed.bare) {
120
120
  // `history --last` with no role: the newest completed run of any role; its role is the row's.
121
- const row = newestRunRow(root, null, classes, models);
121
+ const row = newestRunRow(root, null, parsed.classes, parsed.models);
122
122
  if (row !== null)
123
123
  role = row.role;
124
124
  runNumber = row?.run ?? null;
125
125
  }
126
126
  else {
127
- runNumber = last ? newestRunNumber(root, role, classes, models) : parseCountFlag("history <n>", positionals[1]);
127
+ runNumber = last ? newestRunNumber(root, role, parsed.classes, parsed.models) : parseCountFlag("history <n>", positionals[1]);
128
128
  }
129
129
  const detail = runNumber === null ? null : readRunDetail(root, role, runNumber);
130
+ return { role, runNumber, detail };
131
+ }
132
+ /** `tumwater history <role> [<n>] [--last [--result <class>[,...]]] [--stat] [--patch]
133
+ * [--transcript [--prompt] [--grep <text>]] [--json]`: print one completed
134
+ * run's full event trail. Read-only: stdout only, no state file created — a run the
135
+ * scan cannot find prints a not-found line and exits 0 (history's empty-output convention:
136
+ * an absent record is an answer, not a failure), and under --json it prints the JSON document
137
+ * `null` instead — the report --json precedent
138
+ * that every exit-0 output is parseable, never prose. `tumwater history <role> --last` resolves
139
+ * the role's newest completed run in the scanned window and prints its trail exactly as the
140
+ * numbered form would — the summary header and event rendering are shared, only the number's
141
+ * provenance differs (readRunRows' newest-first scan, not the user's argument). --last and
142
+ * a numeric <n> are rivals: exactly one positional under --last, exactly two without. With
143
+ * --last, `--result <class>[,...]` picks the newest run in the listed outcome classes instead
144
+ * (the listing's classes, scanning up to HISTORY_MAX_RUNS runs); it needs --last. `--model
145
+ * <selector>[,...]` with --last keeps only runs that started on a listed model (the listing's
146
+ * exact match) and composes with --result; it needs --last too. With --last and no role, the
147
+ * newest run of any role is opened (newestRunRow with a null role) and renders as its
148
+ * role's form. */
149
+ export async function cmdRunDetail(root, positionals, json, { last = false, stat = false, patch = false, transcript = false, prompt = false, grep = null, result = null, model = null } = {}) {
150
+ const parsed = parseRunDetailInvocation(root, positionals, { last, transcript, prompt, grep, result, model });
151
+ const { role, runNumber, detail } = resolveRunDetailTarget(root, parsed, positionals, last);
130
152
  if (runNumber === null) {
131
153
  // The --last scan came up empty: not found by construction, the last-form wording.
132
154
  if (json)
133
155
  sayJson(null);
134
156
  else
135
- say(runNotFoundLastMessage(bare ? null : role, classes, models));
157
+ say(runNotFoundLastMessage(parsed.bare ? null : role, parsed.classes, parsed.models));
136
158
  return;
137
159
  }
138
160
  if (detail === null) {
@@ -0,0 +1,43 @@
1
+ /** The outcome-recording a run writes at its dismissal — the qa-flow ledger entry and the
2
+ * feedback filing — shared by the two dismissal paths so the run that changed the tree
3
+ * (run-stage.ts) and the no-change run (run-verdict.ts) file their flow and filing
4
+ * identically. Before the split each path carried its own copy: the same try/catch, the same
5
+ * `not recorded` warn wording, and the same issue_filed event, so a wording or guard drift
6
+ * in one path could not show up in the other. Each recorder is bookkeeping that never fails
7
+ * the run — a failed write warns and the run's outcome stands — and the caller keeps its own
8
+ * guard (the cut-off runs record nothing, the changed path records even when the merge is
9
+ * queued). The ci-parity recorder is deliberately NOT shared: only the no-change path writes
10
+ * it, because a run that changed the tree has not verified main (run-stage.ts's comment). */
11
+ import { logEventBestEffort } from "../events/events.js";
12
+ import { errorMessage } from "../text/text.js";
13
+ import { recordFiled } from "./feedback-filings.js";
14
+ import { recordFlow } from "./qa-coverage.js";
15
+ /** Record a dismissed run's qa flow (qa-coverage.ts's recordFlow) — a failed write warns
16
+ * with the same wording on both dismissal paths. `summary` is the run's summary only when
17
+ * the flow result is a bug (the recordFlow shape carries one just for the bug case, as each
18
+ * path's caller spelled the ternary inline); a passed flow records none. */
19
+ export function recordDismissedFlow(ctx, root, flow, result, summary) {
20
+ try {
21
+ recordFlow(root, flow, result, result === "bug" ? summary : undefined);
22
+ }
23
+ catch (err) {
24
+ ctx.warn(`qa flow "${flow}" not recorded: ${errorMessage(err)}`);
25
+ }
26
+ }
27
+ /** Record a dismissed run's feedback filing (feedback-filings.ts's recordFiled) and log the
28
+ * issue_filed event so the filing reaches the operator's activity feed — a failed ledger
29
+ * write warns with the same wording on both dismissal paths. */
30
+ export function recordDismissedFiling(ctx, root, filed) {
31
+ logEventBestEffort(root, {
32
+ source: ctx.role,
33
+ type: "issue_filed",
34
+ issue: filed.issue,
35
+ title: filed.title,
36
+ });
37
+ try {
38
+ recordFiled(root, filed.issue, filed.title);
39
+ }
40
+ catch (err) {
41
+ ctx.warn(`feedback filing #${filed.issue} not recorded: ${errorMessage(err)}`);
42
+ }
43
+ }
@@ -2,7 +2,7 @@ import { hasOutstandingWork, saveRunnerStateBestEffort } from "../runner/runner-
2
2
  import { branchHead } from "../git/git.js";
3
3
  import { logEventBestEffort, usageFragment, warnEventBestEffort } from "../events/events.js";
4
4
  import { observeAccount } from "../agent/subscription-account.js";
5
- import { ERROR_STREAK_WARN, QUIET_KILL_RESUME_LIMIT, applyRunOutcome } from "./run-apply.js";
5
+ import { CUT_OFF_RESUME_LIMIT, ERROR_STREAK_WARN, QUIET_KILL_RESUME_LIMIT, applyRunOutcome } from "./run-apply.js";
6
6
  import { restoreMidRunWake } from "../scheduling/backoff.js";
7
7
  import { consecutiveFailuresWarning } from "../text/phrases.js";
8
8
  /** The run results that end an instance's claim: the run found nothing to do, refused the
@@ -48,28 +48,46 @@ export async function finalizeRun(deps) {
48
48
  // Record the outcome on state and schedule the next run (see src/run/run-apply.ts for the
49
49
  // per-result policy: prompt retry, backoff, bounded cut-off resumes).
50
50
  applyRunOutcome(s, config, role, outcome);
51
- // A run that ends without producing or keeping work releases this instance's backlog claim
52
- // (plans/parallel-work-instances.md "Claims", part 4/7): the next run may take a different
53
- // entry. Every other result — queued, changed, rejected with a revision, an error, an abort
54
- // that resumes — keeps the claim so the same instance retries it; an outstanding revision or
55
- // a pending resume also holds it, because the entry is still this instance's task.
56
- if (s.claim &&
57
- CLAIM_RELEASING.has(outcome.result) &&
58
- !hasOutstandingWork(s)) {
59
- const title = s.claim.title;
60
- s.claim = undefined;
61
- logEventBestEffort(root, { source: role, type: "claim", action: "released", title, reason: outcome.result });
62
- }
63
- // A wake consumed while this run ran stamped the shared state in place, but the outcome
64
- // schedule above overwrites it (lastRunEndedAt past wokenAt, nextRunAt a fresh gap or
65
- // backoff out), so a plain `tumwater wake --role` with an empty queue would silently wait
66
- // out the whole interval (BUGS.md 2026-09-25). Re-apply it here — exactly like a wake
67
- // arriving one poll after the run ended.
51
+ releaseFinishedClaim(root, role, s, outcome);
68
52
  restoreMidRunWake(s);
69
53
  // A failed save warns and the run still ends: the in-memory state already holds the fold and
70
54
  // the runner's next save writes it, while a throw here would skip run_end and the launch
71
55
  // task's breaker fold.
72
56
  saveRunnerStateBestEffort(root, s);
57
+ warnOnStreakCrossings(root, role, s, outcome);
58
+ // The run's wall-clock span rides on run_end too: the
59
+ // failure digest prices a run by its own durationMs first (src/failure/failure-data.ts
60
+ // runDurationMs), so the field is ALWAYS present — a run that ends within the same
61
+ // Date.now() millisecond it started (an instant early-abort error run) or crosses a
62
+ // backward clock step still attests its 0 ms, where the old omit-when-zero convention
63
+ // dropped the field and broke the "every run_end carries a span" contract (BUGS.md
64
+ // 2026-09-29). Unlike the usage fields, run_end's renderer never prints the span, so an
65
+ // always-present 0 changes no rendered line.
66
+ const runDurationMs = Math.max(0, Date.now() - deps.runStartedAt);
67
+ observeRunAccount(root, usage);
68
+ logRunEnd(root, role, runNumber, s, outcome, usage, runDurationMs);
69
+ return outcome;
70
+ }
71
+ /** A run that ends without producing or keeping work releases this instance's backlog claim
72
+ * (plans/parallel-work-instances.md "Claims", part 4/7): the next run may take a different
73
+ * entry. Every other result — queued, changed, rejected with a revision, an error, an abort
74
+ * that resumes — keeps the claim so the same instance retries it; an outstanding revision or
75
+ * a pending resume also holds it, because the entry is still this instance's task. */
76
+ function releaseFinishedClaim(root, role, state, outcome) {
77
+ if (state.claim &&
78
+ CLAIM_RELEASING.has(outcome.result) &&
79
+ !hasOutstandingWork(state)) {
80
+ const title = state.claim.title;
81
+ state.claim = undefined;
82
+ logEventBestEffort(root, { source: role, type: "claim", action: "released", title, reason: outcome.result });
83
+ }
84
+ }
85
+ /** One warning per error episode (BUGS.md 2026-09-15: every role failing identically
86
+ * looked like a quiet team), quiet-kill episode (BUGS.md 2026-09-18: a role burning an hour
87
+ * per run with no output must not look like a sleeping role) and cut-off episode (mirroring
88
+ * quiet-kill: a role cycling cut-off → resume burns an hour-plus per cycle with no other
89
+ * operator signal) — each fired exactly once at the streak's crossing, after the schedule. */
90
+ function warnOnStreakCrossings(root, role, state, outcome) {
73
91
  // One warning per error episode (BUGS.md 2026-09-15: every role failing identically
74
92
  // looked like a quiet team). applyRunOutcome increments the streak on error-class
75
93
  // outcomes, preserves it across a plain `queued` run (the run's merge resolves it),
@@ -78,37 +96,34 @@ export async function finalizeRun(deps) {
78
96
  // re-warn on the queued run's end. A rejection's crossing warns from the merge side
79
97
  // instead (writeMergeOutcome, src/merge/merge-slot.ts), which is where rejections resolve.
80
98
  if ((outcome.result === "error" || outcome.recoveryFailure !== undefined) &&
81
- (s.consecutiveErrors ?? 0) === ERROR_STREAK_WARN) {
82
- warnEventBestEffort(root, role, consecutiveFailuresWarning(s.consecutiveErrors ?? 0, s.lastError ?? outcome.recoveryFailure ?? "unknown error"));
99
+ (state.consecutiveErrors ?? 0) === ERROR_STREAK_WARN) {
100
+ warnEventBestEffort(root, role, consecutiveFailuresWarning(state.consecutiveErrors ?? 0, state.lastError ?? outcome.recoveryFailure ?? "unknown error"));
83
101
  }
84
102
  // One warning per quiet-kill episode (BUGS.md 2026-09-18): a role burning an hour per
85
103
  // run with no output must not look like a sleeping role. applyRunOutcome grows the
86
104
  // streak on each kill and resets it on any other result, so the crossing fires once;
87
105
  // the give-up (fresh session + backoff) follows on the next kill.
88
- if ((s.quietKillStreak ?? 0) === QUIET_KILL_RESUME_LIMIT) {
89
- warnEventBestEffort(root, role, `${s.quietKillStreak} consecutive stalls (no progress): ${s.lastError ?? "unknown hang"}`);
106
+ if ((state.quietKillStreak ?? 0) === QUIET_KILL_RESUME_LIMIT) {
107
+ warnEventBestEffort(root, role, `${state.quietKillStreak} consecutive stalls (no progress): ${state.lastError ?? "unknown hang"}`);
90
108
  }
91
- // Per-run usage: the event feed is where operators see
92
- // spend — this run's tokens and cost ride on run_end so a budget pause or a money-burning
93
- // no_change/error/rejected/refused run shows what it spent without diffing status-table
94
- // snapshots. Both fields are per-run windows (reset at run start, folded in over every
95
- // pi run of the run); they ride on HarnessEvent's index signature like other payloads and
96
- // are omitted when zero so skipped runs render byte-identical to a pre-feature line.
97
- // The run's wall-clock span rides on run_end too: the
98
- // failure digest prices a run by its own durationMs first (src/failure/failure-data.ts
99
- // runDurationMs), so the field is ALWAYS present — a run that ends within the same
100
- // Date.now() millisecond it started (an instant early-abort error run) or crosses a
101
- // backward clock step still attests its 0 ms, where the old omit-when-zero convention
102
- // dropped the field and broke the "every run_end carries a span" contract (BUGS.md
103
- // 2026-09-29). Unlike the usage fields, run_end's renderer never prints the span, so an
104
- // always-present 0 changes no rendered line.
105
- const runDurationMs = Math.max(0, Date.now() - deps.runStartedAt);
106
- // The account state (plans/agent-backends.md part 6a): fold this run's billing answer and
107
- // rate-window readings into the reporting backend's .tumwater/state/<backend>-account.json
108
- // for the sibling parts that throttle or display the subscription window. Best-effort — the
109
- // file is a cache, and an unwritable state dir must not fail a run whose real work is
110
- // already done. A pi run never reports either, so in practice claude owns the file until a
111
- // second subscription backend lands (plans/codex-backend.md).
109
+ // One warning per cut-off episode, mirroring quiet-kill above: a role cycling cut-off →
110
+ // resume burns an hour-plus per cycle (run-apply.ts CUT_OFF_RESUME_LIMIT) with no other
111
+ // operator signal, so the crossing must not look like a benign idle role. The `outcome.cutOff`
112
+ // guard makes it fire only on the run that increments the streak to the limit — every
113
+ // non-cut-off run resets the streak to 0 and a later cut-off sits above the limit — so
114
+ // exactly one warning per episode. No role guard, matching quiet-kill (a cut-off director
115
+ // prompt is re-queued but its streak still counts).
116
+ if (outcome.cutOff === true && (state.cutOffStreak ?? 0) === CUT_OFF_RESUME_LIMIT) {
117
+ warnEventBestEffort(root, role, `${state.cutOffStreak} consecutive runs cut off at the context ceiling; give the role a smaller task or a longer context window`);
118
+ }
119
+ }
120
+ /** The account state (plans/agent-backends.md part 6a): fold this run's billing answer and
121
+ * rate-window readings into the reporting backend's .tumwater/state/<backend>-account.json
122
+ * for the sibling parts that throttle or display the subscription window. Best-effort — the
123
+ * file is a cache, and an unwritable state dir must not fail a run whose real work is
124
+ * already done. A pi run never reports either, so in practice claude owns the file until a
125
+ * second subscription backend lands (plans/codex-backend.md). */
126
+ function observeRunAccount(root, usage) {
112
127
  if (usage.billing !== undefined || usage.rateWindows !== undefined) {
113
128
  try {
114
129
  observeAccount(root, usage.agent ?? "pi", {
@@ -120,6 +135,12 @@ export async function finalizeRun(deps) {
120
135
  // The next reporting run rewrites it; nothing this run depends on is lost.
121
136
  }
122
137
  }
138
+ }
139
+ /** Assemble and log the run_end event: the per-run usage fields (tokens and cost), the run's
140
+ * wall-clock span, and every conditional payload spread. Omitted-when-zero fields ride the
141
+ * event feed only when nonzero so skipped runs render byte-identically to a pre-feature line;
142
+ * `durationMs` is ALWAYS present to keep the "every run_end carries a span" contract. */
143
+ function logRunEnd(root, role, runNumber, state, outcome, usage, runDurationMs) {
123
144
  // The per-provider split of this run's usage (plans: per-provider spend in the day
124
145
  // report): a Record<provider, {costUsd, notionalCostUsd}> for the report's `Cost by
125
146
  // provider` line. Omitted when empty like the prompt-token fields, so a run whose runs
@@ -131,10 +152,14 @@ export async function finalizeRun(deps) {
131
152
  run: runNumber,
132
153
  result: outcome.result,
133
154
  summary: outcome.summary,
134
- error: s.lastError,
155
+ error: state.lastError,
135
156
  durationMs: runDurationMs,
136
157
  ...(outcome.heldOnBranch ? { heldOnBranch: true } : {}),
137
- ...usageFragment(s.generatedTokens, usage.costUsd, usage.notionalCostUsd),
158
+ // The context-ceiling truncation flag: set when the run burned its whole window and
159
+ // the harness resumes its pinned session (slotPinnedForResume). Omitted when false, so
160
+ // a normal run — and every run_end written by an older build — renders byte-identically.
161
+ ...(outcome.cutOff === true ? { cutOff: true } : {}),
162
+ ...usageFragment(state.generatedTokens, usage.costUsd, usage.notionalCostUsd),
138
163
  ...(Object.keys(providerCost).length > 0 ? { providerCost } : {}),
139
164
  // The billing answer the run's runs reported, so the report can tell notional spend from
140
165
  // real spend without re-deriving it from the file (plans/agent-backends.md part 6a).
@@ -150,6 +175,10 @@ export async function finalizeRun(deps) {
150
175
  ...(usage.promptTokens > 0 ? { promptTokens: usage.promptTokens } : {}),
151
176
  ...(usage.cacheReadTokens > 0 ? { cacheReadTokens: usage.cacheReadTokens } : {}),
152
177
  ...(usage.preEditPromptTokens > 0 ? { preEditPromptTokens: usage.preEditPromptTokens } : {}),
178
+ // The run's peak single-request context: how hot the run's largest model call ran
179
+ // against the model's declared window (runner-state.ts's peakContextTokens, reset at
180
+ // run start). Omitted when zero like the prompt-token fields, so a run that reported
181
+ // no context — and every run_end written by an older build — renders byte-identically.
182
+ ...(state.peakContextTokens > 0 ? { peakContextTokens: state.peakContextTokens } : {}),
153
183
  });
154
- return outcome;
155
184
  }