tumwater 0.4.1 → 0.4.3

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 (338) hide show
  1. package/README.md +3 -5
  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} +35 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +70 -37
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +24 -24
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +6 -14
  8. package/dist/src/agent/backend-traits.js +16 -2
  9. package/dist/src/agent/claude/claude-account.js +1 -8
  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 +131 -78
  13. package/dist/src/agent/codex/codex-account.js +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +120 -69
  18. package/dist/src/agent/codex/codex-stream.js +72 -39
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +21 -33
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +52 -29
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +51 -29
  27. package/dist/src/agent/session-ledger.js +16 -0
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-archive.js +64 -165
  30. package/dist/src/backlog/backlog-cli-view.js +81 -41
  31. package/dist/src/backlog/backlog-cli.js +57 -26
  32. package/dist/src/backlog/backlog-eligibility.js +84 -136
  33. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  34. package/dist/src/backlog/backlog-index-data.js +2 -2
  35. package/dist/src/backlog/backlog-md.js +42 -31
  36. package/dist/src/backlog/backlog-parts.js +65 -27
  37. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +30 -14
  38. package/dist/src/backlog/backlog-release.js +15 -23
  39. package/dist/src/backlog/backlog-structure.js +128 -62
  40. package/dist/src/backlog/backlog-write.js +1 -1
  41. package/dist/src/backlog/backlog.js +5 -4
  42. package/dist/src/baseline/main-baseline.js +178 -104
  43. package/dist/src/baseline/main-guardrails.js +54 -17
  44. package/dist/src/baseline/main-watch.js +1 -1
  45. package/dist/src/budget/budget-fallback-view.js +100 -243
  46. package/dist/src/budget/budget-wiring.js +24 -65
  47. package/dist/src/budget/budget.js +2 -1
  48. package/dist/src/budget/fallback-breaker.js +2 -2
  49. package/dist/src/build/build-check-detect.js +43 -14
  50. package/dist/src/build/build-check-events.js +2 -2
  51. package/dist/src/build/build-check-scoped.js +103 -50
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/change/change-data.js +4 -2
  54. package/dist/src/cli/cli-command-args.js +275 -137
  55. package/dist/src/cli/cli-flag-specs.js +43 -26
  56. package/dist/src/cli/cli-init.js +34 -12
  57. package/dist/src/cli/cli-query-commands.js +1 -1
  58. package/dist/src/cli/cli-repo-gate.js +2 -2
  59. package/dist/src/cli/cli-review-commands.js +2 -1
  60. package/dist/src/cli/cli-role-command.js +27 -50
  61. package/dist/src/cli/cli-start.js +150 -67
  62. package/dist/src/cli/completion.js +82 -46
  63. package/dist/src/cli/config-commands.js +82 -71
  64. package/dist/src/cli/follow-view.js +3 -1
  65. package/dist/src/cli/help.js +6 -9
  66. package/dist/src/cli/log-commands.js +46 -24
  67. package/dist/src/cli/notify-command.js +2 -2
  68. package/dist/src/cli/question-commands.js +86 -60
  69. package/dist/src/cli.js +183 -141
  70. package/dist/src/collections.js +52 -23
  71. package/dist/src/concurrency/check-slots.js +70 -0
  72. package/dist/src/concurrency/lock.js +22 -0
  73. package/dist/src/concurrency/run-permit.js +49 -0
  74. package/dist/src/concurrency/semaphore.js +46 -250
  75. package/dist/src/config/config-editable-keys.js +4 -7
  76. package/dist/src/config/config-fallbacks.js +79 -103
  77. package/dist/src/config/config-field-checks.js +66 -37
  78. package/dist/src/config/config-keys.js +5 -20
  79. package/dist/src/config/config-live.js +161 -153
  80. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  81. package/dist/src/config/config-request.js +153 -102
  82. package/dist/src/config/config-schema.js +9 -11
  83. package/dist/src/config/config-selector-checks.js +1 -1
  84. package/dist/src/config/config-validation-agents.js +2 -2
  85. package/dist/src/config/config-validation-roles.js +4 -52
  86. package/dist/src/config/config-validation.js +4 -38
  87. package/dist/src/config/config-views.js +4 -34
  88. package/dist/src/config/config-write.js +22 -46
  89. package/dist/src/config/config.js +4 -5
  90. package/dist/src/doctor/doctor-backlog-bugs.js +24 -121
  91. package/dist/src/doctor/doctor-backlog-plans.js +27 -27
  92. package/dist/src/doctor/doctor-backlog.js +29 -41
  93. package/dist/src/doctor/doctor-check-names.js +4 -7
  94. package/dist/src/doctor/doctor-checks.js +5 -5
  95. package/dist/src/doctor/doctor-claude-checks.js +46 -30
  96. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  97. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  98. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  99. package/dist/src/doctor/doctor-holds.js +19 -56
  100. package/dist/src/doctor/doctor-install.js +1 -1
  101. package/dist/src/doctor/doctor-launch-services.js +1 -1
  102. package/dist/src/doctor/doctor-model-checks.js +181 -273
  103. package/dist/src/doctor/doctor-notify.js +4 -7
  104. package/dist/src/doctor/doctor-opencode-checks.js +90 -52
  105. package/dist/src/doctor/doctor-orphans.js +120 -20
  106. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  107. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  108. package/dist/src/doctor/doctor-principles.js +3 -3
  109. package/dist/src/doctor/{probe.js → doctor-probe.js} +1 -1
  110. package/dist/src/doctor/doctor-project-checks.js +69 -20
  111. package/dist/src/doctor/doctor-role-notes.js +7 -25
  112. package/dist/src/doctor/doctor.js +12 -15
  113. package/dist/src/events/event-format.js +19 -53
  114. package/dist/src/events/event-labels.js +0 -5
  115. package/dist/src/events/event-legacy.js +0 -2
  116. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  117. package/dist/src/events/event-read.js +1 -1
  118. package/dist/src/events/event-window.js +1 -1
  119. package/dist/src/events/events.js +15 -2
  120. package/dist/src/failure/failure-cluster.js +6 -6
  121. package/dist/src/failure/failure-data.js +8 -4
  122. package/dist/src/failure/failure-render.js +44 -34
  123. package/dist/src/failure/failure-state-change.js +177 -92
  124. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  125. package/dist/src/files/files.js +30 -0
  126. package/dist/src/files/json-object.js +21 -8
  127. package/dist/src/files/stat-cache.js +12 -8
  128. package/dist/src/gates/budget-gates.js +85 -84
  129. package/dist/src/gates/gate-polls.js +27 -68
  130. package/dist/src/gates/pause-gates.js +16 -2
  131. package/dist/src/gates/readiness.js +1 -1
  132. package/dist/src/gates/role-cap-gates.js +44 -35
  133. package/dist/src/gates/{startup-gate.js → startup-gates.js} +34 -34
  134. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  135. package/dist/src/git/git.js +75 -45
  136. package/dist/src/git/slot-salvage.js +5 -4
  137. package/dist/src/git/worktree-pool.js +3 -6
  138. package/dist/src/gui/gui-admin-endpoints.js +5 -6
  139. package/dist/src/gui/gui-backlog-commands.js +4 -4
  140. package/dist/src/gui/gui-backlog-endpoints.js +62 -48
  141. package/dist/src/gui/gui-command.js +43 -24
  142. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  143. package/dist/src/gui/gui-history-endpoints.js +100 -52
  144. package/dist/src/gui/gui-operator-commands.js +22 -35
  145. package/dist/src/gui/gui-prompt-commands.js +48 -42
  146. package/dist/src/gui/gui-report-endpoints.js +3 -2
  147. package/dist/src/gui/gui-server.js +1 -2
  148. package/dist/src/history/history-data.js +71 -44
  149. package/dist/src/history/history-queued.js +49 -27
  150. package/dist/src/history/history.js +138 -85
  151. package/dist/src/inbox/inbox-submit.js +1 -5
  152. package/dist/src/inbox/prompt-commands.js +82 -60
  153. package/dist/src/init/init.js +156 -55
  154. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  155. package/dist/src/merge/merge-core.js +2 -2
  156. package/dist/src/merge/merge-drain.js +8 -11
  157. package/dist/src/merge/merge-pipeline.js +6 -7
  158. package/dist/src/merge/merge-post-review-drain.js +11 -14
  159. package/dist/src/merge/merge-requeue.js +1 -1
  160. package/dist/src/merge/merge-slot.js +0 -8
  161. package/dist/src/merge/merge-verify.js +8 -18
  162. package/dist/src/notes-cli.js +52 -42
  163. package/dist/src/operator/operator-commands.js +143 -90
  164. package/dist/src/operator/operator-intent.js +3 -3
  165. package/dist/src/operator/{retire.js → operator-retire.js} +2 -1
  166. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  167. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  168. package/dist/src/orchestrator/orchestrator.js +8 -21
  169. package/dist/src/orchestrator/retention.js +6 -5
  170. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  171. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  172. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  173. package/dist/src/orchestrator/startup-migrations.js +1 -1
  174. package/dist/src/paths.js +8 -13
  175. package/dist/src/pi/pi-args.js +5 -2
  176. package/dist/src/pi/pi-backend.js +2 -2
  177. package/dist/src/pi/pi-bin.js +3 -3
  178. package/dist/src/pi/pi-event-line.js +1 -1
  179. package/dist/src/pi/pi-models.js +43 -29
  180. package/dist/src/pi/pi-settings.js +43 -20
  181. package/dist/src/pi/pi-stream.js +32 -6
  182. package/dist/src/pi/pi-watchdogs.js +29 -3
  183. package/dist/src/pi/pi.js +2 -2
  184. package/dist/src/pi-extension/context-shake.js +87 -51
  185. package/dist/src/pi-extension/kill-guard.js +69 -0
  186. package/dist/src/process/process-group.js +49 -5
  187. package/dist/src/process/process.js +31 -8
  188. package/dist/src/process/run-marker.js +31 -13
  189. package/dist/src/process/supervisor.js +57 -29
  190. package/dist/src/prompt/dialect.js +1 -1
  191. package/dist/src/prompt/gate-notes.js +11 -11
  192. package/dist/src/prompt/notes-backlog.js +73 -57
  193. package/dist/src/prompt/principles.js +6 -4
  194. package/dist/src/prompt/prompt.js +92 -43
  195. package/dist/src/report/report-data.js +61 -260
  196. package/dist/src/report/report-fold.js +34 -302
  197. package/dist/src/report/report-main-red.js +45 -42
  198. package/dist/src/report/report-render.js +37 -183
  199. package/dist/src/report/report-since-data.js +219 -0
  200. package/dist/src/report/report-since-render.js +173 -0
  201. package/dist/src/report/report-usage-fold.js +288 -0
  202. package/dist/src/report/report.js +48 -23
  203. package/dist/src/review/known-flakes.js +3 -3
  204. package/dist/src/review/post-merge-review-accept.js +3 -2
  205. package/dist/src/review/post-merge-review-render.js +9 -3
  206. package/dist/src/review/post-merge-review.js +10 -10
  207. package/dist/src/review/quarantine-render.js +2 -1
  208. package/dist/src/review/review-precheck.js +11 -1
  209. package/dist/src/review/review.js +40 -153
  210. package/dist/src/review/secret-leaks.js +26 -16
  211. package/dist/src/roles/role-catalog.js +28 -32
  212. package/dist/src/roles/role-guidance.js +25 -46
  213. package/dist/src/roles/role-render.js +1 -14
  214. package/dist/src/roles/role-view.js +10 -23
  215. package/dist/src/run/acceptance-greps.js +88 -45
  216. package/dist/src/run/ci-parity.js +5 -6
  217. package/dist/src/run/qa-coverage.js +4 -4
  218. package/dist/src/run/run-apply.js +12 -6
  219. package/dist/src/run/run-detail-data.js +137 -68
  220. package/dist/src/run/run-detail-render.js +97 -34
  221. package/dist/src/run/run-detail.js +106 -53
  222. package/dist/src/run/run-dismiss.js +43 -0
  223. package/dist/src/run/run-prompt.js +150 -93
  224. package/dist/src/run/run-resume.js +85 -37
  225. package/dist/src/run/run-stage.js +139 -105
  226. package/dist/src/run/run-timing.js +26 -6
  227. package/dist/src/run/run-usage.js +31 -1
  228. package/dist/src/run/run-verdict.js +71 -67
  229. package/dist/src/run/stage-backlog-checks.js +2 -29
  230. package/dist/src/run/stage-check.js +52 -43
  231. package/dist/src/run/stage-checks-shared.js +28 -0
  232. package/dist/src/run/stage-name-checks.js +111 -92
  233. package/dist/src/run/stage-path-checks.js +69 -42
  234. package/dist/src/run/stage-prose-checks.js +55 -40
  235. package/dist/src/runner/runner-outcome.js +2 -2
  236. package/dist/src/runner/runner-pi.js +3 -10
  237. package/dist/src/runner/runner-recovery.js +1 -1
  238. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  239. package/dist/src/runner/runner-run.js +23 -38
  240. package/dist/src/runner/runner-state-load.js +2 -9
  241. package/dist/src/runner/runner.js +21 -36
  242. package/dist/src/scheduling/claims.js +3 -14
  243. package/dist/src/scheduling/scheduling.js +53 -66
  244. package/dist/src/status/status-data.js +7 -11
  245. package/dist/src/status/status-role-notes.js +20 -16
  246. package/dist/src/status/status-roles.js +11 -52
  247. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  248. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  249. package/dist/src/team/team-hold.js +98 -65
  250. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  251. package/dist/src/team/team-polls.js +28 -126
  252. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  253. package/dist/src/team/team-state.js +4 -13
  254. package/dist/src/text/datetime.js +8 -1
  255. package/dist/src/text/markdown.js +1 -1
  256. package/dist/src/text/phrases.js +20 -15
  257. package/dist/src/text/text.js +15 -3
  258. package/dist/src/ui/alert-feeds.js +52 -281
  259. package/dist/src/ui/badges.js +3 -14
  260. package/dist/src/ui/gui/gui-client-boot.js +21 -6
  261. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  262. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  263. package/dist/src/ui/gui/gui-client-drawer.js +2 -2
  264. package/dist/src/ui/gui/gui-client-history.js +28 -14
  265. package/dist/src/ui/gui/gui-client-model.js +0 -12
  266. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  267. package/dist/src/ui/gui/gui-client-report-chart.js +12 -10
  268. package/dist/src/ui/gui/gui-client-report.js +1 -2
  269. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  270. package/dist/src/ui/gui/gui-client-settings.js +43 -107
  271. package/dist/src/ui/gui/gui-client-team.js +30 -4
  272. package/dist/src/ui/gui/gui-client.js +7 -1
  273. package/dist/src/ui/gui/gui-icons.js +7 -1
  274. package/dist/src/ui/progress-data.js +53 -40
  275. package/dist/src/ui/run-progress-model.js +9 -2
  276. package/dist/src/ui/status-model.js +4 -26
  277. package/dist/src/ui/status-payload.js +4 -8
  278. package/dist/src/ui/status-render.js +6 -10
  279. package/dist/src/ui/team-alert-backlog.js +29 -60
  280. package/dist/src/ui/team-alert-shared.js +23 -10
  281. package/dist/src/ui/team-alert-status.js +17 -173
  282. package/dist/src/ui/team-alert-tail.js +19 -228
  283. package/dist/src/ui/team-alerts.js +12 -24
  284. package/dist/src/ui/transcript-run.js +27 -16
  285. package/dist/src/ui/transcript-tail.js +137 -94
  286. package/dist/src/ui/tui/tui-editor-answer.js +66 -54
  287. package/dist/src/ui/tui/tui-editor-attach.js +77 -65
  288. package/dist/src/ui/tui/tui-editor-backends.js +65 -58
  289. package/dist/src/ui/tui/tui-editor-brief.js +64 -57
  290. package/dist/src/ui/tui/tui-editor-budget.js +109 -96
  291. package/dist/src/ui/tui/tui-editor-config.js +81 -71
  292. package/dist/src/ui/tui/tui-editor-entry.js +167 -157
  293. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  294. package/dist/src/ui/tui/tui-editor-pause.js +208 -188
  295. package/dist/src/ui/tui/tui-editor-prompt.js +50 -44
  296. package/dist/src/ui/tui/tui-editor-queued.js +74 -67
  297. package/dist/src/ui/tui/tui-editor-quiet.js +124 -110
  298. package/dist/src/ui/tui/tui-editor-release.js +53 -44
  299. package/dist/src/ui/tui/tui-editor-retire.js +60 -49
  300. package/dist/src/ui/tui/tui-editor-role-enabled.js +93 -82
  301. package/dist/src/ui/tui/tui-editor-role-file.js +61 -49
  302. package/dist/src/ui/tui/tui-editor-role-key.js +52 -44
  303. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  304. package/dist/src/ui/tui/tui-editor-search.js +73 -60
  305. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  306. package/dist/src/ui/tui/tui-editors.js +1 -7
  307. package/dist/src/ui/tui/tui-frame.js +182 -134
  308. package/dist/src/ui/tui/tui-guard.js +20 -0
  309. package/dist/src/ui/tui/tui-input.js +159 -92
  310. package/dist/src/ui/tui/tui-keymap.js +51 -29
  311. package/dist/src/ui/tui/tui-keys.js +2 -2
  312. package/dist/src/ui/tui/tui-pane-activity.js +65 -53
  313. package/dist/src/ui/tui/tui-pane-backlog.js +78 -40
  314. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  315. package/dist/src/ui/tui/tui-pane-health.js +254 -251
  316. package/dist/src/ui/tui/tui-pane-history.js +81 -94
  317. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  318. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  319. package/dist/src/ui/tui/tui-pane-report.js +76 -58
  320. package/dist/src/ui/tui/tui-pane-session.js +14 -9
  321. package/dist/src/ui/tui/tui-pane-transcript.js +152 -141
  322. package/dist/src/ui/tui/tui-pane.js +205 -175
  323. package/dist/src/ui/tui/tui-panes.js +31 -35
  324. package/dist/src/ui/tui/tui.js +4 -5
  325. package/dist/src/verdict/fix-claim.js +87 -53
  326. package/dist/src/verdict/refusal.js +31 -7
  327. package/package.json +5 -5
  328. package/dist/src/concurrency/provider-lease.js +0 -105
  329. package/dist/src/doctor/image-probe.js +0 -257
  330. package/dist/src/runner/model-fallback.js +0 -76
  331. package/dist/src/runner/runner-fallback.js +0 -66
  332. package/dist/src/ui/team-alert-backlog-feed.js +0 -181
  333. package/dist/src/ui/team-alert-merge.js +0 -183
  334. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  335. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  336. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  337. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  338. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
