tumwater 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
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.0**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
45
+ **v0.4.2**: 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
 
@@ -75,11 +75,9 @@ and `tumwater pause`, `resume`, `wake`, and `stop` control the roles. `tumwater
75
75
  so pair it with `--token <secret>`.
76
76
 
77
77
  `tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
78
- model cannot read images, the command refuses the attachment up front (naming the fix) unless
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
82
- run reroutes.
78
+ model cannot read images, the command refuses the attachment up front (naming the fix); the
79
+ dashboards and `tumwater role <id>` say which roles read images. Nothing reroutes a blind read
80
+ to another tier — a run that reads an image its model cannot see fails with `lastError`.
83
81
 
84
82
  Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
85
83
  same list, and `tumwater completion bash|zsh|fish` prints shell completion. Settings live in
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "4d2bccd84735720b529ab2695a5d274df2e48bbb",
3
- "builtAt": 1792915702889,
2
+ "sha": "115515a5f5134c37c2bf8fa9dfb0edf620dd94b6",
3
+ "builtAt": 1793081614993,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -1,7 +1,10 @@
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-agent.ts
4
- * owns everything backend-neutral (spawn, marker, watchdogs, abort, stderr capture); the
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
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
5
8
  * post-run settlement — the group-kill and marker sweeps on exit, the spawn-error result and
6
9
  * the close-time reconcile/classify that builds the `PiRunResult` — lives in
7
10
  * src/agent/run-settle.ts. A backend registered here supplies the rest. src/agent/run-agent.ts
@@ -88,3 +88,24 @@ export function backendDefaultLabel(backend) {
88
88
  return `${backend} default`;
89
89
  return "pi default";
90
90
  }
91
+ /** The HTTP status codes the native claude and codex stream translators read as a transient
92
+ * backend (server) failure — the `transientBackend` classification with `backendKind:
93
+ * "server"`. 529 is Anthropic's "overloaded" spelling, which codex's `turn.failed` error
94
+ * carries too. One home for the set the two adapters' classifiers share, so a status added
95
+ * for one cannot go missing from the other (opencode classifies the same failure from message
96
+ * text, not a status code, so it is not in the set). */
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
+ }
@@ -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-agent.ts owns the spawn and records this run in the session
4
+ * backend-specific. src/agent/run-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,12 +11,13 @@
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-agent.ts forwards each as an
14
+ * on the seam's optional `warnings()` — src/agent/run-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";
18
- import { secondsUntil } from "../../text/datetime.js";
19
- import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
18
+ import { secondsUntilUnixStamp } from "../../text/datetime.js";
19
+ import { canonicalUsageBlock, isJsonObject, nonNegativeNumber, objectOrEmpty, optionalString, parseJsonObject } from "../../files/json-object.js";
20
+ import { SERVER_TRANSIENT_STATUS, transientProseKind } from "../backend-traits.js";
20
21
  /** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
21
22
  * lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
22
23
  * lower-cased so a future tool still reads as a name rather than as `undefined`. */
@@ -29,14 +30,8 @@ const TOOL_NAMES = {
29
30
  Glob: "glob",
30
31
  Grep: "grep",
31
32
  };
32
- /** claude's HTTP status classes that mean a transient backend failure. 529 is Anthropic's
33
- * "overloaded" spelling. */
34
- const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
35
33
  /** claude's HTTP status classes that mean a permanent configuration error. */
36
34
  const CONFIG_STATUS = new Set([400, 401, 403, 404]);
37
- function str(value) {
38
- return typeof value === "string" ? value : undefined;
39
- }
40
35
  /** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
41
36
  * authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
42
37
  * on every event, so summing per event would double-count — the fold takes it once). The
@@ -51,15 +46,15 @@ function canonicalUsage(usage) {
51
46
  function canonicalBlock(block) {
52
47
  switch (block.type) {
53
48
  case "text":
54
- return { type: "text", text: str(block.text) ?? "" };
49
+ return { type: "text", text: optionalString(block.text) ?? "" };
55
50
  case "thinking":
56
- return { type: "thinking", thinking: str(block.thinking) ?? str(block.text) ?? "" };
51
+ return { type: "thinking", thinking: optionalString(block.thinking) ?? optionalString(block.text) ?? "" };
57
52
  case "tool_use": {
58
53
  const input = objectOrEmpty(block.input);
59
54
  const args = { ...input };
60
55
  if (typeof input.file_path === "string")
61
56
  args.path = input.file_path;
62
- const name = str(block.name) ?? "";
57
+ const name = optionalString(block.name) ?? "";
63
58
  // Own-property test, not a bare index: a tool name like `constructor`, `toString`, or
64
59
  // `__proto__` would otherwise inherit a function (or Object.prototype) from the table's
65
60
  // prototype chain and bypass the lower-cased fallback — JSON.stringify then drops the
@@ -67,7 +62,7 @@ function canonicalBlock(block) {
67
62
  // applies to EFFORT_BY_THINKING.
68
63
  return {
69
64
  type: "toolCall",
70
- id: str(block.id) ?? "",
65
+ id: optionalString(block.id) ?? "",
71
66
  name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
72
67
  arguments: args,
73
68
  };
@@ -83,18 +78,18 @@ function canonicalBlock(block) {
83
78
  * provider classification applies. `status` is `api_error_status`, which is null on the
84
79
  * not-logged-in result, so the text checks carry that case. */
