tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -11,7 +11,7 @@
11
11
  * Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
12
12
  * state (the pending split-block assistant message, the unmapped native types seen, whether a
13
13
  * `run_cost` arrived), so call it once per run. Unmapped native types are recorded once each
14
- * on the seam's optional `warnings()` — src/agent/run-stream.ts forwards each as an
14
+ * on the seam's optional `warnings()` — src/agent/agent-stream.ts forwards each as an
15
15
  * `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
16
16
  * run produced no authoritative `run_cost`. */
17
17
  import { joinParts } from "../../text/text.js";
@@ -71,47 +71,80 @@ function canonicalBlock(block) {
71
71
  return undefined;
72
72
  }
73
73
  }
74
- /** How a `result` event's error signals classify into the canonical `tumwaterKind` (and, for
75
- * a transient backend, its kind), trusted by `PiStreamParser` before its text regexes. Empty
76
- * when the result is not an error or carries no signal the vocabulary names — a SIGTERM
77
- * (`terminal_reason: "aborted_tools"`) is deliberate: the harness killed the run, so no
78
- * provider classification applies. `status` is `api_error_status`, which is null on the
79
- * not-logged-in result, so the text checks carry that case. */
80
- function classifyResult(result) {
81
- const terminal = optionalString(result.terminal_reason);
74
+ /** `terminal === "aborted_tools"` is deliberate: the harness killed the run, so no provider
75
+ * classification applies — the chain stops there. `error_max_budget_usd`, as a terminal
76
+ * reason or in the text, caps the budget. */
77
+ function terminalAndBudgetClassification(terminal, text) {
82
78
  if (terminal === "aborted_tools")
83
79
  return {};
84
80
  if (terminal === "error_max_budget_usd")
85
81
  return { kind: "budgetCapped" };
86
- const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
87
82
  if (/error_max_budget_usd/i.test(text))
88
83
  return { kind: "budgetCapped" };
89
- const status = nonNegativeNumber(result.api_error_status, undefined);
84
+ return undefined;
85
+ }
86
+ function statusClassification(status) {
90
87
  if (status === 429)
91
88
  return { kind: "transientRateLimit" };
92
89
  if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status))
93
90
  return { kind: "transientBackend", backendKind: "server" };
94
- // The context-overflow text wins over a configuration status: claude reports an oversized
95
- // prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError.
91
+ return undefined;
92
+ }
93
+ /** The context-overflow text wins over a configuration status: claude reports an oversized
94
+ * prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError. */
95
+ function contextOverflowClassification(text) {
96
96
  if (/prompt is too long/i.test(text))
97
97
  return { kind: "contextExceeded" };
98
+ return undefined;
99
+ }
100
+ function configStatusClassification(status) {
98
101
  if (status !== undefined && CONFIG_STATUS.has(status))
99
102
  return { kind: "configError" };
100
- // Text fallbacks, for the statuses claude leaves null or omits.
103
+ return undefined;
104
+ }
105
+ function resumeMissingClassification(text) {
101
106
  if (/No conversation found with session ID/i.test(text))
102
107
  return { kind: "resumeMissing" };
108
+ return undefined;
109
+ }
110
+ function configTextClassification(text) {
103
111
  if (/Not logged in|issue with the selected model|does not support this model/i.test(text))
104
112
  return { kind: "configError" };
113
+ return undefined;
114
+ }
115
+ function transientTextClassification(text) {
105
116
  if (/\b429\b|rate limit/i.test(text))
106
117
  return { kind: "transientRateLimit" };
107
118
  if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
108
119
  return { kind: "transientBackend", backendKind: "server" };
109
120
  const prose = transientProseKind(text);
110
- if (prose)
111
- return prose;
121
+ return prose;
122
+ }
123
+ /** Text fallbacks, for the statuses claude leaves null or omits. */
124
+ function httpStatusTextClassification(text) {
112
125
  if (/\b(400|401|403|404)\b/i.test(text))
113
126
  return { kind: "configError" };
114
- return {};
127
+ return undefined;
128
+ }
129
+ /** How a `result` event's error signals classify into the canonical `tumwaterKind` (and, for
130
+ * a transient backend, its kind), trusted by `PiStreamParser` before its text regexes. Empty
131
+ * when the result is not an error or carries no signal the vocabulary names — a SIGTERM
132
+ * (`terminal_reason: "aborted_tools"`) is deliberate: the harness killed the run, so no
133
+ * provider classification applies. `status` is `api_error_status`, which is null on the
134
+ * not-logged-in result, so the text checks carry that case. */
135
+ function classifyResult(result) {
136
+ const terminal = optionalString(result.terminal_reason);
137
+ const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
138
+ const status = nonNegativeNumber(result.api_error_status, undefined);
139
+ return (terminalAndBudgetClassification(terminal, text) ??
140
+ statusClassification(status) ??
141
+ contextOverflowClassification(text) ??
142
+ configStatusClassification(status) ??
143
+ resumeMissingClassification(text) ??
144
+ configTextClassification(text) ??
145
+ transientTextClassification(text) ??
146
+ httpStatusTextClassification(text) ??
147
+ {});
115
148
  }
116
149
  /** Build a fresh per-run claude translator. */
117
150
  export function claudeTranslator(opts = {}) {
@@ -250,43 +283,10 @@ export function claudeTranslator(opts = {}) {
250
283
  const info = objectOrEmpty(event.rate_limit_info);
251
284
  const rateLimitType = optionalString(info.rateLimitType);
252
285
  const status = optionalString(info.status);
253
- // claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
254
- // keyed by window type, and only the binding window's status at the top level.
255
- const windows = [];
256
- for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
257
- if (type === rateLimitType || !isJsonObject(raw))
258
- continue;
259
- const reading = { type: "rate_limit", rateLimitType: type };
260
- const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
261
- if (windowResets !== undefined)
262
- reading.resetsAt = windowResets;
263
- const windowUtil = nonNegativeNumber(raw.utilization, undefined);
264
- if (windowUtil !== undefined)
265
- reading.utilization = windowUtil;
266
- windows.push(JSON.stringify(reading));
267
- }
268
- const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
269
- const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
270
- const out = { type: "rate_limit" };
271
- if (rateLimitType !== undefined)
272
- out.rateLimitType = rateLimitType;
273
- if (status !== undefined)
274
- out.status = status;
275
- if (resetsAt !== undefined)
276
- out.resetsAt = resetsAt;
277
- const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
278
- if (utilization !== undefined)
279
- out.utilization = utilization;
280
- const overageStatus = optionalString(info.overageStatus);
281
- if (overageStatus !== undefined)
282
- out.overageStatus = overageStatus;
283
- if (info.isUsingOverage === true)
284
- out.isUsingOverage = true;
285
- if (status === "rejected" && resetsAt !== undefined) {
286
- // resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
287
- out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
288
- }
289
- return [JSON.stringify(out), ...windows];
286
+ return [
287
+ JSON.stringify(claudeRateLimitReading(info, rateLimitType, status)),
288
+ ...claudeRateLimitWindowReadings(info, rateLimitType),
289
+ ];
290
290
  };
291
291
  const handleResult = (event) => {
292
292
  sawActivity = true;
@@ -363,6 +363,63 @@ export function claudeTranslator(opts = {}) {
363
363
  },
364
364
  };
365
365
  }
366
+ /** One reading per sibling `unifiedWindows` window other than the binding window. */
367
+ function claudeRateLimitWindowReadings(info, rateLimitType) {
368
+ // claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
369
+ // keyed by window type, and only the binding window's status at the top level.
370
+ const windows = [];
371
+ for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
372
+ if (type === rateLimitType || !isJsonObject(raw))
373
+ continue;
374
+ const reading = { type: "rate_limit", rateLimitType: type };
375
+ const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
376
+ if (windowResets !== undefined)
377
+ reading.resetsAt = windowResets;
378
+ const windowUtil = nonNegativeNumber(raw.utilization, undefined);
379
+ if (windowUtil !== undefined)
380
+ reading.utilization = windowUtil;
381
+ windows.push(JSON.stringify(reading));
382
+ }
383
+ return windows;
384
+ }
385
+ /** The binding window's own `unifiedWindows` record, if a rate-limit type was named. */
386
+ function claudeRateLimitOwnWindow(info, rateLimitType) {
387
+ return rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
388
+ }
389
+ /** The binding window's reset time: the top-level `resetsAt` falling back to its own record. */
390
+ function claudeRateLimitResetsAt(info, own) {
391
+ return nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
392
+ }
393
+ /** The binding window's utilization: the top-level `utilization` falling back to its own record. */
394
+ function claudeRateLimitUtilization(info, own) {
395
+ return nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
396
+ }
397
+ /** The binding window's canonical reading: its optional fields, plus a rejected window's
398
+ * retry-after. */
399
+ function claudeRateLimitReading(info, rateLimitType, status) {
400
+ const own = claudeRateLimitOwnWindow(info, rateLimitType);
401
+ const resetsAt = claudeRateLimitResetsAt(info, own);
402
+ const out = { type: "rate_limit" };
403
+ if (rateLimitType !== undefined)
404
+ out.rateLimitType = rateLimitType;
405
+ if (status !== undefined)
406
+ out.status = status;
407
+ if (resetsAt !== undefined)
408
+ out.resetsAt = resetsAt;
409
+ const utilization = claudeRateLimitUtilization(info, own);
410
+ if (utilization !== undefined)
411
+ out.utilization = utilization;
412
+ const overageStatus = optionalString(info.overageStatus);
413
+ if (overageStatus !== undefined)
414
+ out.overageStatus = overageStatus;
415
+ if (info.isUsingOverage === true)
416
+ out.isUsingOverage = true;
417
+ if (status === "rejected" && resetsAt !== undefined) {
418
+ // resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
419
+ out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
420
+ }
421
+ return out;
422
+ }
366
423
  /** The run's billing, from `system/init.apiKeySource`: claude.ai OAuth reports `"none"`, an
367
424
  * environment key reports `ANTHROPIC_API_KEY`, a helper reports `apiKeyHelper`. A
368
425
  * third-party provider overrides both — it is API billing even when the init omitted a
@@ -84,6 +84,14 @@ export function parseCodexLoginStatus(stdout) {
84
84
  return "api";
85
85
  return null;
86
86
  }
87
+ /** The refusal message for a codex logged in with an API key, shared by the startup gate, the
88
+ * live-config warn and the doctor's codex auth check (decision 5): Codex reports no cost, so
89
+ * per-token billing would spend money the budget cap cannot see. `home` names the codex home
90
+ * the operator should re-login under; the gate and the live warn pass the literal "<home>"
91
+ * placeholder because they do not resolve one, while the doctor passes the resolved home. */
92
+ export function codexApiKeyRefusalMessage(home) {
93
+ return `Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run \`CODEX_HOME=${home} codex login --device-auth\``;
94
+ }
87
95
  /** The billing the login probe names, or null when it cannot be decided. */