package/README.md CHANGED
@@ -42,7 +42,7 @@ and is also outside `npm test`.
42
42
  ## Status
43
43
 
44
44
  <!-- tumwater:status:start -->
45
- **v0.4.1**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
45
+ **v0.4.3**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
46
46
  which files tumwater bugs from tumwater's own event log, and `feedback`, which files them as GitHub
47
47
  issues on znation/tumwater, are opt-in (`roles.selfcheck.enabled`, `roles.feedback.enabled`).
48
48
 
@@ -77,8 +77,7 @@ so pair it with `--token <secret>`.
77
77
  `tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
78
78
  model cannot read images, the command refuses the attachment up front (naming the fix) unless
79
79
  the role reroutes a blind read to a vision model, in which case it says where the message will
80
- run; `tumwater doctor` probes each claimed image-input model once (cached 7 days, `--reprobe` to
81
- force) and the dashboards and `tumwater role <id>` say which roles read images and where a blind
80
+ run; the dashboards and `tumwater role <id>` say which roles read images and where a blind
82
81
  run reroutes.
83
82
 
84
83
  Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
@@ -109,8 +108,7 @@ object with both, so one config can mix a Claude strong tier with pi providers:
109
108
 
110
109
  The default tier runs on pi through Hugging Face, the strong model jobs (the director, the plan
111
110
  role, the reviewer, and the conflict resolver) run on the claude CLI's included usage, and the fallback runs on pi through
112
- oMLX — all concurrently, under each provider's cap (set it with `maxConcurrentPerProvider`;
113
- `tumwater status` shows how many runs each key has going out of its cap). An OpenCode tier is
111
+ oMLX — all concurrently, under the team's `maxConcurrent` cap. An OpenCode tier is
114
112
  `{ "backend": "opencode", "model": "deepseek-v4.1-flash-free" }`, so its free tier can sit beside pi
115
113
  and claude in one config; a codex tier is `{ "backend": "codex", "model": "gpt-6.1-sol" }`, so a ChatGPT
116
114
  subscription can cover the team's default tier beside pi and claude. A Claude model that should stay on pi (billed per token) is the plain
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "defe3e5dd243a9eb3c844751752328d88f5ad674",
3
- "builtAt": 1792989016668,
2
+ "sha": "45348f61e3e060de873bbd10ce8cdea30e4102d6",
3
+ "builtAt": 1793264084617,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -1,16 +1,16 @@
1
1
  /** The backend seam (plans/agent-backends.md "The seam"): what one non-interactive agent
2
2
  * run needs from its backend — how to build the command, how to translate its native stdout
3
- * into tumwater's canonical event stream, and how to classify its exit. src/agent/run-spawn.ts
4
- * builds and spawns the run (the marker, parser, bin resolution and spawn); src/agent/run-agent.ts
3
+ * into tumwater's canonical event stream, and how to classify its exit. src/agent/agent-spawn.ts
4
+ * builds and spawns the run (the marker, parser, bin resolution and spawn); src/agent/agent-run.ts
5
5
  * keeps the coordinator — the backend-neutral watchdogs, the abort wiring and the settlement-state
6
- * assembly — while src/agent/run-stream.ts owns the streaming pipeline (native-to-canonical stdout
7
- * split and the stderr window) and the settle/flush; the
6
+ * assembly — while src/agent/agent-stream.ts owns the streaming pipeline (the native-to-canonical
7
+ * stdout split and the stderr window) and the settle/flush; the
8
8
  * post-run settlement — the group-kill and marker sweeps on exit, the spawn-error result and
9
9
  * the close-time reconcile/classify that builds the `PiRunResult` — lives in
10
- * src/agent/run-settle.ts. A backend registered here supplies the rest. src/agent/run-agent.ts
10
+ * src/agent/agent-settle.ts. A backend registered here supplies the rest. src/agent/agent-run.ts
11
11
  * registers the pi and claude adapters here.
12
12
  *
13
- * The registry is keyed by id and fed by explicit registration (src/agent/run-agent.ts
13
+ * The registry is keyed by id and fed by explicit registration (src/agent/agent-run.ts
14
14
  * registers both adapters at load), so a test can register a fake backend without importing a
15
15
  * real one. */
16
16
  import { hasResumableSession } from "./session-ledger.js";
@@ -6,16 +6,16 @@
6
6
  * matcher, and the exit classification. This file keeps the coordinator — setup, the watchdog
7
7
  * wiring, abort, the child's stream-attachment and the settlement-state assembly — and
8
8
  * delegates the build-and-spawn stage (the marker,
9
- * parser, bin resolution, `prepare`, invocation and spawn) to src/agent/run-spawn.ts, the
9
+ * parser, bin resolution, `prepare`, invocation and spawn) to src/agent/agent-spawn.ts, the
10
10
  * streaming pipeline (native-to-canonical stdout split, the stderr window, the stream-minted
11
- * session record and the settle/flush) to src/agent/run-stream.ts, and the post-run settlement
11
+ * session record and the settle/flush) to src/agent/agent-stream.ts, and the post-run settlement
12
12
  * (spawn-error result, exit orphan sweep, close-time reconcile and
13
- * classify) to src/agent/run-settle.ts. `runPi` (src/pi/pi.ts) is the thin alias the existing
13
+ * classify) to src/agent/agent-settle.ts. `runPi` (src/pi/pi.ts) is the thin alias the existing
14
14
  * call sites use, so a caller and its fakes are untouched by this split. */
15
15
  import fs from "node:fs";
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
- import { ensureDir } from "../files/files.js";
18
+ import { ensureDir, removeTree } from "../files/files.js";
19
19
  import { runOnAbort, terminateChild } from "../process/process.js";
20
20
  import { startPiWatchdogs } from "../pi/pi-watchdogs.js";
21
21
  import { DEFAULT_BACKEND, backendById, registerAgentBackend } from "./agent-backend.js";
@@ -24,12 +24,12 @@ import { piBackend } from "../pi/pi-backend.js";
24
24
  import { claudeBackend } from "./claude/claude-backend.js";
25
25
  import { codexBackend } from "./codex/codex-backend.js";
26
26
  import { opencodeBackend } from "./opencode/opencode-backend.js";
27
- import { setupFailureResult, settleClose, settleError, settleExit } from "./run-settle.js";
28
- import { openAppendLog, spawnPiRun } from "./run-spawn.js";
29
- import { createAgentStream } from "./run-stream.js";
27
+ import { setupFailureResult, settleClose, settleError, settleExit } from "./agent-settle.js";
28
+ import { openAppendLog, spawnPiRun } from "./agent-spawn.js";
29
+ import { createAgentStream } from "./agent-stream.js";
30
30
  // The streaming pipeline's over-long-native-line cap stays reachable from here, the historical
31
- // home the test suite still imports it from, even though the value now lives in run-stream.ts.
32
- export { MAX_NATIVE_LINE_CHARS } from "./run-stream.js";
31
+ // home the test suite still imports it from, even though the value now lives in agent-stream.ts.
32
+ export { MAX_NATIVE_LINE_CHARS } from "./agent-stream.js";
33
33
  // The seam's adapters. Registering here — where the lookup happens — keeps the registration
34
34
  // explicit and lets a test register a fake under its own id.
35
35
  registerAgentBackend(piBackend);
@@ -78,7 +78,7 @@ export function runAgent(opts) {
78
78
  // The whole build-and-spawn stage — the marker write, parser construction, run marker,
79
79
  // backend-bin resolution, notes shim, run spec, the backend's optional `prepare`, the
80
80
  // session-ledger record, translator/decoders, and the child spawn — now lives in
81
- // spawnPiRun (src/agent/run-spawn.ts) and returns the SpawnedRun bundle. A synchronous
81
+ // spawnPiRun (src/agent/agent-spawn.ts) and returns the SpawnedRun bundle. A synchronous
82
82
  // throw anywhere in it (a broken marker write, parser construction, backend.resolveBin,
83
83
  // claude's invocation rejecting a run with no model) propagates out of the await and is
84
84
  // rejected by the catch below, matching what a synchronous executor's throw used to get
@@ -133,7 +133,7 @@ export function runAgent(opts) {
133
133
  // The streaming pipeline — native-to-canonical stdout split with the over-long-line
134
134
  // discard, the stderr window and its fallback-clone warning matcher, the stream-minted
135
135
  // session's second ledger record, and the settle/flush that resolves the run — now lives
136
- // in src/agent/run-stream.ts behind the AgentStream interface. run-agent only wires the
136
+ // in src/agent/agent-stream.ts behind the AgentStream interface. agent-run only wires the
137
137
  // child's `data` events to it and binds its getters into the settlement state below.
138
138
  const stream = createAgentStream({
139
139
  opts,
@@ -148,7 +148,7 @@ export function runAgent(opts) {
148
148
  });
149
149
  child.stdout?.on("data", (chunk) => stream.feedStdout(chunk));
150
150
  child.stderr?.on("data", (chunk) => stream.feedStderr(chunk));
151
- // The run state the settlement-half handlers (run-settle.ts) read when their event
151
+ // The run state the settlement-half handlers (agent-settle.ts) read when their event
152
152
  // fires. The getters read through to the run's still-mutating feed state, so a handler
153
153
  // sees the current value rather than a snapshot taken when its listener was registered.
154
154
  const state = {
@@ -208,7 +208,29 @@ export function runAgent(opts) {
208
208
  catch (err) {
209
209
  // A synchronous throw in the run's build-and-spawn stage (claude's invocation rejects a
210
210
  // run with no resolved model) rejects the run, preserving the contract a synchronous
211
- // executor's throw used to get from the Promise constructor.
211
+ // executor's throw used to get from the Promise constructor. No child was spawned, so no
212
+ // `error`/`close` ever fires and the settle path that removes the private TMPDIR
213
+ // (agent-stream.ts's finish) never runs — remove it here, best-effort like finish does, so
214
+ // a broken run does not leave a `tumwater-run-*` temp dir behind.
215
+ try {
216
+ removeTree(runTmp);
217
+ }
218
+ catch {
219
+ /* left for the OS temp sweep */
220
+ }
221
+ // The two append-log streams opened before the build-and-spawn stage (the raw and
222
+ // backend-native transcripts) are never handed to a stream-settlement path on this throw
223
+ // — there is no child and no finish(), so nothing else would ever end them — end them
224
+ // here best-effort (the same cleanup the outer setup-failure catch makes) or every
225
+ // rejected run leaks two open transcript descriptors that accumulate in the
226
+ // long-lived orchestrator process.
227
+ try {
228
+ rawLog?.stream.end();
229
+ nativeLog?.stream.end();
230
+ }
231
+ catch {
232
+ /* best-effort */
233
+ }
212
234
  reject(err instanceof Error ? err : new Error(`runAgent: ${String(err)}`));
213
235
  }
214
236
  });
@@ -1,15 +1,15 @@
1
- /** The settlement half of a backend agent run (the split of src/agent/run-agent.ts along the
1
+ /** The settlement half of a backend agent run (the split of src/agent/agent-run.ts along the
2
2
  * same seam the runner-run → runner-outcome split used): everything that happens after the
3
3
  * run's live feed — the spawn-`error` result, the `exit` orphan sweep, and the `close`-time
4
- * flush/reconcile/classify that builds the `PiRunResult`. run-agent.ts keeps the lifecycle
5
- * (setup, watchdogs, the feed), delegates its build-and-spawn stage to src/agent/run-spawn.ts,
6
- * its streaming pipeline to src/agent/run-stream.ts and its `error`/`exit`/`close` listeners
4
+ * flush/reconcile/classify that builds the `PiRunResult`. agent-run.ts keeps the lifecycle
5
+ * (setup, watchdogs, the feed), delegates its build-and-spawn stage to src/agent/agent-spawn.ts,
6
+ * its streaming pipeline to src/agent/agent-stream.ts and its `error`/`exit`/`close` listeners
7
7
  * here, so the streaming half stays readable.
8
8
  *
9
- * Each handler takes the run's live state (`RunSettleState`, defined in run-agent.ts and
9
+ * Each handler takes the run's live state (`RunSettleState`, defined in agent-run.ts and
10
10
  * imported here only as a type) and reads what it closes over at the moment its event fires —
11
11
  * the live flags it needs are getters on that state. There is no runtime import cycle:
12
- * run-agent imports these functions at runtime, and this module imports only run-agent's type.
12
+ * agent-run imports these functions at runtime, and this module imports only agent-run's type.
13
13
  */
14
14
  import { backendBinConfigKey, backendBinSourceLabel } from "../pi/pi-bin.js";
15
15
  import { terminateChild } from "../process/process.js";
@@ -144,58 +144,91 @@ export function settleExit(state) {
144
144
  })
145
145
  .catch(() => { });
146
146
  }
147
- /** The run's `close` event: the process died and all its streams closed. Flush what the
148
- * decoders held back, reconcile the backend's post-run work, classify the exit cause, and
149
- * settle the run's result. */
150
- export function settleClose(state, code) {
151
- const { backend, runSpec, invocation, translator, onLine } = state;
147
+ /** Settle the run's `lastError` text in priority order: a failed blind image read names both
148
+ * the file and the model (plans/image-input.md "Failing the run"), then the abort, quiet-kill
149
+ * and timeout causes the watchdogs and signal path set, then the parser's own event-derived
150
+ * error, else the raw stderr tail. Undefined for a run that simply ended with a message. */
151
+ function settleErrorMessage(state, blindRead, failed, stderr, code) {
152
+ if (blindRead !== undefined) {
153
+ return `read ${blindRead}, but ${state.config.model ?? "the model"} cannot read images`;
154
+ }
155
+ if (state.aborted())
156
+ return "stopped by tumwater shutdown";
157
+ if (state.wd.quietKilled) {
158
+ return `killed as hung: no agent progress for over ${state.config.quietTimeoutSeconds}s`;
159
+ }
160
+ if (state.wd.timedOut) {
161
+ return state.wd.timedOutProgressing
162
+ ? `timed out after ${state.wd.timeoutBudgetMs / 1000}s while still making progress — session and worktree edits preserved for resume`
163
+ : `timed out after ${state.wd.timeoutBudgetMs / 1000}s`;
164
+ }
165
+ return (state.parser.errorMessage ??
166
+ (failed ? stderr.trim().slice(-500) || `the agent exited ${code}` : undefined));
167
+ }
168
+ /** Flush the tail the decoders and translator held back at stream end: the decoder's last
169
+ * bytes and unterminated final line, then a batching backend's unclosed final assistant
170
+ * message (which a killed run never closes with a later event). */
171
+ function flushRunTail(state) {
152
172
  // Flush any bytes the decoder held back at stream end and any unterminated final line so the
153
173
  // tail is not lost, then let the translator flush its own pending canonical line(s) — a
154
174
  // batching backend's unclosed last assistant message, which a killed run never closes with a
155
175
  // later event.
156
176
  state.feedCanonical(state.decoder.end(), true);
157
- for (const canonical of translator.finalize?.() ?? [])
158
- state.parser.feed(canonical + "\n", onLine);
159
- // A blind image read on an authoring run deliberately killed it, so its exit is a kill like
160
- // the watchdogs' — the half-done work is discarded.
161
- const killed = state.wasKilled();
177
+ for (const canonical of state.translator.finalize?.() ?? [])
178
+ state.parser.feed(canonical + "\n", state.onLine);
179
+ }
180
+ /** Reconcile the backend's post-run work (opencode's authoritative session export), only when
181
+ * the run is not already settled (a failed spawn settles in the 'error' handler before
182
+ * 'close') and was not killed — a killed run's session is half-done and no export should spend
183
+ * its 10 s on it. Its lines are written and parsed like streamed ones, so they reach the raw
184
+ * log and the parser's authoritative fold (run_cost/run_usage). */
185
+ function reconcileBackendAfterRun(state, killed) {
162
186
  // The backend's post-run reconciliation (opencode's authoritative session export), only when
163
187
  // the run is not already settled (a failed spawn settles in the 'error' handler before
164
188
  // 'close') and was not killed — a killed run's session is half-done and no export should
165
189
  // spend its 10 s on it. Its lines are written and parsed like streamed ones, so they reach
166
190
  // the raw log and the parser's authoritative fold (run_cost/run_usage).
167
191
  if (!state.settled() && !killed) {
168
- for (const canonical of backend.afterRun?.(runSpec, invocation, translator) ?? []) {
169
- state.parser.feed(canonical + "\n", onLine);
192
+ for (const canonical of state.backend.afterRun?.(state.runSpec, state.invocation, state.translator) ?? []) {
193
+ state.parser.feed(canonical + "\n", state.onLine);
170
194
  }
171
195
  }
172
- const failed = killed ||
196
+ }
197
+ /** Whether the exit reads as failed: killed, a parser error stop, or a non-zero code with no
198
+ * final text. */
199
+ function closeFailed(state, code, killed) {
200
+ return (killed ||
173
201
  state.parser.stopReason === "error" ||
174
- (code !== 0 && !state.parser.finalText.trim());
202
+ (code !== 0 && !state.parser.finalText.trim()));
203
+ }
204
+ /** Whether the exit is a permanent config error — a non-killed exit with no transient pi crash
205
+ * and no backend kind, whose parser flag or backend classification say config error. */
206
+ function closeConfigError(state, cls, killed) {
207
+ return (!killed &&
208
+ cls.transientPiCrash !== true &&
209
+ cls.backendKind === undefined &&
210
+ (state.parser.configError || cls.configError === true));
211
+ }
212
+ /** The run's `close` event: the process died and all its streams closed. Flush what the
213
+ * decoders held back, reconcile the backend's post-run work, classify the exit cause, and
214
+ * settle the run's result. */
215
+ export function settleClose(state, code) {
216
+ const { backend } = state;
217
+ flushRunTail(state);
218
+ // A blind image read on an authoring run deliberately killed it, so its exit is a kill like
219
+ // the watchdogs' — the half-done work is discarded.
220
+ const killed = state.wasKilled();
221
+ reconcileBackendAfterRun(state, killed);
222
+ const failed = closeFailed(state, code, killed);
175
223
  // The backend classifies the exit's stderr (pi: the transient JSON-parse crash, a
176
224
  // provider-wide backend failure, a permanent config error). The runner merges that with the
177
225
  // parser's own event-derived flags — one exit has one cause.
178
226
  const stderr = state.stderr();
179
227
  const cls = backend.classifyExit({ code, stderr, killed });
180
228
  const stderrBackend = cls.backendKind;
181
- const configError = !killed &&
182
- cls.transientPiCrash !== true &&
183
- stderrBackend === undefined &&
184
- (state.parser.configError || cls.configError === true);
185
- // The blind-read failure names both the file and the model (plans/image-input.md
186
- // "Failing the run"): take it over any generic exit text so lastError is actionable.
229
+ const configError = closeConfigError(state, cls, killed);
187
230
  const blindRead = state.blindRead();
188
- const errorMessage = blindRead !== undefined
189
- ? `read ${blindRead}, but ${state.config.model ?? "the model"} cannot read images`
190
- : state.aborted()
191
- ? "stopped by tumwater shutdown"
192
- : state.wd.quietKilled
193
- ? `killed as hung: no agent progress for over ${state.config.quietTimeoutSeconds}s`
194
- : state.wd.timedOut
195
- ? state.wd.timedOutProgressing
196
- ? `timed out after ${state.wd.timeoutBudgetMs / 1000}s while still making progress — session and worktree edits preserved for resume`
197
- : `timed out after ${state.wd.timeoutBudgetMs / 1000}s`
198
- : (state.parser.errorMessage ?? (failed ? stderr.trim().slice(-500) || `the agent exited ${code}` : undefined));
231
+ const errorMessage = settleErrorMessage(state, blindRead, failed, stderr, code);
199
232
  state.finish(resultFromParser(state, {
200
233
  ok: !failed,
201
234
  transientPiCrash: cls.transientPiCrash === true,
@@ -3,10 +3,10 @@
3
3
  * parser, the run marker, backend-bin resolution, the notes shim, the run spec, the backend's
4
4
  * optional `prepare`, the session-ledger record, the native-to-canonical translator, the two
5
5
  * incremental decoders, and the child spawn. It returns the `SpawnedRun` bundle the coordinator
6
- * (src/agent/run-agent.ts) binds its watchdogs and settlement wiring to, and passes to
7
- * createAgentStream (src/agent/run-stream.ts).
8
- * Part 1/2 of the runAgent split: the streaming pipeline (part 2/2) lives in run-stream.ts.
9
- * This module imports only run-agent's type surface, so
6
+ * (src/agent/agent-run.ts) binds its watchdogs and settlement wiring to, and passes to
7
+ * createAgentStream (src/agent/agent-stream.ts).
8
+ * Part 1/2 of the runAgent split: the streaming pipeline (part 2/2) lives in agent-stream.ts.
9
+ * This module imports only agent-run's type surface, so
10
10
  * there is no runtime import cycle. */
11
11
  import { spawn } from "node:child_process";
12
12
  import fs from "node:fs";
@@ -15,7 +15,7 @@ import { ensureParentDir, rotateIfLarge } from "../files/files.js";
15
15
  import { terminateChild } from "../process/process.js";
16
16
  import { makeRunMarker } from "../process/run-marker.js";
17
17
  import { PiStreamParser } from "../pi/pi-stream.js";
18
- import { recordSession } from "./session-ledger.js";
18
+ import { recordSession, sessionRecordFor } from "./session-ledger.js";
19
19
  import { errorMessage } from "../text/text.js";
20
20
  import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
21
21
  import { seamImageInput } from "./image-input.js";
@@ -44,15 +44,7 @@ function buildAndSpawn(backend, opts, pre) {
44
44
  // session of its own kind instead of whatever file is newest in the dir. The id is the backend's
45
45
  // own when it mints one ahead of the run (claude's UUID, or the resumed record's id); pi mints
46
46
  // none and keeps the session name.
47
- recordSession(opts.sessionDir, {
48
- agent: backend.id,
49
- id: preSpawnSessionId,
50
- kind: opts.kind,
51
- name: opts.sessionName,
52
- cwd: opts.cwd,
53
- ...(opts.config.model !== undefined ? { model: opts.config.model } : {}),
54
- at: Date.now(),
55
- });
47
+ recordSession(opts.sessionDir, sessionRecordFor(backend.id, preSpawnSessionId, opts));
56
48
  const translator = backend.translator();
57
49
  // Decode stdout incrementally instead of per chunk: a raw Buffer.toString("utf8") replaces any
58
50
  // multi-byte character whose bytes straddle two 'data' events with U+FFFD, corrupting that
@@ -108,6 +100,15 @@ function buildAndSpawn(backend, opts, pre) {
108
100
  blindRead: () => blind.read,
109
101
  };
110
102
  }
103
+ /** Warn on a run's own root that a backend's optional `prepare` step failed, then let the run
104
+ * go on without it — the shared tail of the synchronous-throw and async-rejection prepare guards
105
+ * below, so both warn the same way. `warnRoot` is undefined when the session dir has no team
106
+ * root; the warning is then skipped because there is no tumwater event log to write to. */
107
+ function warnPrepareFailed(warnRoot, error) {
108
+ if (warnRoot !== undefined) {
109
+ warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
110
+ }
111
+ }
111
112
  /** Build and spawn one non-interactive agent run, from the marker write through the child spawn,
112
113
  * and return the bundle the run's stream pipeline, watchdogs and settlement wiring consume. The
113
114
  * caller's setup (opened logs, the run's private TMPDIR) is passed in; a synchronous throw here
@@ -146,8 +147,7 @@ export function spawnPiRun(backend, logs, runTmp, opts) {
146
147
  }
147
148
  const parser = new PiStreamParser(opts.onToolCallStart, {
148
149
  // The seam's effective image input (src/agent/image-input.ts): a text-only seam makes every
149
- // image read blind even when pi adds no note — a claim its doctor probe proved false — which
150
- // the parser checks against its own note rule.
150
+ // image read blind even when pi adds no note — the parser checks against its own note rule.
151
151
  imageInput: seamImageInput(opts.config, readPiProviders(piModelsPath())),
152
152
  onBlindRead: (path) => {
153
153
  // Kill an authoring run at once, before the agent goes on working from a picture it never
@@ -160,8 +160,12 @@ export function spawnPiRun(backend, logs, runTmp, opts) {
160
160
  },
161
161
  });
162
162
  // The run's cross-group attribution mark, minted before the spawn so the environment can carry
163
- // it and the exit sweep can name it.
164
- const runMarker = makeRunMarker();
163
+ // it and the exit sweep can name it. When the session sits under a team root, the mark is
164
+ // stamped with that root's token so a dead-marked orphan can be attributed to this checkout
165
+ // and never reaped as another team's (BUGS.md, "Reap the doctor's orphans, part 1/3"); a
166
+ // session with no team root (a lab script, a smoke run, a prompt-lab replay) stays rootless.
167
+ const markerRoot = nearestTumwaterRoot(opts.sessionDir);
168
+ const runMarker = makeRunMarker(process.pid, markerRoot);
165
169
  const resolved = backend.resolveBin(opts.config);
166
170
  // Install the notes shim under the team root's `.tumwater/bin` so every backend's run finds
167
171
  // `tumwater-notes` first on PATH (notes-shim.ts). A failure to write it logs one warning and
@@ -211,9 +215,7 @@ export function spawnPiRun(backend, logs, runTmp, opts) {
211
215
  prepared = backend.prepare?.(runSpec);
212
216
  }
213
217
  catch (error) {
214
- if (warnRoot !== undefined) {
215
- warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
216
- }
218
+ warnPrepareFailed(warnRoot, error);
217
219
  }
218
220
  if (prepared === undefined) {
219
221
  // No `prepare` step: build and spawn synchronously and hand the bundle straight back, so
@@ -226,9 +228,7 @@ export function spawnPiRun(backend, logs, runTmp, opts) {
226
228
  await prepared;
227
229
  }
228
230
  catch (error) {
229
- if (warnRoot !== undefined) {
230
- warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
231
- }
231
+ warnPrepareFailed(warnRoot, error);
232
232
  }
233
233
  return buildAndSpawn(backend, opts, { runSpec, parser, runMarker, resolved, blind, runTmp });
234
234
  })();
@@ -3,13 +3,13 @@
3
3
  * canonical stdout line splitting with its over-long-line discard, the stderr window and its
4
4
  * fallback-clone warning matcher, the stream-minted session's second ledger record, and the
5
5
  * settle/flush that resolves the run's promise. It is the part of the runAgent split (part
6
- * 2/2) that the coordinator (src/agent/run-agent.ts) delegates to behind the `AgentStream`
7
- * interface: run-agent wires the child's `data` events to `feedStdout`/`feedStderr`, binds the
6
+ * 2/2) that the coordinator (src/agent/agent-run.ts) delegates to behind the `AgentStream`
7
+ * interface: agent-run wires the child's `data` events to `feedStdout`/`feedStderr`, binds the
8
8
  * settlement state's stream getters, and keeps the watchdogs, abort wiring and the
9
- * `error`/`exit`/`close` listeners itself. This module imports only run-agent's, run-spawn's
10
- * and run-settle's type surfaces, so there is no runtime import cycle. */
9
+ * `error`/`exit`/`close` listeners itself. This module imports only agent-run's, agent-spawn's
10
+ * and agent-settle's type surfaces, so there is no runtime import cycle. */
11
11
  import { removeTree } from "../files/files.js";
12
- import { recordSession } from "./session-ledger.js";
12
+ import { recordSession, sessionRecordFor } from "./session-ledger.js";
13
13
  /** How many trailing stderr characters are carried into the next chunk's warning match, so a
14
14
  * warning split across two `data` events is still seen. The warnings are short lines, so a few
15
15
  * hundred characters always cover a straddle. */
@@ -60,15 +60,7 @@ export function createAgentStream(input) {
60
60
  const sid = translator.sessionId?.();
61
61
  if (sid !== undefined && sid !== preSpawnSessionId && !streamMintedSession) {
62
62
  streamMintedSession = true;
63
- recordSession(opts.sessionDir, {
64
- agent: backend.id,
65
- id: sid,
66
- kind: opts.kind,
67
- name: opts.sessionName,
68
- cwd: opts.cwd,
69
- ...(opts.config.model !== undefined ? { model: opts.config.model } : {}),
70
- at: Date.now(),
71
- });
63
+ recordSession(opts.sessionDir, sessionRecordFor(backend.id, sid, opts));
72
64
  }
73
65
  };
74
66
  // Split native stdout into complete lines, translate each, and feed the canonical lines to
@@ -3,9 +3,9 @@
3
3
  * read (plans/opencode-backend.md "Groundwork: the claude-only branches become backend
4
4
  * traits"). Pure — imports nothing from the runtime registry (agent-backend.ts) — so those
5
5
  * layers can read a backend's traits without pulling the registry in. That is the same reason
6
- * `BACKEND_DEFAULT_MODELS` lives in src/config/model-selector.ts.
6
+ * `BACKEND_DEFAULT_MODELS` lives in src/config/config-model-selector.ts.
7
7
  *
8
- * An id with no entry reads as pi, because src/agent/run-agent.ts routes an unregistered id to
8
+ * An id with no entry reads as pi, because src/agent/agent-run.ts routes an unregistered id to
9
9
  * the pi backend.
10
10
  */
11
11
  import { HARNESS_CLAUDE_FLAGS } from "./claude/claude-args.js";
@@ -95,3 +95,17 @@ export function backendDefaultLabel(backend) {
95
95
  * for one cannot go missing from the other (opencode classifies the same failure from message
96
96
  * text, not a status code, so it is not in the set). */
97
97
  export const SERVER_TRANSIENT_STATUS = new Set([500, 502, 503, 504, 529]);
98
+ /** Classify a failure message's prose into the transient-backend failure it names, when the
99
+ * wire carries no structured signal: a dropped connection or a timeout, read from the message
100
+ * text by claude's and opencode's stream translators, which both fall back to prose for the
101
+ * statuses their native events leave to the message (claude's classifyResult and opencode's
102
+ * classifyError end with this same connection/then-timeout tail). The single home of the
103
+ * connection/timeout vocabulary, so the two backends' tail regexes cannot drift. undefined
104
+ * when the text names neither. */
105
+ export function transientProseKind(text) {
106
+ if (/connection|econn|refused|reset|terminated/i.test(text))
107
+ return { kind: "transientBackend", backendKind: "connection" };
108
+ if (/timed out|timeout/i.test(text))
109
+ return { kind: "transientBackend", backendKind: "timeout" };
110
+ return undefined;
111
+ }
@@ -17,7 +17,7 @@ import { spawnSync } from "node:child_process";
17
17
  import os from "node:os";
18
18
  import { resolvedBackendConfig } from "../../config/config-views.js";
19
19
  import { parseJsonObject } from "../../files/json-object.js";
20
- import { observeAccount, readAccount, subscriptionWindowRejected, utilizationPct, windowEntries, windowLabel, windowReserved, windowUsedUp, writeAccount, } from "../subscription-account.js";
20
+ import { observeAccount, readAccount, subscriptionWindowRejected, windowEntries, windowLabel, windowReserved, windowUsedUp, writeAccount, } from "../subscription-account.js";
21
21
  import { claudeBackend, claudeConfigDir } from "./claude-backend.js";
22
22
  import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
23
23
  /** Read the persisted claude account state, or null when it is absent or unreadable. A file
@@ -57,13 +57,6 @@ export { subscriptionWindowRejected, windowReserved };
57
57
  export function claudeWindowLabel(type) {
58
58
  return windowLabel(type);
59
59
  }
60
- /** The fraction of a subscription usage window consumed as a whole-number percent —
61
- * subscription-account.ts's utilizationPct, under its historical claude name. */
62
- export function claudeUtilizationPct(utilization) {
63
- return utilizationPct(utilization);
64
- }
65
- /** The account's windows as a list, most constrained first — subscription-account.ts's
66
- * windowEntries, under its historical claude name. */
67
60
  export function claudeWindowEntries(account) {
68
61
  return windowEntries(account);
69
62
  }
@@ -1,7 +1,7 @@
1
1
  /** The claude adapter for the agent seam (plans/agent-backends.md "The seam", "Sessions",
2
2
  * "The claude invocation"). It shapes one run's argv/stdin/env, translates claude's
3
3
  * `stream-json` into the canonical stream, 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; claude mints its own session id (a fresh UUID, or the resumed record's id), which
6
6
  * `invocation` returns so the ledger can carry it.
7
7
  *
@@ -11,7 +11,7 @@
11
11
  * missing transcript, a different cwd, or another kind's newer session all review fresh rather
12
12
  * than resuming the wrong conversation.
13
13
  *
14
- * Registration lives in src/agent/run-agent.ts beside the pi adapter, so a test can register a
14
+ * Registration lives in src/agent/agent-run.ts beside the pi adapter, so a test can register a
15
15
  * fake under its own id; a test selects claude by setting `config.agent = "claude"` on a run. */
16
16
  import fs from "node:fs";
17
17
  import os from "node:os";
@@ -19,7 +19,7 @@ import path from "node:path";
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { resolvedBackendConfig } from "../../config/config-views.js";
21
21
  import { MODEL_TIERS } from "../../config/config-schema.js";
22
- import { routeModel } from "../../config/model-selector.js";
22
+ import { routeModel } from "../../config/config-model-selector.js";
23
23
  import { backendRunEnv } from "../../process/run-marker.js";
24
24
  import { resolveBinForBackend } from "../../pi/pi-bin.js";
25
25
  import { expandHomeTilde } from "../../files/files.js";
@@ -17,6 +17,7 @@
17
17
  * the foreground. `BASH_MAX_TIMEOUT_MS` is derived from `quietTimeoutSeconds` so claude's own
18
18
  * Bash timeout fires before tumwater's quiet kill and the model sees a tool error rather than a
19
19
  * dead run. */
20
+ import { scrubEnv } from "../env-scrub.js";
20
21
  /** The cloud-provider switches that make a claude run bill as API rather than a claude.ai
21
22
  * subscription: Bedrock, Vertex, and Azure Foundry. They carry operator intent, so the run
22
23
  * environment keeps them (the allowlist below); claude-account's seedBilling and this module's
@@ -54,35 +55,30 @@ const MIN_BASH_MAX_TIMEOUT_MS = 1_000;
54
55
  /** Build the claude process environment from `base`. Never mutates `base`. */
55
56
  export function claudeEnv(base, opts) {
56
57
  const hadClaudeCode = base.CLAUDECODE !== undefined;
57
- const env = {};
58
- for (const [key, value] of Object.entries(base)) {
59
- if (key === "CLAUDECODE" || key === "CLAUDE_PID" || key === "CLAUDE_EFFORT")
60
- continue;
61
- if (key.startsWith("CLAUDE_CODE_") && !CLAUDE_CODE_ALLOWLIST.has(key))
62
- continue;
63
- if (key.startsWith("CLAUDE_AGENT_SDK_"))
64
- continue;
65
- // The host session's proxy, not the operator's: only when CLAUDECODE says we are inside one.
66
- if (key === "ANTHROPIC_BASE_URL" && hadClaudeCode)
67
- continue;
68
- env[key] = value;
69
- }
70
- env.CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
71
- env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
72
- env.BASH_DEFAULT_TIMEOUT_MS = String(BASH_DEFAULT_TIMEOUT_MS);
73
- if (opts.quietTimeoutSeconds > 0) {
74
- // 60 s under the quiet kill, so claude's tool error arrives before the run is declared hung;
75
- // floored so a quiet window under 60 s never derives a non-positive cap.
76
- const derived = opts.quietTimeoutSeconds * 1000 - 60_000;
77
- env.BASH_MAX_TIMEOUT_MS = String(Math.max(derived, MIN_BASH_MAX_TIMEOUT_MS));
78
- }
79
- env.DISABLE_AUTOUPDATER = "1";
80
- env.DISABLE_TELEMETRY = "1";
81
- env.DISABLE_ERROR_REPORTING = "1";
82
- // A team config dir (plans/notes-command.md "Keeping funes out of team runs"): force claude's
83
- // own config and transcripts under it, so a funes hook on ~/.claude/projects never indexes
84
- // team runs. Unset leaves any inherited CLAUDE_CONFIG_DIR (or claude's default) untouched.
85
- if (opts.configDir)
86
- env.CLAUDE_CONFIG_DIR = opts.configDir;
87
- return env;
58
+ return scrubEnv(base, {
59
+ keep: (key) => (key !== "CLAUDECODE" && key !== "CLAUDE_PID" && key !== "CLAUDE_EFFORT") &&
60
+ !(key.startsWith("CLAUDE_CODE_") && !CLAUDE_CODE_ALLOWLIST.has(key)) &&
61
+ !key.startsWith("CLAUDE_AGENT_SDK_") &&
62
+ // The host session's proxy, not the operator's: only when CLAUDECODE says we are inside one.
63
+ !(key === "ANTHROPIC_BASE_URL" && hadClaudeCode),
64
+ then: (env) => {
65
+ env.CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
66
+ env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
67
+ env.BASH_DEFAULT_TIMEOUT_MS = String(BASH_DEFAULT_TIMEOUT_MS);
68
+ if (opts.quietTimeoutSeconds > 0) {
69
+ // 60 s under the quiet kill, so claude's tool error arrives before the run is
70
+ // declared hung; floored so a short quiet window never derives a non-positive cap.
71
+ const derived = opts.quietTimeoutSeconds * 1000 - 60_000;
72
+ env.BASH_MAX_TIMEOUT_MS = String(Math.max(derived, MIN_BASH_MAX_TIMEOUT_MS));
73
+ }
74
+ env.DISABLE_AUTOUPDATER = "1";
75
+ env.DISABLE_TELEMETRY = "1";
76
+ env.DISABLE_ERROR_REPORTING = "1";
77
+ // A team config dir (plans/notes-command.md "Keeping funes out of team runs"): force claude's
78
+ // own config and transcripts under it, so a funes hook on ~/.claude/projects never indexes
79
+ // team runs. Unset leaves any inherited CLAUDE_CONFIG_DIR (or claude's default) untouched.
80
+ if (opts.configDir)
81
+ env.CLAUDE_CONFIG_DIR = opts.configDir;
82
+ },
83
+ });
88
84
  }