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
@@ -0,0 +1,226 @@
1
+ /** The build-and-spawn stage of the agent runner (plans/agent-backends.md "The seam"): the
2
+ * part of a run before the stream pipeline — the `tumwater_run` raw-log marker, the stream
3
+ * parser, the run marker, backend-bin resolution, the notes shim, the run spec, the backend's
4
+ * optional `prepare`, the session-ledger record, the native-to-canonical translator, the two
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
10
+ * there is no runtime import cycle. */
11
+ import { spawn } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import { StringDecoder } from "node:string_decoder";
14
+ import { ensureParentDir, rotateIfLarge } from "../files/files.js";
15
+ import { terminateChild } from "../process/process.js";
16
+ import { makeRunMarker } from "../process/run-marker.js";
17
+ import { PiStreamParser } from "../pi/pi-stream.js";
18
+ import { recordSession, sessionRecordFor } from "./session-ledger.js";
19
+ import { errorMessage } from "../text/text.js";
20
+ import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
21
+ import { seamImageInput } from "./image-input.js";
22
+ import { ensureNotesShim } from "./notes-shim.js";
23
+ import { nearestTumwaterRoot } from "../paths.js";
24
+ import { piModelsPath, readPiProviders } from "../pi/pi-models.js";
25
+ export function openAppendLog(file, maxBytes) {
26
+ ensureParentDir(file);
27
+ rotateIfLarge(file, maxBytes);
28
+ const log = { stream: fs.createWriteStream(file, { flags: "a" }), broken: false };
29
+ log.stream.on("error", () => {
30
+ log.broken = true;
31
+ });
32
+ return log;
33
+ }
34
+ /** Invoke the backend and spawn the child from the pre-spawn pieces, returning the bundle. */
35
+ function buildAndSpawn(backend, opts, pre) {
36
+ const { runSpec, parser, runMarker, resolved, blind, runTmp } = pre;
37
+ const invocation = backend.invocation(runSpec);
38
+ // The id the pre-spawn ledger record carries: a backend that mints one ahead of the run
39
+ // (claude, a codex resume), else the session name (pi, a fresh codex run — its id is born in
40
+ // the stream). The stream-mint pass later compares against it so a codex run appends its
41
+ // streamed thread id exactly once, and only when it differs from this.
42
+ const preSpawnSessionId = invocation.sessionId ?? opts.sessionName;
43
+ // Record the run in the session ledger before its spawn, so a later resume can pick the newest
44
+ // session of its own kind instead of whatever file is newest in the dir. The id is the backend's
45
+ // own when it mints one ahead of the run (claude's UUID, or the resumed record's id); pi mints
46
+ // none and keeps the session name.
47
+ recordSession(opts.sessionDir, sessionRecordFor(backend.id, preSpawnSessionId, opts));
48
+ const translator = backend.translator();
49
+ // Decode stdout incrementally instead of per chunk: a raw Buffer.toString("utf8") replaces any
50
+ // multi-byte character whose bytes straddle two 'data' events with U+FFFD, corrupting that
51
+ // line's text (commit subjects, summaries, transcripts). StringDecoder holds back the incomplete
52
+ // trailing bytes until the next chunk completes them.
53
+ const decoder = new StringDecoder("utf8");
54
+ // stderr gets its own incremental decoder for the same reason: the failure message and the
55
+ // stderr-keyed matchers (crash, stream-severed, config) read this buffer, so a character whose
56
+ // bytes straddle two 'data' events must not become U+FFFD.
57
+ const stderrDecoder = new StringDecoder("utf8");
58
+ const child = spawn(resolved.bin, invocation.args, {
59
+ cwd: opts.cwd,
60
+ // A backend that feeds the prompt on stdin (every registered one) gets a pipe the runner
61
+ // writes and ends below; one that returns no stdin keeps stdio[0] "ignore".
62
+ stdio: [invocation.stdin !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
63
+ // pi sets its process title at startup, and so does every npm its tool calls run: on macOS
64
+ // each would register with LaunchServices and leak a launchservicesd port. Detached so pi
65
+ // leads its own process group: terminateChild signals the group, so a run's tool-call
66
+ // grandchildren — killed or finished — die with it instead of leaking to launchd.
67
+ detached: true,
68
+ // The run's mark, inherited by every tool call and its descendants: pi 0.87.1's bash tool
69
+ // spawns each command detached in its OWN group, so the group sweep below cannot reach what
70
+ // a tool call backgrounds (BUGS.md 2026-09-30) — the mark is the only attribution that
71
+ // follows a reparented orphan. Appended to any inherited mark so a nested harness under test
72
+ // keeps the outer run's.
73
+ env: { ...invocation.env, TMPDIR: runTmp },
74
+ });
75
+ if (invocation.stdin !== undefined) {
76
+ // A backend that dies before draining its stdin — an immediately-failing startup such as an
77
+ // unknown flag, an unresumable session, or a not-logged-in exit — leaves this write nothing
78
+ // to feed. Once the prompt exceeds the OS pipe buffer the write then fails EPIPE, and an
79
+ // unhandled 'error' event on the stdin stream is thrown from the event loop and takes the
80
+ // whole process (the whole team) down. The child's own exit status and stderr already report
81
+ // why the run died, so swallowing the failed write adds no silence the caller needs to break;
82
+ // git.ts's patchId carries the same guard for a patch-id that dies before reading its input.
83
+ child.stdin?.on("error", () => { });
84
+ child.stdin?.end(invocation.stdin);
85
+ }
86
+ // Armed now that the child exists, so the parser's parse-time onBlindRead (fires during feed,
87
+ // always after spawn) can reach it.
88
+ blind.terminate = () => terminateChild(child);
89
+ return {
90
+ child,
91
+ resolved,
92
+ runSpec,
93
+ invocation,
94
+ parser,
95
+ translator,
96
+ decoder,
97
+ stderrDecoder,
98
+ runMarker,
99
+ preSpawnSessionId,
100
+ blindRead: () => blind.read,
101
+ };
102
+ }
103
+ /** Build and spawn one non-interactive agent run, from the marker write through the child spawn,
104
+ * and return the bundle the run's stream pipeline, watchdogs and settlement wiring consume. The
105
+ * caller's setup (opened logs, the run's private TMPDIR) is passed in; a synchronous throw here
106
+ * propagates to the caller, which rejects the run's promise. The backend's optional `prepare` is
107
+ * awaited here and warned about (never thrown away) exactly as the coordinator once did.
108
+ *
109
+ * The return type is a union on purpose: a backend without a `prepare` step (pi, claude,
110
+ * opencode) builds and spawns synchronously and returns the bundle DIRECTLY, so the coordinator
111
+ * does not `await` it and its watchdogs (a setInterval registration) start in the same
112
+ * synchronous step — the ordering the logical-clock watchdog tests pin, which advance the fake
113
+ * clock immediately after the run promise is created. A backend WITH a `prepare` (codex)
114
+ * resolves its trust first and returns a promise that resolves to the bundle after the await. */
115
+ export function spawnPiRun(backend, logs, runTmp, opts) {
116
+ const rawLog = logs.raw;
117
+ // The first blind image read an authoring run made (plans/image-input.md "Detecting a blind
118
+ // read"): the path, plus an indirection to the child so the parser's parse-time callback
119
+ // (which fires during feed, after spawn) can kill it. Unset for a gate run, which only records
120
+ // the read. Declared before the parser so its onBlindRead closure (which assigns them at parse
121
+ // time, after spawn) can reference them.
122
+ const blind = {};
123
+ // The marker precedes this run's first event in file order — written to the same stream
124
+ // stdout lines flow through, before spawn, so ordering is exact by construction; on a failed
125
+ // spawn finish() still ends the stream and flushes it. Exactly one per run: the kind, agent
126
+ // and start time are always there, the label only when the caller set one (`history
127
+ // <role> <n> --transcript` selects a run's segments by this ts).
128
+ {
129
+ const marker = {
130
+ type: "tumwater_run",
131
+ kind: opts.kind,
132
+ agent: backend.id,
133
+ ts: Date.now(),
134
+ };
135
+ if (opts.label)
136
+ marker.label = opts.label;
137
+ rawLog.stream.write(JSON.stringify(marker) + "\n");
138
+ }
139
+ const parser = new PiStreamParser(opts.onToolCallStart, {
140
+ // The seam's effective image input (src/agent/image-input.ts): a text-only seam makes every
141
+ // image read blind even when pi adds no note — the parser checks against its own note rule.
142
+ imageInput: seamImageInput(opts.config, readPiProviders(piModelsPath())),
143
+ onBlindRead: (path) => {
144
+ // Kill an authoring run at once, before the agent goes on working from a picture it never
145
+ // saw (terminateChild is what the watchdogs use). Gate runs (reviewer, resolver) only
146
+ // record the read and keep going.
147
+ if (opts.kind === "author") {
148
+ blind.read = path;
149
+ blind.terminate?.();
150
+ }
151
+ },
152
+ });
153
+ // The run's cross-group attribution mark, minted before the spawn so the environment can carry
154
+ // it and the exit sweep can name it.
155
+ const runMarker = makeRunMarker();
156
+ const resolved = backend.resolveBin(opts.config);
157
+ // Install the notes shim under the team root's `.tumwater/bin` so every backend's run finds
158
+ // `tumwater-notes` first on PATH (notes-shim.ts). A failure to write it logs one warning and
159
+ // the run goes on without it — notes would then be unwritable by the command, but the run
160
+ // itself is not blocked.
161
+ let binDir;
162
+ try {
163
+ // Install the shim only under a real team root — the nearest ancestor holding a `.tumwater`.
164
+ // A session dir with no such ancestor (a lab script, a smoke run, a prompt-lab replay) must
165
+ // never mint one: nearestTumwaterRoot is undefined then, and the run goes on without the
166
+ // shim, exactly as if the install had failed — writing `<fallback>/.tumwater` from a run
167
+ // created a phantom harness root in a shared directory and broke every other suite on the
168
+ // host (BUGS.md 2026-10-18).
169
+ const root = nearestTumwaterRoot(opts.sessionDir);
170
+ if (root !== undefined)
171
+ binDir = ensureNotesShim(root);
172
+ }
173
+ catch {
174
+ opts.onAgentWarning?.("could not install the tumwater-notes shim; this run will not find tumwater-notes on PATH");
175
+ }
176
+ const runSpec = {
177
+ cwd: opts.cwd,
178
+ prompt: opts.prompt,
179
+ config: opts.config,
180
+ sessionDir: opts.sessionDir,
181
+ sessionName: opts.sessionName,
182
+ continueSession: opts.continueSession,
183
+ kind: opts.kind,
184
+ agentBin: resolved.bin,
185
+ runMarker,
186
+ notesPath: opts.notesPath,
187
+ ...(binDir !== undefined ? { binDir } : {}),
188
+ ...(opts.maxBudgetUsd !== undefined ? { maxBudgetUsd: opts.maxBudgetUsd } : {}),
189
+ };
190
+ // A backend's asynchronous pre-run work (the codex write guard resolving its trust by spawning
191
+ // `app-server`, plans/codex-write-guard.md "Trust (parts 4–5)"): awaited just before the
192
+ // synchronous invocation, so trust is settled before invocation reads it. It never throws a run
193
+ // away — a throw is a warm-up failure, warned about on the run's own root (skipped with no root)
194
+ // and the run goes on; a codex trust lookup failure already records `off` and warns from prepare
195
+ // itself, so this catch is the guard for a backend whose prepare throws unexpectedly. The call
196
+ // itself is also guarded: a synchronous `prepare` throw would otherwise escape and (on the
197
+ // asynchronous path) leave the run's promise pending, so it is warned-and-continued like an
198
+ // async rejection.
199
+ const warnRoot = nearestTumwaterRoot(opts.sessionDir);
200
+ let prepared;
201
+ try {
202
+ prepared = backend.prepare?.(runSpec);
203
+ }
204
+ catch (error) {
205
+ if (warnRoot !== undefined) {
206
+ warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
207
+ }
208
+ }
209
+ if (prepared === undefined) {
210
+ // No `prepare` step: build and spawn synchronously and hand the bundle straight back, so
211
+ // the coordinator's watchdog start lands in the same synchronous step (see the doc comment).
212
+ return buildAndSpawn(backend, opts, { runSpec, parser, runMarker, resolved, blind, runTmp });
213
+ }
214
+ // Only a backend with a `prepare` step (codex) awaits it, then builds and spawns.
215
+ return (async () => {
216
+ try {
217
+ await prepared;
218
+ }
219
+ catch (error) {
220
+ if (warnRoot !== undefined) {
221
+ warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
222
+ }
223
+ }
224
+ return buildAndSpawn(backend, opts, { runSpec, parser, runMarker, resolved, blind, runTmp });
225
+ })();
226
+ }
@@ -0,0 +1,185 @@
1
+ /** The streaming pipeline of the backend agent runner (plans/agent-backends.md "The seam"):
2
+ * everything the run does live with the child's stdout/stderr after spawn — the native-to-
3
+ * canonical stdout line splitting with its over-long-line discard, the stderr window and its
4
+ * fallback-clone warning matcher, the stream-minted session's second ledger record, and the
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
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. */
11
+ import { removeTree } from "../files/files.js";
12
+ import { recordSession, sessionRecordFor } from "./session-ledger.js";
13
+ /** How many trailing stderr characters are carried into the next chunk's warning match, so a
14
+ * warning split across two `data` events is still seen. The warnings are short lines, so a few
15
+ * hundred characters always cover a straddle. */
16
+ const STDERR_MATCH_TAIL = 512;
17
+ /** The largest native stdout line the runner will buffer. A complete native event is one JSON
18
+ * object; a line this long is already at the default whole-log cap (`logMaxBytes`, 16 MiB), so
19
+ * continuing to buffer it can only be a malformed stream. Past it `feedCanonical` drops the
20
+ * buffered line and skips input to the next newline, so a backend that streams bytes with no
21
+ * newline — each byte resetting the quiet watchdog — cannot grow the buffer until the run
22
+ * timeout (4 h by default) exhausts the harness's memory. */
23
+ export const MAX_NATIVE_LINE_CHARS = 16 * 1024 * 1024;
24
+ /** Build the streaming pipeline for one spawned run. The coordinator passes the bundle, its
25
+ * watchdogs, the abort wiring, the two logs the flush-at-settle writes, the run's TMPDIR it
26
+ * removes at settle, and the promise's `resolve` — everything the moved closures captured
27
+ * before the refactor — and this call returns the single `AgentStream` the coordinator binds
28
+ * the child's `data` events and the settlement state to. */
29
+ export function createAgentStream(input) {
30
+ const { opts, backend, spawned, wd, onAbort, rawLog, nativeLog, runTmp, resolve } = input;
31
+ const { parser, translator, decoder, stderrDecoder, preSpawnSessionId } = spawned;
32
+ // The stream's mutable state (its own per-run locals, shared by exactly the closures below):
33
+ // the stderr window and its warning matcher tail, the settled guard, and the translator's
34
+ // native-line buffer.
35
+ let stderr = "";
36
+ // The fallback-clone warning arrives at the START of a run, while `stderr` keeps only the
37
+ // last 64 KiB; match each chunk (with a short carried tail for a straddle) so a chatty run
38
+ // that pushes the warning out of that window still surfaces the mispriced id.
39
+ let fallbackClone;
40
+ let stderrTail = "";
41
+ let settled = false;
42
+ // The translator's native line buffer: feed the decoder's text through it and only complete
43
+ // lines reach the translator, whose canonical output feeds the parser (and the raw log).
44
+ // `nativeOverflow` marks a buffered line that passed MAX_NATIVE_LINE_CHARS without a newline:
45
+ // its remainder is skipped to the next newline so it can never be mistaken for an event.
46
+ let nativeTail = "";
47
+ let nativeOverflow = false;
48
+ const onLine = (line) => rawLog.stream.write(line + "\n");
49
+ // The translator's session id, once the stream has named it (codex's `thread.started`).
50
+ // The pre-spawn record for a fresh codex run carries the session name; the id it actually
51
+ // ran with is born in the stream, so the first time this returns one the pre-spawn record
52
+ // lacked, append a second ledger record carrying it (same kind, name, cwd and model) — the
53
+ // record a later `exec resume` resumes (design §4 "Sessions").
54
+ let streamMintedSession = false;
55
+ const emitNativeLine = (line) => {
56
+ if (!nativeLog.broken)
57
+ nativeLog.stream.write(line + "\n");
58
+ for (const canonical of translator.translate(line))
59
+ parser.feed(canonical + "\n", onLine);
60
+ const sid = translator.sessionId?.();
61
+ if (sid !== undefined && sid !== preSpawnSessionId && !streamMintedSession) {
62
+ streamMintedSession = true;
63
+ recordSession(opts.sessionDir, sessionRecordFor(backend.id, sid, opts));
64
+ }
65
+ };
66
+ // Split native stdout into complete lines, translate each, and feed the canonical lines to
67
+ // the parser (which also hands them to onLine for the raw log). A trailing partial native
68
+ // line stays buffered exactly as the parser's own buffer once held it. On the final flush
69
+ // (`final`), a remaining tail is emitted as one line: a backend may close after flushing a
70
+ // complete JSON event but not its newline terminator, and dropping that tail loses the
71
+ // run's authoritative result (cost, final text). A genuinely truncated fragment parses as
72
+ // no data (parseJsonObject → []), so this can only recover the last event.
73
+ const feedCanonical = (text, final = false) => {
74
+ // A buffered overflow: discard this line's remainder up to (and including) its newline, so
75
+ // the stream resynchronizes on the next real event line instead of emitting a truncated one.
76
+ if (nativeOverflow) {
77
+ const nl = text.indexOf("\n");
78
+ if (nl < 0)
79
+ return;
80
+ nativeOverflow = false;
81
+ text = text.slice(nl + 1);
82
+ }
83
+ nativeTail += text;
84
+ let start = 0;
85
+ for (;;) {
86
+ const nl = nativeTail.indexOf("\n", start);
87
+ if (nl < 0)
88
+ break;
89
+ emitNativeLine(nativeTail.slice(start, nl));
90
+ start = nl + 1;
91
+ }
92
+ if (start > 0)
93
+ nativeTail = nativeTail.slice(start);
94
+ if (nativeTail.length > MAX_NATIVE_LINE_CHARS) {
95
+ // No newline within the cap: drop the line and warn. On the final flush there is no
96
+ // remainder to skip, so only the buffer is cleared.
97
+ nativeTail = "";
98
+ if (!final) {
99
+ nativeOverflow = true;
100
+ opts.onAgentWarning?.(`native output line exceeded ${MAX_NATIVE_LINE_CHARS / (1024 * 1024)} MiB without a newline and was discarded`);
101
+ }
102
+ }
103
+ else if (final && nativeTail !== "") {
104
+ emitNativeLine(nativeTail);
105
+ nativeTail = "";
106
+ }
107
+ };
108
+ const feedStdout = (chunk) => {
109
+ wd.noteByte();
110
+ feedCanonical(decoder.write(chunk));
111
+ };
112
+ const feedStderr = (chunk) => {
113
+ // stderr is rare and meaningful (crash traces, warnings): treat it as progress.
114
+ wd.noteStderr();
115
+ const text = stderrDecoder.write(chunk);
116
+ fallbackClone ??= backend.matchStderrWarning?.(text, stderrTail);
117
+ stderrTail = (stderrTail + text).slice(-STDERR_MATCH_TAIL);
118
+ stderr += text;
119
+ if (stderr.length > 64 * 1024)
120
+ stderr = stderr.slice(-64 * 1024);
121
+ };
122
+ const finish = (result) => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ // A blind image read (plans/image-input.md "Detecting a blind read"): surface it to the
127
+ // caller once, at settle, with the action its kind earned — the authoring run was killed
128
+ // ("fail"), a gate run only recorded it ("logged"). The caller logs the image_blind event
129
+ // with its own role/run context.
130
+ if (parser.imageBlind) {
131
+ opts.onBlindRead?.(parser.imageBlind.path, opts.kind === "author" ? "fail" : "logged");
132
+ }
133
+ // Best-effort: an unremovable scratch dir (read-only subdir, a straggler still writing) must
134
+ // not throw out of the close handler before the run's result resolves.
135
+ try {
136
+ removeTree(runTmp);
137
+ }
138
+ catch {
139
+ /* left for the OS temp sweep */
140
+ }
141
+ wd.stop();
142
+ opts.signal?.removeEventListener("abort", onAbort);
143
+ // Surface the translator's per-run warnings once, after all native lines were fed.
144
+ for (const warning of translator.warnings?.() ?? [])
145
+ opts.onAgentWarning?.(warning);
146
+ // Resolve only once the logs have flushed: writes complete on libuv's threadpool, so
147
+ // resolving before 'finish' can leave the tail of <role>.pi.jsonl (or the native log)
148
+ // unwritten — a run that dies right after runAgent settles (an abort during a restart
149
+ // drain, a supervisor swap) loses its last lines. end()'s callback fires on 'finish';
150
+ // if a stream errors instead it never does, so settle from 'error' too — a broken log
151
+ // degrades to a lost log, never a stuck run.
152
+ let resolved = false;
153
+ const settle = () => {
154
+ if (!resolved) {
155
+ resolved = true;
156
+ resolve(result);
157
+ }
158
+ };
159
+ let pending = 0;
160
+ const flushed = () => {
161
+ pending -= 1;
162
+ if (pending === 0)
163
+ settle();
164
+ };
165
+ for (const log of [rawLog, nativeLog]) {
166
+ log.stream.on("error", settle);
167
+ if (!log.broken) {
168
+ pending += 1;
169
+ log.stream.end(flushed);
170
+ }
171
+ }
172
+ if (pending === 0)
173
+ settle();
174
+ };
175
+ return {
176
+ feedStdout,
177
+ feedStderr,
178
+ onLine,
179
+ feedCanonical,
180
+ finish,
181
+ settled: () => settled,
182
+ stderr: () => stderr,
183
+ fallbackClone: () => fallbackClone,
184
+ };
185
+ }
@@ -38,6 +38,22 @@ export function recordSession(dir, record) {
38
38
  // Best effort: a lost ledger line costs resumability, not the run.
39
39
  }