85
80
  function classifyResult(result) {
86
- const terminal = str(result.terminal_reason);
81
+ const terminal = optionalString(result.terminal_reason);
87
82
  if (terminal === "aborted_tools")
88
83
  return {};
89
84
  if (terminal === "error_max_budget_usd")
90
85
  return { kind: "budgetCapped" };
91
- const text = joinParts([str(result.result), str(result.error)], " ");
86
+ const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
92
87
  if (/error_max_budget_usd/i.test(text))
93
88
  return { kind: "budgetCapped" };
94
89
  const status = nonNegativeNumber(result.api_error_status, undefined);
95
90
  if (status === 429)
96
91
  return { kind: "transientRateLimit" };
97
- if (status !== undefined && SERVER_STATUS.has(status))
92
+ if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status))
98
93
  return { kind: "transientBackend", backendKind: "server" };
99
94
  // The context-overflow text wins over a configuration status: claude reports an oversized
100
95
  // prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError.
@@ -111,10 +106,9 @@ function classifyResult(result) {
111
106
  return { kind: "transientRateLimit" };
112
107
  if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
113
108
  return { kind: "transientBackend", backendKind: "server" };
114
- if (/connection|econn|refused|reset|terminated/i.test(text))
115
- return { kind: "transientBackend", backendKind: "connection" };
116
- if (/timed out|timeout/i.test(text))
117
- return { kind: "transientBackend", backendKind: "timeout" };
109
+ const prose = transientProseKind(text);
110
+ if (prose)
111
+ return prose;
118
112
  if (/\b(400|401|403|404)\b/i.test(text))
119
113
  return { kind: "configError" };
120
114
  return {};
@@ -170,174 +164,179 @@ export function claudeTranslator(opts = {}) {
170
164
  seen.add(kind);
171
165
  warnings.add(kind);
172
166
  };
167
+ const handleSystem = (event) => {
168
+ const subtype = optionalString(event.subtype);
169
+ if (subtype === "init") {
170
+ const out = [];
171
+ const cwd = optionalString(event.cwd);
172
+ const id = optionalString(event.session_id);
173
+ out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
174
+ out.push(JSON.stringify({ type: "agent_start" }));
175
+ const billing = billingOf(optionalString(event.apiKeySource), opts.thirdPartyProvider === true);
176
+ if (billing)
177
+ out.push(JSON.stringify({ type: "billing", billing }));
178
+ return out;
179
+ }
180
+ if (subtype === "api_retry") {
181
+ const attempt = nonNegativeNumber(event.attempt, undefined);
182
+ const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
183
+ const status = nonNegativeNumber(event.error_status, undefined);
184
+ const error = optionalString(event.error) ?? "";
185
+ const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
186
+ return [
187
+ JSON.stringify({
188
+ type: "auto_retry_start",
189
+ ...(attempt !== undefined ? { attempt } : {}),
190
+ ...(maxAttempts !== undefined ? { maxAttempts } : {}),
191
+ errorMessage: errorMessage || "api retry",
192
+ }),
193
+ ];
194
+ }
195
+ if (subtype === "compact_boundary")
196
+ return [JSON.stringify({ type: "compaction_start" })];
197
+ // A thinking-token estimate during extended thinking: known, carries nothing canonical.
198
+ if (subtype === "thinking_tokens")
199
+ return [];
200
+ record(`system:${subtype ?? "unknown"}`);
201
+ return [];
202
+ };
203
+ const handleAssistant = (event) => {
204
+ sawActivity = true;
205
+ const message = objectOrEmpty(event.message);
206
+ const id = optionalString(message.id) ?? `anon-${anonymous++}`;
207
+ // A new id flushes the open message; a continuing one flushes nothing. Either way the
208
+ // fresh or open pending then folds this event's content, usage, and stop reason the
209
+ // same way — that shared fold is why both branches live on one path.
210
+ let out = [];
211
+ if (pending?.id !== id) {
212
+ out = flush();
213
+ pending = { id, content: [], usage: canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined) };
214
+ }
215
+ const blocks = Array.isArray(message.content) ? message.content : [];
216
+ for (const raw of blocks) {
217
+ if (!isJsonObject(raw))
218
+ continue;
219
+ const block = canonicalBlock(raw);
220
+ if (!block)
221
+ continue;
222
+ pending.content.push(block);
223
+ if (block.type === "toolCall")
224
+ out.push(startToolCall(block));
225
+ }
226
+ // Usage repeats on every event of the same message; the last read wins and is counted
227
+ // once because the fold emits one message_end per id.
228
+ pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
229
+ const stopReason = optionalString(message.stop_reason);
230
+ if (stopReason !== undefined)
231
+ pending.stopReason = stopReason;
232
+ return out;
233
+ };
234
+ const handleUser = (event) => {
235
+ const out = flush();
236
+ const message = objectOrEmpty(event.message);
237
+ const blocks = Array.isArray(message.content) ? message.content : [];
238
+ for (const raw of blocks) {
239
+ if (!isJsonObject(raw) || raw.type !== "tool_result")
240
+ continue;
241
+ out.push(JSON.stringify({
242
+ type: "tool_execution_end",
243
+ toolCallId: optionalString(raw.tool_use_id) ?? "",
244
+ isError: raw.is_error === true,
245
+ }));
246
+ }
247
+ return out;
248
+ };
249
+ const handleRateLimitEvent = (event) => {
250
+ const info = objectOrEmpty(event.rate_limit_info);
251
+ const rateLimitType = optionalString(info.rateLimitType);
252
+ 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];
290
+ };
291
+ const handleResult = (event) => {
292
+ sawActivity = true;
293
+ const out = flush();
294
+ // The assistant events' usage is the message-start snapshot, so the result's total
295
+ // is the run's real output-token count (thinking included).
296
+ const outputTokens = nonNegativeNumber(objectOrEmpty(event.usage).output_tokens, undefined);
297
+ if (outputTokens !== undefined)
298
+ out.push(JSON.stringify({ type: "run_usage", outputTokens }));
299
+ const total = nonNegativeNumber(event.total_cost_usd, undefined);
300
+ if (total !== undefined) {
301
+ sawRunCost = true;
302
+ out.push(JSON.stringify({ type: "run_cost", total }));
303
+ }
304
+ else {
305
+ warnings.add("cost_unknown");
306
+ }
307
+ if (event.is_error === true) {
308
+ const { kind, backendKind } = classifyResult(event);
309
+ const message = {
310
+ role: "assistant",
311
+ content: [],
312
+ stopReason: "error",
313
+ };
314
+ const errorMessage = optionalString(event.result) ?? optionalString(event.error);
315
+ if (errorMessage !== undefined)
316
+ message.errorMessage = errorMessage;
317
+ // tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
318
+ // reads event.tumwaterKind and trusts it before its text regexes.
319
+ out.push(JSON.stringify({
320
+ type: "message_end",
321
+ message,
322
+ ...(kind !== undefined ? { tumwaterKind: kind } : {}),
323
+ ...(backendKind !== undefined ? { backendKind } : {}),
324
+ }));
325
+ }
326
+ return out;
327
+ };
173
328
  const translate = (line) => {
174
329
  const parsed = parseJsonObject(line);
175
330
  if (!parsed)
176
331
  return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
177
332
  const event = parsed;
178
- const type = str(event.type);
333
+ const type = optionalString(event.type);
179
334
  switch (type) {
180
- case "system": {
181
- const subtype = str(event.subtype);
182
- if (subtype === "init") {
183
- const out = [];
184
- const cwd = str(event.cwd);
185
- const id = str(event.session_id);
186
- out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
187
- out.push(JSON.stringify({ type: "agent_start" }));
188
- const billing = billingOf(str(event.apiKeySource), opts.thirdPartyProvider === true);
189
- if (billing)
190
- out.push(JSON.stringify({ type: "billing", billing }));
191
- return out;
192
- }
193
- if (subtype === "api_retry") {
194
- const attempt = nonNegativeNumber(event.attempt, undefined);
195
- const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
196
- const status = nonNegativeNumber(event.error_status, undefined);
197
- const error = str(event.error) ?? "";
198
- const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
199
- return [
200
- JSON.stringify({
201
- type: "auto_retry_start",
202
- ...(attempt !== undefined ? { attempt } : {}),
203
- ...(maxAttempts !== undefined ? { maxAttempts } : {}),
204
- errorMessage: errorMessage || "api retry",
205
- }),
206
- ];
207
- }
208
- if (subtype === "compact_boundary")
209
- return [JSON.stringify({ type: "compaction_start" })];
210
- // A thinking-token estimate during extended thinking: known, carries nothing canonical.
211
- if (subtype === "thinking_tokens")
212
- return [];
213
- record(`system:${subtype ?? "unknown"}`);
214
- return [];
215
- }
216
- case "assistant": {
217
- sawActivity = true;
218
- const message = objectOrEmpty(event.message);
219
- const id = str(message.id) ?? `anon-${anonymous++}`;
220
- // A new id flushes the open message; a continuing one flushes nothing. Either way the
221
- // fresh or open pending then folds this event's content, usage, and stop reason the
222
- // same way — that shared fold is why both branches live on one path.
223
- let out = [];
224
- if (pending?.id !== id) {
225
- out = flush();
226
- pending = { id, content: [], usage: canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined) };
227
- }
228
- const blocks = Array.isArray(message.content) ? message.content : [];
229
- for (const raw of blocks) {
230
- if (!isJsonObject(raw))
231
- continue;
232
- const block = canonicalBlock(raw);
233
- if (!block)
234
- continue;
235
- pending.content.push(block);
236
- if (block.type === "toolCall")
237
- out.push(startToolCall(block));
238
- }
239
- // Usage repeats on every event of the same message; the last read wins and is counted
240
- // once because the fold emits one message_end per id.
241
- pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
242
- const stopReason = str(message.stop_reason);
243
- if (stopReason !== undefined)
244
- pending.stopReason = stopReason;
245
- return out;
246
- }
247
- case "user": {
248
- const out = flush();
249
- const message = objectOrEmpty(event.message);
250
- const blocks = Array.isArray(message.content) ? message.content : [];
251
- for (const raw of blocks) {
252
- if (!isJsonObject(raw) || raw.type !== "tool_result")
253
- continue;
254
- out.push(JSON.stringify({
255
- type: "tool_execution_end",
256
- toolCallId: str(raw.tool_use_id) ?? "",
257
- isError: raw.is_error === true,
258
- }));
259
- }
260
- return out;
261
- }
262
- case "rate_limit_event": {
263
- const info = objectOrEmpty(event.rate_limit_info);
264
- const rateLimitType = str(info.rateLimitType);
265
- const status = str(info.status);
266
- // claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
267
- // keyed by window type, and only the binding window's status at the top level.
268
- const windows = [];
269
- for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
270
- if (type === rateLimitType || !isJsonObject(raw))
271
- continue;
272
- const reading = { type: "rate_limit", rateLimitType: type };
273
- const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
274
- if (windowResets !== undefined)
275
- reading.resetsAt = windowResets;
276
- const windowUtil = nonNegativeNumber(raw.utilization, undefined);
277
- if (windowUtil !== undefined)
278
- reading.utilization = windowUtil;
279
- windows.push(JSON.stringify(reading));
280
- }
281
- const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
282
- const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
283
- const out = { type: "rate_limit" };
284
- if (rateLimitType !== undefined)
285
- out.rateLimitType = rateLimitType;
286
- if (status !== undefined)
287
- out.status = status;
288
- if (resetsAt !== undefined)
289
- out.resetsAt = resetsAt;
290
- const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
291
- if (utilization !== undefined)
292
- out.utilization = utilization;
293
- const overageStatus = str(info.overageStatus);
294
- if (overageStatus !== undefined)
295
- out.overageStatus = overageStatus;
296
- if (info.isUsingOverage === true)
297
- out.isUsingOverage = true;
298
- if (status === "rejected" && resetsAt !== undefined) {
299
- // resetsAt is a unix-seconds stamp; secondsUntil folds it (×1000) to whole seconds.
300
- out.retryAfterSeconds = secondsUntil(resetsAt * 1000, Date.now());
301
- }
302
- return [JSON.stringify(out), ...windows];
303
- }
304
- case "result": {
305
- sawActivity = true;
306
- const out = flush();
307
- // The assistant events' usage is the message-start snapshot, so the result's total
308
- // is the run's real output-token count (thinking included).
309
- const outputTokens = nonNegativeNumber(objectOrEmpty(event.usage).output_tokens, undefined);
310
- if (outputTokens !== undefined)
311
- out.push(JSON.stringify({ type: "run_usage", outputTokens }));
312
- const total = nonNegativeNumber(event.total_cost_usd, undefined);
313
- if (total !== undefined) {
314
- sawRunCost = true;
315
- out.push(JSON.stringify({ type: "run_cost", total }));
316
- }
317
- else {
318
- warnings.add("cost_unknown");
319
- }
320
- if (event.is_error === true) {
321
- const { kind, backendKind } = classifyResult(event);
322
- const message = {
323
- role: "assistant",
324
- content: [],
325
- stopReason: "error",
326
- };
327
- const errorMessage = str(event.result) ?? str(event.error);
328
- if (errorMessage !== undefined)
329
- message.errorMessage = errorMessage;
330
- // tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
331
- // reads event.tumwaterKind and trusts it before its text regexes.
332
- out.push(JSON.stringify({
333
- type: "message_end",
334
- message,
335
- ...(kind !== undefined ? { tumwaterKind: kind } : {}),
336
- ...(backendKind !== undefined ? { backendKind } : {}),
337
- }));
338
- }
339
- return out;
340
- }
335
+ case "system": return handleSystem(event);
336
+ case "assistant": return handleAssistant(event);
337
+ case "user": return handleUser(event);
338
+ case "rate_limit_event": return handleRateLimitEvent(event);
339
+ case "result": return handleResult(event);
341
340
  case "tool_progress":