88
96
  function seedBilling(config) {
89
97
  const status = probeLoginStatus(config);
@@ -106,11 +106,11 @@ export const EFFORT_BY_THINKING = {
106
106
  * spawn on a model that lists it. */
107
107
  const KNOWN_EFFORTS = new Set(Object.values(EFFORT_BY_THINKING));
108
108
  /** Map a thinking level to the Codex `model_reasoning_effort` it means, gated by the model's own
109
- * catalog. `levels` is the model's `supported_reasoning_levels` (design finding 14), which a
110
- * stream-minted catalog (part 8/9) supplies; until then the caller passes `undefined`, and the
111
- * result is returned unclamped. An unknown thinking level returns `undefined` (no effort flag) —
112
- * the level simply has no Codex effort, unlike a thinking level that maps but the model does not
113
- * support, which is also `undefined`. */
109
+ * catalog. `levels` is the model's `supported_reasoning_levels` (design finding 14), which the
110
+ * codex catalog (part 8/9) supplies; when the caller has no catalog it passes `undefined`, and
111
+ * the result is returned unclamped. An unknown thinking level returns `undefined` (no effort
112
+ * flag) — the level simply has no Codex effort, unlike a thinking level that maps but the model
113
+ * does not support, which is also `undefined`. */
114
114
  export function codexEffort(thinking, levels) {
115
115
  if (!Object.hasOwn(EFFORT_BY_THINKING, thinking))
116
116
  return undefined;
@@ -1,7 +1,7 @@
1
1
  /** The codex adapter for the agent seam (plans/codex-backend.md §4, §5, §6): it shapes one
2
2
  * run's argv/stdin/env, translates codex's `exec --json` stream, reads the authoritative
3
3
  * rollout after the process exits, and classifies the exit's stderr — everything
4
- * backend-specific. src/agent/run-spawn.ts spawns the run and records this run in the session
4
+ * backend-specific. src/agent/agent-spawn.ts spawns the run and records this run in the session
5
5
  * ledger; codex mints its own session id (a thread id, or the resumed record's id) in the
6
6
  * stream's `thread.started`, which the runner's stream-mint pass appends to the ledger and
7
7
  * `invocation` returns so a later `exec resume` can reach it.
@@ -12,7 +12,7 @@
12
12
  * thread and the existing `handoff` path takes over (design §4). Together with the `resumable`
13
13
  * hook, that is every resume point's one rule.
14
14
  *
15
- * Registration lives in src/agent/run-agent.ts beside the pi, claude and opencode adapters; a
15
+ * Registration lives in src/agent/agent-run.ts beside the pi, claude and opencode adapters; a
16
16
  * test selects codex by setting `config.agent = "codex"` on a run. A run carrying a note gets
17
17
  * the notes path in its environment (the `tumwater-notes` command on PATH reads it), as the
18
18
  * pi and claude adapters do. */
@@ -1,18 +1,31 @@
1
+ /** Building the environment for one codex run (plans/codex-backend.md "The invocation" §3,
2
+ * findings 10, 13) — pure scrub-and-set over a base environment, with no subprocess. The caller
3
+ * layers the run-marker, the macOS LaunchServices preload and the notes shim on first, then hands
4
+ * the result here.
5
+ *
6
+ * Inherited state from a host Codex or Claude Code session breaks the team run: a Codex child
7
+ * keeps every `CODEX_*` it was given (finding 13), and `--ignore-user-config` still reads a host
8
+ * home whose `config.toml` a run writes into (finding 10). So every `CODEX_*`, the host Claude
9
+ * session's `CLAUDECODE` / `CLAUDE_CODE_*`, and both API keys an operator might carry
10
+ * (`OPENAI_API_KEY`, `CODEX_API_KEY`) are scrubbed — pi's OpenAI key must never switch Codex to
11
+ * per-token billing behind tumwater's back (decision 5). Everything else survives, including the
12
+ * run marker and the PATH that carries the notes shim.
13
+ *
14
+ * The set half points Codex at a home of the team's own (`CODEX_HOME`) when one is configured, so
15
+ * team sessions and trust entries stay out of the operator's `~/.codex` (finding 10). Unset, runs
16
+ * keep the operator's home. */
17
+ import { scrubEnv } from "../env-scrub.js";
1
18
  /** Build the codex process environment from `base`. Never mutates `base`. */
2
19
  export function codexEnv(base, opts = {}) {
3
- const env = {};
4
- for (const [key, value] of Object.entries(base)) {
5
- if (key === "CLAUDECODE")
6
- continue;
7
- if (key.startsWith("CODEX_"))
8
- continue;
9
- if (key.startsWith("CLAUDE_CODE_"))
10
- continue;
11
- if (key === "OPENAI_API_KEY" || key === "CODEX_API_KEY")
12
- continue;
13
- env[key] = value;
14
- }
15
- if (opts.home)
16
- env.CODEX_HOME = opts.home;
17
- return env;
20
+ return scrubEnv(base, {
21
+ keep: (key) => key !== "CLAUDECODE" &&
22
+ !key.startsWith("CODEX_") &&
23
+ !key.startsWith("CLAUDE_CODE_") &&
24
+ key !== "OPENAI_API_KEY" &&
25
+ key !== "CODEX_API_KEY",
26
+ then: (env) => {
27
+ if (opts.home)
28
+ env.CODEX_HOME = opts.home;
29
+ },
30
+ });
18
31
  }
@@ -37,29 +37,48 @@ function hookListMessages(cwd) {
37
37
  * `eventName: "preToolUse"` and `command` exactly the guard definition's (finding 3), or null
38
38
  * when none matches. The key and hash are returned as codex reports them. */
39
39
  function findTrust(result, definition) {
40
+ for (const group of hookGroups(result)) {
41
+ for (const hook of groupHooks(group)) {
42
+ const trust = matchHookTrust(hook, definition);
43
+ if (trust !== null)
44
+ return trust;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /** The top-level group list from `hooks/list`'s `result.data`, or `[]` when the result is not
50
+ * an object or has no array `data`. */
51
+ function hookGroups(result) {
40
52
  if (typeof result !== "object" || result === null)
41
- return null;
53
+ return [];
42
54
  const data = result.data;
43
- if (!Array.isArray(data))
55
+ if (Array.isArray(data))
56
+ return data;
57
+ return [];
58
+ }
59
+ /** One group's hook list from its `hooks` field, or `[]` when the group is not an object or
60
+ * has no array `hooks`. */
61
+ function groupHooks(group) {
62
+ if (typeof group !== "object" || group === null)
63
+ return [];
64
+ const hooks = group.hooks;
65
+ if (Array.isArray(hooks))
66
+ return hooks;
67
+ return [];
68
+ }
69
+ /** The trust of one hook when it is the guard's — `source` `sessionFlags`, `eventName`
70
+ * `preToolUse`, `command` exactly the definition's, with string `key` and `currentHash` — or
71
+ * null. */
72
+ function matchHookTrust(hook, definition) {
73
+ if (typeof hook !== "object" || hook === null)
44
74
  return null;
45
- for (const group of data) {
46
- if (typeof group !== "object" || group === null)
47
- continue;
48
- const hooks = group.hooks;
49
- if (!Array.isArray(hooks))
50
- continue;
51
- for (const hook of hooks) {
52
- if (typeof hook !== "object" || hook === null)
53
- continue;
54
- const h = hook;
55
- if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
56
- continue;
57
- if (h.command !== definition.command)
58
- continue;
59
- if (typeof h.key === "string" && typeof h.currentHash === "string") {
60
- return { key: h.key, hash: h.currentHash };
61
- }
62
- }
75
+ const h = hook;
76
+ if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
77
+ return null;
78
+ if (h.command !== definition.command)
79
+ return null;
80
+ if (typeof h.key === "string" && typeof h.currentHash === "string") {
81
+ return { key: h.key, hash: h.currentHash };
63
82
  }
64
83
  return null;
65
84
  }
@@ -29,6 +29,33 @@ function windowStatus(windowType, rateLimitReachedType, usedPercent) {
29
29
  return "allowed_warning";
30
30
  return "allowed";
31
31
  }
32
+ /** The carrier type of one rollout line: an `event_msg` wrapper's `payload.type` when the
33
+ * outer `type` is `event_msg`, else the outer `type` itself. `task_started`, `task_complete`
34
+ * and `token_count` come wrapped in an `event_msg` whose `payload.type` discriminates;
35
+ * `turn_context` and `token_usage_record` name themselves in the outer `type`. Read either. */
36
+ function lineCarrierType(parsed, payload) {
37
+ return optionalString(parsed.type) === "event_msg" ? optionalString(payload.type) : optionalString(parsed.type);
38
+ }
39
+ /** Whether a `task_started` line begins the read turn and, when it does, its turn id. Each new
40
+ * task_started begins a fresh turn (a resume appends a second run to the same file), so the
41
+ * collectors restart at the block we are reading. With no turn id (the adapter's read) every
42
+ * task_started restarts them and the last block wins; with one, only the requested turn's does. */
43
+ function turnStartReading(payload, sinceTurnStart) {
44
+ const turnId = optionalString(payload.turn_id);
45
+ return { turnId, readsTurn: sinceTurnStart === undefined || turnId === sinceTurnStart };
46
+ }
47
+ /** The usage figure a `token_usage_record` line contributes — its `turn_token_usage` when it
48
+ * carries input or output tokens, else undefined. */
49
+ function usageFromRecord(payload) {
50
+ const turnUsage = objectOrEmpty(payload.turn_token_usage);
51
+ return isJsonObject(turnUsage) && (turnUsage.input_tokens !== undefined || turnUsage.output_tokens !== undefined) ? turnUsage : undefined;
52
+ }
53
+ /** The rate limits and `plan_type` a `token_count` line contributes, from its `rate_limits`
54
+ * object (assigned even when empty). */
55
+ function rateLimitsFromCount(payload) {
56
+ const rateLimits = objectOrEmpty(payload.rate_limits);
57
+ return { rateLimits, planType: optionalString(rateLimits.plan_type) };
58
+ }
32
59
  /** Scan ONE turn's records out of a rollout's text, tracking `task_started` blocks and
33
60
  * collecting the read turn's `turn_context.model`, `token_usage_record` usage, `token_count`
34
61
  * rate limits and `plan_type`. `rolloutLines` decides emptiness from `sawTurn`/`lastUsage`;
@@ -47,17 +74,11 @@ function scanTurnRecords(text, sinceTurnStart) {
47
74
  if (!parsed)
48
75
  continue; // a torn or foreign line belongs to no turn
49
76
  const payload = objectOrEmpty(parsed.payload);
50
- // `task_started`, `task_complete` and `token_count` come wrapped in an `event_msg` whose
51
- // `payload.type` discriminates; `turn_context` and `token_usage_record` name themselves in
52
- // the outer `type`. Read either.
53
- const ptype = optionalString(parsed.type) === "event_msg" ? optionalString(payload.type) : optionalString(parsed.type);
77
+ const ptype = lineCarrierType(parsed, payload);
54
78
  if (ptype === "task_started") {
55
- currentTurn = optionalString(payload.turn_id);
56
- // Each new task_started begins a fresh turn (a resume appends a second run to the same
57
- // file), so the collectors restart at the block we are reading. With no turn id (the
58
- // adapter's read) every task_started restarts them and the last block wins; with one,
59
- // only the requested turn's does.
60
- if (sinceTurnStart === undefined || currentTurn === sinceTurnStart) {
79
+ const start = turnStartReading(payload, sinceTurnStart);
80
+ currentTurn = start.turnId;
81
+ if (start.readsTurn) {
61
82
  sawTurn = true;
62
83
  lastUsage = undefined;
63
84
  lastRateLimits = undefined;
@@ -73,19 +94,14 @@ function scanTurnRecords(text, sinceTurnStart) {
73
94
  if (model === undefined)
74
95
  model = optionalString(payload.model);
75
96
  break;
76
- case "token_usage_record": {
77
- const turnUsage = objectOrEmpty(payload.turn_token_usage);
78
- if (isJsonObject(turnUsage) && (turnUsage.input_tokens !== undefined || turnUsage.output_tokens !== undefined)) {
79
- lastUsage = turnUsage;
80
- }
97
+ case "token_usage_record":
98
+ lastUsage = usageFromRecord(payload) ?? lastUsage;
81
99
  break;
82
- }
83
100
  case "token_count": {
84
- const rateLimits = objectOrEmpty(payload.rate_limits);
85
- lastRateLimits = rateLimits;
86
- const plan = optionalString(rateLimits.plan_type);
87
- if (plan !== undefined)
88
- planType = plan;
101
+ const reading = rateLimitsFromCount(payload);
102
+ lastRateLimits = reading.rateLimits;
103
+ if (reading.planType !== undefined)
104
+ planType = reading.planType;
89
105
  break;
90
106
  }
91
107
  default:
@@ -94,52 +110,73 @@ function scanTurnRecords(text, sinceTurnStart) {
94
110
  }
95
111
  return { sawTurn, lastUsage, lastRateLimits, model, planType };
96
112
  }
97
- /** Assemble the reading's canonical `run_usage`, `billing` and up-to-two `rate_limit` lines
98
- * (in that order), plus the `switchedTo` model comparison, from scanned records. */
99
- function buildRolloutLines(records, requestedModel) {
100
- // `rolloutLines` guarantees `lastUsage` is a Json object before calling (its presence check
101
- // stays there, so this builder never reads `sawTurn`); narrow the shared interface here.
102
- const lastUsage = records.lastUsage;
103
- const { lastRateLimits, model, planType } = records;
104
- const lines = [];
113
+ /** The `run_usage` line of a turn's scanned usage: this run's own input (net of what the
114
+ * cache already held), output and cache figures (finding 4: the stream reports only the
115
+ * whole-thread total; the rollout records this run's own). */
116
+ function runUsageLine(lastUsage) {
105
117
  const input = finite(lastUsage.input_tokens) ?? 0;
106
118
  const cacheRead = finite(lastUsage.cached_input_tokens) ?? 0;
107
- lines.push(JSON.stringify({
119
+ return JSON.stringify({
108
120
  type: "run_usage",
109
121
  input: Math.max(0, input - cacheRead),
110
122
  output: finite(lastUsage.output_tokens) ?? 0,
111
123
  cacheRead,
112
124
  cacheWrite: finite(lastUsage.cache_write_input_tokens) ?? 0,
113
- }));
114
- if (planType !== undefined) {
115
- // A reading with a plan_type bills on the subscription (finding 5; API-key login is
116
- // refused, decision 5).
117
- lines.push(JSON.stringify({ type: "billing", billing: "subscription" }));
118
- }
125
+ });
126
+ }
127
+ /** The `billing` line of a turn with a plan_type: a reading with a plan_type bills on the
128
+ * subscription (finding 5; API-key login is refused, decision 5). */
129
+ function billingLine() {
130
+ // A reading with a plan_type bills on the subscription (finding 5; API-key login is
131
+ // refused, decision 5).
132
+ return JSON.stringify({ type: "billing", billing: "subscription" });
133
+ }
134
+ /** The up-to-two `rate_limit` lines of a turn's scanned rate limits: the primary (5 h) and
135
+ * the secondary (weekly) windows when both are present and well-formed. */
136
+ function rateLimitLines(lastRateLimits) {
119
137
  // Two windows when present: the primary (5 h) and the secondary (weekly). A killed run's
120
138
  // rollout has a token_usage_record but no token_count, so it yields run_usage with no
121
139
  // rate_limit (finding 23).
122
- if (lastRateLimits !== undefined) {
123
- const reached = optionalString(lastRateLimits.rate_limit_reached_type);
124
- for (const key of ["primary", "secondary"]) {
125
- const windowRaw = lastRateLimits[key];
126
- if (!isJsonObject(windowRaw))
127
- continue;
128
- const minutes = finite(windowRaw.window_minutes);
129
- if (minutes === undefined)
130
- continue;
131
- const typeName = windowTypeName(minutes);
132
- const usedPercent = finite(windowRaw.used_percent);
133
- lines.push(JSON.stringify({
134
- type: "rate_limit",
135
- rateLimitType: typeName,
136
- status: windowStatus(typeName, reached, usedPercent),
137
- ...(usedPercent !== undefined ? { utilization: usedPercent / 100 } : {}),
138
- ...(finite(windowRaw.resets_at) !== undefined ? { resetsAt: finite(windowRaw.resets_at) } : {}),
139
- }));
140
- }
140
+ if (lastRateLimits === undefined)
141
+ return [];
142
+ const reached = optionalString(lastRateLimits.rate_limit_reached_type);
143
+ const lines = [];
144
+ for (const key of ["primary", "secondary"]) {
145
+ const windowRaw = lastRateLimits[key];
146
+ if (!isJsonObject(windowRaw))
147
+ continue;
148
+ const minutes = finite(windowRaw.window_minutes);
149
+ if (minutes === undefined)
150
+ continue;
151
+ const typeName = windowTypeName(minutes);
152
+ const usedPercent = finite(windowRaw.used_percent);
153
+ lines.push(JSON.stringify({
154
+ type: "rate_limit",
155
+ rateLimitType: typeName,
156
+ status: windowStatus(typeName, reached, usedPercent),
157
+ ...(usedPercent !== undefined ? { utilization: usedPercent / 100 } : {}),
158
+ ...(finite(windowRaw.resets_at) !== undefined ? { resetsAt: finite(windowRaw.resets_at) } : {}),
159
+ }));
160
+ }
161
+ return lines;
162
+ }
163
+ /** The model this turn switched to when its `turn_context.model` differs from what the run
164
+ * requested (finding 24): the model itself, else undefined for no switch. */
165
+ function switchedToModel(model, requestedModel) {
166
+ return model !== undefined && model !== requestedModel ? model : undefined;
167
+ }
168
+ /** Assemble the reading's canonical `run_usage`, `billing` and up-to-two `rate_limit` lines
169
+ * (in that order), plus the `switchedTo` model comparison, from scanned records. */
170
+ function buildRolloutLines(records, requestedModel) {
171
+ // `rolloutLines` guarantees `lastUsage` is a Json object before calling (its presence check
172
+ // stays there, so this builder never reads `sawTurn`); narrow the shared interface here.
173
+ const lastUsage = records.lastUsage;
174
+ const lines = [runUsageLine(lastUsage)];
175
+ if (records.planType !== undefined) {
176
+ lines.push(billingLine());
141
177
  }
142
- const switchedTo = model !== undefined && requestedModel !== undefined && model !== requestedModel ? model : undefined;
178
+ lines.push(...rateLimitLines(records.lastRateLimits));
179
+ const switchedTo = switchedToModel(records.model, requestedModel);
143
180
  return { lines, ...(switchedTo !== undefined ? { switchedTo } : {}) };
144
181
  }
145
182
  /** Read the records of ONE turn out of a rollout's text. A rollout is one file per thread that
@@ -10,7 +10,7 @@
10
10
  * state (the open tool calls, the turn's usage fallback, the unmapped native types seen), so
11
11
  * call it once per run. Unmapped native types, the distinct text of `error` items, and a
12
12
  * command abandoned at the end of its turn are recorded once each on the seam's optional
13
- * `warnings()` — src/agent/run-stream.ts forwards each as an `agent_unknown_event` warning
13
+ * `warnings()` — src/agent/agent-stream.ts forwards each as an `agent_unknown_event` warning
14
14
  * after the run. `sessionId()` reports the thread id once `thread.started` has named it.
15
15
  */
16
16
  import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
@@ -0,0 +1,18 @@
1
+ /** Shared environment-building skeleton for the three agent backend env builders
2
+ * (claude-env.ts, codex-env.ts, opencode-env.ts). Each backend isolates one run the same shape:
3
+ * copy the keys of `base` that survive the scrub, then let the backend set its own run vars.
4
+ * The copy half never writes anything but `keep`-accepted keys of `base`, so a caller's base
5
+ * (the process env, or a layered run-marker/notes/preload env) is never mutated — the invariant
6
+ * the per-backend tests pin. */
7
+ /** Build a process environment from `base` under `scrub` without mutating `base`: copy the
8
+ * `keep`-accepted base entries into a fresh object, then hand it to `then` for the backend's
9
+ * own additions. */
10
+ export function scrubEnv(base, scrub) {
11
+ const env = {};
12
+ for (const [key, value] of Object.entries(base)) {
13
+ if (scrub.keep(key))
14
+ env[key] = value;
15
+ }
16
+ scrub.then(env);
17
+ return env;
18
+ }