40
40
  }
41
+ /** The one run's ledger record, minus the id the two recording sites mint differently — the
42
+ * pre-spawn id in run-spawn.ts, or the stream-minted session id in run-stream.ts. One place
43
+ * for the record's non-id shape (agent, kind, name, cwd, the optional model and the stamp), so
44
+ * the two appends cannot drift on the model spread or the required fields. `agent` is the
45
+ * owning backend's id; the run options supply the rest. */
46
+ export function sessionRecordFor(agent, id, opts) {
47
+ return {
48
+ agent,
49
+ id,
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
+ };
56
+ }
41
57
  /** True when `dir` holds at least one session file a backend could resume by directory. This is
42
58
  * the backend-neutral filesystem check behind pi's `--continue` and the fallback for an
43
59
  * unregistered backend. The ledger file shares the dir and is a `.jsonl` too, so it is
@@ -1,5 +1,6 @@
1
1
  /** The write guard on a run's file-editing tools: pi's `edit`/`write` (src/pi-extension/
2
- * write-guard.ts), claude's `Edit`/`Write` (src/agent/claude/write-guard-hook.ts) and OpenCode's
2
+ * write-guard.ts), claude's `Edit`/`Write` (src/agent/claude/claude-write-guard-hook.ts) and
3
+ * OpenCode's
3
4
  * `edit` permission (src/agent/opencode/write-guard-plugin/index.ts) refuse a write that would
4
5
  * land under the team root but outside the run's own checkout.
5
6
  *
@@ -124,8 +125,9 @@ export function strayWriteRefusal(target, runDir, resolveFrom = runDir) {
124
125
  + (instead ?? "Leave it alone and work only inside the worktree.");
125
126
  }
126
127
  /** The stdin-driven guard-hook driver shared by the claude and codex PreToolUse hooks
127
- * (src/agent/claude/write-guard-hook.ts, src/agent/codex/write-guard-hook.ts). Each hook runs as
128
- * `node write-guard-hook.js` with the backend's hook payload JSON on stdin; this reads it in one
128
+ * (src/agent/claude/claude-write-guard-hook.ts, src/agent/codex/codex-write-guard-hook.ts). Each
129
+ * hook runs as `node claude-write-guard-hook.js` / `node codex-write-guard-hook.js` with the
130
+ * backend's hook payload JSON on stdin; this reads it in one
129
131
  * place, asks `computeRefusal` for a refusal string (null lets the edit/patch run), and prints the
130
132
  * `deny` decision only when there is a refusal. Any failure — unreadable stdin, malformed JSON, a
131
133
  * throw — reads as "no refusal" (null), so a broken hook cannot stop every edit of a run; the