342
341
  // A 30 s heartbeat of a running tool call: known, and the open call already tracks
343
342
  // liveness.
@@ -1,5 +1,5 @@
1
1
  /** The PreToolUse hook claude runs before each file write in a team run (claude-write-guard.ts
2
- * installs it through `--settings`): `node write-guard-hook.js <worktree>` with claude's hook
2
+ * installs it through `--settings`): `node claude-write-guard-hook.js <worktree>` with the hook
3
3
  * payload on stdin. To block the write it prints a `deny` decision with the refusal as its reason:
4
4
  * the model reads only the reason, where a refusal by exit 2 would also quote the whole hook
5
5
  * command. It prints nothing to let the write run. Any failure of its own lets the write run, so a
@@ -7,7 +7,7 @@
7
7
  * `--setting-sources project` leaves the operator's own hooks out of a run, while `--settings`
8
8
  * still loads this one. The hook gets the run's worktree as an argument rather than reading
9
9
  * claude's `cwd`, which follows the Bash tool's `cd` into subdirectories. The entry point is
10
- * write-guard-hook.ts beside this file. */
10
+ * claude-write-guard-hook.ts beside this file. */
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { shellQuote, strayWriteRefusal } from "../write-guard.js";
13
13
  /** claude's file-writing tools a team run has: claude-args.ts's `--tools` allows only these two,
@@ -16,12 +16,13 @@ const WRITE_TOOLS = ["Edit", "Write"];
16
16
  /** Seconds claude waits for the hook before running the tool anyway. */
17
17
  const HOOK_TIMEOUT_SECONDS = 30;
18
18
  /** The `--settings` value for a run in `cwd`: one PreToolUse hook on the file-writing tools that
19
- * runs the compiled write-guard-hook.js with this process's node, falling back to `node` on PATH
20
- * when that binary has moved (as the notes shim does), and the run's worktree as its argument. The
19
+ * runs the compiled claude-write-guard-hook.js with this process's node, falling back to
20
+ * `node` on PATH when that binary has moved (as the notes shim does), and the run's worktree
21
+ * as its argument. The
21
22
  * hook path resolves from this module, so a staged build (.tumwater/build/<sha>) runs its own
22
23
  * copy. */
23
24
  export function writeGuardSettings(cwd, nodeBin = process.execPath) {
24
- const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
25
+ const hook = fileURLToPath(new URL("./claude-write-guard-hook.js", import.meta.url));
25
26
  const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)} ${shellQuote(cwd)}`;
26
27
  return JSON.stringify({
27
28
  hooks: {
@@ -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-agent.ts owns the spawn and records this run in the session
4
+ * backend-specific. src/agent/run-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.