tumwater 0.3.0 → 0.4.1

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 (315) hide show
  1. package/README.md +1 -1
  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 +20 -0
  5. package/dist/src/agent/claude/claude-backend.js +3 -3
  6. package/dist/src/agent/claude/claude-stream.js +170 -166
  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 +4 -4
  10. package/dist/src/agent/codex/codex-rollout.js +10 -21
  11. package/dist/src/agent/codex/codex-stream.js +202 -179
  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/notes-shim.js +2 -2
  15. package/dist/src/agent/opencode/opencode-backend.js +4 -4
  16. package/dist/src/agent/opencode/opencode-config.js +2 -2
  17. package/dist/src/agent/opencode/opencode-stream.js +97 -88
  18. package/dist/src/agent/run-agent.js +66 -381
  19. package/dist/src/agent/run-settle.js +3 -2
  20. package/dist/src/agent/run-spawn.js +235 -0
  21. package/dist/src/agent/run-stream.js +193 -0
  22. package/dist/src/agent/write-guard.js +5 -3
  23. package/dist/src/backlog/backlog-archive.js +88 -36
  24. package/dist/src/backlog/backlog-cli-view.js +92 -98
  25. package/dist/src/backlog/backlog-cli.js +9 -64
  26. package/dist/src/backlog/backlog-commit.js +17 -9
  27. package/dist/src/backlog/backlog-eligibility.js +45 -27
  28. package/dist/src/backlog/backlog-index-data.js +159 -0
  29. package/dist/src/backlog/backlog-index.js +7 -5
  30. package/dist/src/backlog/backlog-md.js +1 -1
  31. package/dist/src/backlog/backlog-parts.js +23 -13
  32. package/dist/src/backlog/backlog-release.js +43 -33
  33. package/dist/src/backlog/backlog-render.js +1 -1
  34. package/dist/src/backlog/backlog-structure.js +0 -144
  35. package/dist/src/backlog/backlog-write.js +30 -17
  36. package/dist/src/backlog/backlog.js +9 -0
  37. package/dist/src/backlog/question-answer.js +3 -1
  38. package/dist/src/baseline/main-baseline.js +50 -15
  39. package/dist/src/baseline/main-guardrails.js +29 -78
  40. package/dist/src/baseline/main-red.js +64 -103
  41. package/dist/src/baseline/main-watch.js +89 -35
  42. package/dist/src/brief/brief-edit.js +3 -1
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +330 -0
  45. package/dist/src/budget/budget-wiring.js +167 -0
  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/dep-install.js +64 -1
  50. package/dist/src/cli/cli-args.js +72 -0
  51. package/dist/src/cli/cli-flag-specs.js +38 -0
  52. package/dist/src/cli/cli-marker-commands.js +2 -2
  53. package/dist/src/cli/cli-query-commands.js +51 -41
  54. package/dist/src/cli/cli-review-commands.js +113 -0
  55. package/dist/src/cli/cli-role-command.js +140 -58
  56. package/dist/src/cli/cli-start-summary.js +10 -3
  57. package/dist/src/cli/cli-start.js +92 -38
  58. package/dist/src/cli/completion.js +21 -1
  59. package/dist/src/cli/config-commands.js +17 -8
  60. package/dist/src/cli/help.js +84 -16
  61. package/dist/src/cli/log-commands.js +137 -105
  62. package/dist/src/cli/question-commands.js +89 -64
  63. package/dist/src/cli.js +408 -286
  64. package/dist/src/collections.js +56 -6
  65. package/dist/src/config/config-editable-keys.js +3 -0
  66. package/dist/src/config/config-fallbacks.js +3 -2
  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 +51 -241
  70. package/dist/src/config/config-views.js +10 -2
  71. package/dist/src/config/config-write.js +209 -140
  72. package/dist/src/config/config.js +9 -3
  73. package/dist/src/doctor/doctor-backlog-bugs.js +96 -53
  74. package/dist/src/doctor/doctor-backlog-plans.js +222 -88
  75. package/dist/src/doctor/doctor-backlog.js +171 -32
  76. package/dist/src/doctor/doctor-check-names.js +7 -1
  77. package/dist/src/doctor/doctor-checks.js +26 -19
  78. package/dist/src/doctor/doctor-claude-checks.js +6 -9
  79. package/dist/src/doctor/doctor-codex-checks.js +15 -29
  80. package/dist/src/doctor/doctor-contention-checks.js +293 -134
  81. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  82. package/dist/src/doctor/doctor-holds.js +13 -15
  83. package/dist/src/doctor/doctor-model-checks.js +45 -200
  84. package/dist/src/doctor/doctor-notify.js +7 -2
  85. package/dist/src/doctor/doctor-opencode-checks.js +23 -9
  86. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  87. package/dist/src/doctor/doctor-principles.js +26 -0
  88. package/dist/src/doctor/doctor-project-checks.js +108 -13
  89. package/dist/src/doctor/doctor-role-notes.js +51 -0
  90. package/dist/src/doctor/doctor.js +17 -5
  91. package/dist/src/doctor/probe.js +14 -1
  92. package/dist/src/events/event-format.js +313 -157
  93. package/dist/src/events/event-labels.js +0 -1
  94. package/dist/src/events/event-read.js +13 -0
  95. package/dist/src/events/event-window.js +20 -2
  96. package/dist/src/events/notify.js +1 -1
  97. package/dist/src/failure/failure-cluster.js +4 -3
  98. package/dist/src/failure/failure-data.js +127 -90
  99. package/dist/src/failure/failure-render.js +62 -21
  100. package/dist/src/failure/failure-state-change.js +174 -8
  101. package/dist/src/failure/time-spend.js +136 -167
  102. package/dist/src/files/file-queue.js +17 -0
  103. package/dist/src/files/files.js +7 -0
  104. package/dist/src/gates/budget-gates.js +116 -324
  105. package/dist/src/gates/gate-polls.js +123 -184
  106. package/dist/src/gates/maintenance-quota.js +27 -17
  107. package/dist/src/gates/role-cap-gates.js +1 -1
  108. package/dist/src/gates/streak-gate.js +1 -1
  109. package/dist/src/git/commit-message.js +12 -0
  110. package/dist/src/git/git.js +18 -0
  111. package/dist/src/git/slot-salvage.js +4 -5
  112. package/dist/src/git/slots-state.js +12 -3
  113. package/dist/src/git/worktree-pool-startup.js +127 -0
  114. package/dist/src/git/worktree-pool.js +1 -120
  115. package/dist/src/gui/gui-admin-endpoints.js +1 -1
  116. package/dist/src/gui/gui-args.js +1 -1
  117. package/dist/src/gui/gui-backlog-commands.js +21 -12
  118. package/dist/src/gui/gui-backlog-endpoints.js +6 -5
  119. package/dist/src/gui/gui-command.js +19 -1
  120. package/dist/src/gui/gui-config-commands.js +126 -85
  121. package/dist/src/gui/gui-endpoint-commands.js +5 -3
  122. package/dist/src/gui/gui-endpoint-common.js +4 -3
  123. package/dist/src/gui/gui-history-endpoints.js +36 -27
  124. package/dist/src/gui/gui-operator-commands.js +175 -10
  125. package/dist/src/gui/gui-prompt-commands.js +16 -7
  126. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  127. package/dist/src/gui/gui-report-endpoints.js +14 -4
  128. package/dist/src/gui/gui-server.js +20 -3
  129. package/dist/src/gui/gui-team-commands.js +1 -1
  130. package/dist/src/history/history-data.js +7 -1
  131. package/dist/src/history/history-queued.js +8 -0
  132. package/dist/src/history/history-render.js +15 -10
  133. package/dist/src/history/history.js +3 -6
  134. package/dist/src/inbox/inbox-top.js +4 -9
  135. package/dist/src/inbox/pending-prompt.js +3 -2
  136. package/dist/src/inbox/prompt-commands.js +189 -161
  137. package/dist/src/init/init.js +43 -17
  138. package/dist/src/merge/merge-core.js +2 -25
  139. package/dist/src/merge/merge-drain.js +23 -136
  140. package/dist/src/merge/merge-git.js +5 -5
  141. package/dist/src/merge/merge-pipeline.js +1 -1
  142. package/dist/src/merge/merge-post-review-drain.js +214 -0
  143. package/dist/src/merge/merge-queue-render.js +11 -12
  144. package/dist/src/merge/merge-queue.js +24 -2
  145. package/dist/src/merge/merge-requeue.js +1 -0
  146. package/dist/src/merge/merge-stack.js +1 -7
  147. package/dist/src/merge/merge-to-main.js +2 -10
  148. package/dist/src/merge/merge-top.js +33 -19
  149. package/dist/src/merge/merge-verify.js +65 -9
  150. package/dist/src/notes-cli.js +8 -3
  151. package/dist/src/operator/operator-commands.js +16 -5
  152. package/dist/src/operator/operator-intent.js +26 -3
  153. package/dist/src/operator/revert.js +18 -12
  154. package/dist/src/orchestrator/orchestrator-poll.js +49 -17
  155. package/dist/src/orchestrator/orchestrator-scheduling.js +4 -2
  156. package/dist/src/orchestrator/orchestrator.js +6 -5
  157. package/dist/src/orchestrator/scheduling-scope.js +51 -7
  158. package/dist/src/orchestrator/scheduling-skip.js +15 -5
  159. package/dist/src/paths.js +26 -10
  160. package/dist/src/pi/pi-backend.js +2 -2
  161. package/dist/src/pi/pi-bin.js +2 -2
  162. package/dist/src/pi/pi-models.js +9 -0
  163. package/dist/src/pi/pi-stream.js +63 -41
  164. package/dist/src/pi/pi-watchdogs.js +65 -49
  165. package/dist/src/pi-extension/full-output.js +2 -3
  166. package/dist/src/process/process.js +1 -1
  167. package/dist/src/process/run-marker.js +10 -2
  168. package/dist/src/process/supervisor.js +7 -4
  169. package/dist/src/prompt/gate-notes.js +35 -10
  170. package/dist/src/prompt/gate-prompts.js +30 -7
  171. package/dist/src/prompt/notes-backlog.js +24 -15
  172. package/dist/src/prompt/principles.js +11 -0
  173. package/dist/src/prompt/prompt.js +13 -7
  174. package/dist/src/redeploy/redeploy-probes.js +11 -1
  175. package/dist/src/redeploy/redeployer.js +54 -7
  176. package/dist/src/redeploy/self-reload.js +7 -2
  177. package/dist/src/report/report-data.js +322 -223
  178. package/dist/src/report/report-fold.js +257 -191
  179. package/dist/src/report/report-main-red.js +64 -0
  180. package/dist/src/report/report-render.js +133 -25
  181. package/dist/src/report/report.js +2 -5
  182. package/dist/src/review/diff-rules.js +53 -0
  183. package/dist/src/review/known-flakes.js +32 -1
  184. package/dist/src/review/post-merge-review-accept.js +51 -0
  185. package/dist/src/review/post-merge-review-render.js +112 -0
  186. package/dist/src/review/post-merge-review.js +159 -24
  187. package/dist/src/review/protected-edits.js +22 -7
  188. package/dist/src/review/quarantine-render.js +82 -0
  189. package/dist/src/review/review-precheck.js +2 -1
  190. package/dist/src/review/review.js +33 -49
  191. package/dist/src/roles/role-catalog.js +73 -2
  192. package/dist/src/roles/role-guidance.js +18 -0
  193. package/dist/src/roles/role-labels.js +6 -3
  194. package/dist/src/roles/role-render.js +62 -8
  195. package/dist/src/roles/role-view.js +54 -12
  196. package/dist/src/roles/roles.js +3 -23
  197. package/dist/src/run/ci-parity.js +121 -0
  198. package/dist/src/run/feedback-filings.js +10 -1
  199. package/dist/src/run/qa-coverage.js +50 -15
  200. package/dist/src/run/run-apply.js +14 -2
  201. package/dist/src/run/run-detail-data.js +36 -16
  202. package/dist/src/run/run-detail-render.js +27 -9
  203. package/dist/src/run/run-finalize.js +76 -46
  204. package/dist/src/run/run-prompt.js +231 -177
  205. package/dist/src/run/run-stage.js +168 -70
  206. package/dist/src/run/run-verdict.js +127 -13
  207. package/dist/src/run/stage-backlog-checks.js +2 -2
  208. package/dist/src/run/stage-check.js +17 -11
  209. package/dist/src/run/stage-checks-shared.js +80 -0
  210. package/dist/src/run/stage-name-checks.js +194 -0
  211. package/dist/src/run/stage-path-checks.js +35 -537
  212. package/dist/src/run/stage-prose-checks.js +261 -0
  213. package/dist/src/runner/leftover.js +61 -21
  214. package/dist/src/runner/revision.js +14 -25
  215. package/dist/src/runner/runner-fallback.js +1 -1
  216. package/dist/src/runner/runner-outcome.js +4 -2
  217. package/dist/src/runner/runner-pi.js +72 -49
  218. package/dist/src/runner/runner-recovery.js +18 -3
  219. package/dist/src/runner/runner-run.js +36 -29
  220. package/dist/src/runner/runner-state.js +4 -3
  221. package/dist/src/scheduling/claim-entries.js +49 -0
  222. package/dist/src/scheduling/claims.js +1 -1
  223. package/dist/src/status/status-data.js +144 -86
  224. package/dist/src/status/status-polls.js +56 -5
  225. package/dist/src/status/status-role-notes.js +49 -0
  226. package/dist/src/status/status-roles.js +19 -2
  227. package/dist/src/team/reclaim.js +2 -2
  228. package/dist/src/text/datetime.js +3 -2
  229. package/dist/src/text/format.js +13 -1
  230. package/dist/src/text/phrases.js +22 -5
  231. package/dist/src/text/text.js +11 -0
  232. package/dist/src/ui/alert-feeds.js +320 -0
  233. package/dist/src/ui/badges.js +35 -2
  234. package/dist/src/ui/gui/gui-client-backlog.js +25 -3
  235. package/dist/src/ui/gui/gui-client-boot.js +102 -74
  236. package/dist/src/ui/gui/gui-client-drawer-config.js +64 -17
  237. package/dist/src/ui/gui/gui-client-drawer.js +46 -5
  238. package/dist/src/ui/gui/gui-client-feed.js +4 -9
  239. package/dist/src/ui/gui/gui-client-history.js +90 -88
  240. package/dist/src/ui/gui/gui-client-labels.js +5 -1
  241. package/dist/src/ui/gui/gui-client-model.js +9 -9
  242. package/dist/src/ui/gui/gui-client-operator.js +11 -11
  243. package/dist/src/ui/gui/gui-client-pending.js +189 -12
  244. package/dist/src/ui/gui/gui-client-report-chart.js +137 -10
  245. package/dist/src/ui/gui/gui-client-report.js +81 -6
  246. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  247. package/dist/src/ui/gui/gui-client-settings.js +138 -4
  248. package/dist/src/ui/gui/gui-client-team.js +13 -2
  249. package/dist/src/ui/gui/gui-client.js +44 -5
  250. package/dist/src/ui/gui/gui-page.js +1 -0
  251. package/dist/src/ui/gui/gui-styles.js +4 -0
  252. package/dist/src/ui/progress-data.js +2 -2
  253. package/dist/src/ui/run-progress-model.js +4 -3
  254. package/dist/src/ui/status-model.js +24 -14
  255. package/dist/src/ui/status-payload.js +168 -117
  256. package/dist/src/ui/status-render.js +140 -81
  257. package/dist/src/ui/team-alert-backlog-feed.js +181 -0
  258. package/dist/src/ui/team-alert-backlog.js +267 -0
  259. package/dist/src/ui/team-alert-merge.js +183 -0
  260. package/dist/src/ui/team-alert-shared.js +79 -0
  261. package/dist/src/ui/team-alert-status.js +321 -0
  262. package/dist/src/ui/team-alert-tail.js +251 -0
  263. package/dist/src/ui/team-alerts.js +31 -513
  264. package/dist/src/ui/tone.js +7 -0
  265. package/dist/src/ui/tui/tui-backlog.js +8 -6
  266. package/dist/src/ui/tui/tui-editor-attach.js +1 -7
  267. package/dist/src/ui/tui/tui-editor-backends.js +1 -6
  268. package/dist/src/ui/tui/tui-editor-brief.js +4 -24
  269. package/dist/src/ui/tui/tui-editor-budget.js +10 -12
  270. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  271. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  272. package/dist/src/ui/tui/tui-editor-concurrency.js +1 -0
  273. package/dist/src/ui/tui/tui-editor-config.js +19 -6
  274. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  275. package/dist/src/ui/tui/tui-editor-entry.js +3 -17
  276. package/dist/src/ui/tui/tui-editor-instances.js +9 -0
  277. package/dist/src/ui/tui/tui-editor-instructions.js +51 -127
  278. package/dist/src/ui/tui/tui-editor-interval.js +51 -113
  279. package/dist/src/ui/tui/tui-editor-notes.js +29 -103
  280. package/dist/src/ui/tui/tui-editor-pause.js +2 -1
  281. package/dist/src/ui/tui/tui-editor-prompt.js +2 -1
  282. package/dist/src/ui/tui/tui-editor-quiet.js +4 -13
  283. package/dist/src/ui/tui/tui-editor-release.js +77 -0
  284. package/dist/src/ui/tui/tui-editor-retire.js +1 -7
  285. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  286. package/dist/src/ui/tui/tui-editor-role-file.js +75 -0
  287. package/dist/src/ui/tui/tui-editor-role-key.js +3 -7
  288. package/dist/src/ui/tui/tui-editor-session.js +44 -0
  289. package/dist/src/ui/tui/tui-editor-slots.js +1 -0
  290. package/dist/src/ui/tui/tui-editors.js +13 -1
  291. package/dist/src/ui/tui/tui-frame.js +23 -7
  292. package/dist/src/ui/tui/tui-history.js +12 -10
  293. package/dist/src/ui/tui/tui-keys.js +14 -0
  294. package/dist/src/ui/tui/tui-pane-activity.js +21 -8
  295. package/dist/src/ui/tui/tui-pane-backlog.js +36 -9
  296. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  297. package/dist/src/ui/tui/tui-pane-health.js +153 -3
  298. package/dist/src/ui/tui/tui-pane-history.js +308 -228
  299. package/dist/src/ui/tui/tui-pane-pending.js +285 -56
  300. package/dist/src/ui/tui/tui-pane-queued.js +7 -2
  301. package/dist/src/ui/tui/tui-pane-report.js +9 -3
  302. package/dist/src/ui/tui/tui-pane-session.js +12 -0
  303. package/dist/src/ui/tui/tui-pane-transcript.js +12 -8
  304. package/dist/src/ui/tui/tui-pane.js +16 -10
  305. package/dist/src/ui/tui/tui-panes.js +247 -134
  306. package/dist/src/ui/tui/tui.js +8 -5
  307. package/dist/src/verdict/fix-claim.js +47 -1
  308. package/dist/src/verdict/refusal.js +2 -1
  309. package/dist/src/verdict/reply-contract.js +35 -0
  310. package/package.json +1 -1
  311. package/dist/src/baseline/main-culprit.js +0 -94
  312. package/dist/src/baseline/main-revert.js +0 -133
  313. package/dist/src/merge/merge-post-review.js +0 -90
  314. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  315. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -10,11 +10,13 @@
10
10
  * state (the open tool calls, the turn's usage fallback, the unmapped native types seen), so
11
11
  * call it once per run. Unmapped native types, the distinct text of `error` items, and a
12
12
  * command abandoned at the end of its turn are recorded once each on the seam's optional
13
- * `warnings()` — src/agent/run-agent.ts forwards each as an `agent_unknown_event` warning
13
+ * `warnings()` — src/agent/run-stream.ts forwards each as an `agent_unknown_event` warning
14
14
  * after the run. `sessionId()` reports the thread id once `thread.started` has named it.
15
15
  */
16
16
  import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
17
17
  import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
18
+ import { secondsUntil } from "../../text/datetime.js";
19
+ import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
18
20
  /** How codex wraps every shell command — the prefix to strip (with its quoting) so the
19
21
  * canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
20
22
  const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
@@ -24,9 +26,6 @@ const RECONNECT = /^Reconnecting\.\.\. (\d+)\/(\d+) /;
24
26
  /** The item types dropped silently, with no canonical event and no `warnings()` entry
25
27
  * (design §5's `item.*` row): `todo_list` and `web_search`. */
26
28
  const DROPPED_ITEM_TYPES = new Set(["todo_list", "web_search"]);
27
- /** HTTP status classes that mean a transient backend failure (shared with the claude and
28
- * codex classifiers' reading of `codex exec`'s JSON error). */
29
- const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
30
29
  /** Strip codex's `/bin/zsh -lc '<cmd>'` (or `bash -lc`) wrapper and its quoting, returning
31
30
  * the model's own command — the argument suite-rerun detection matches. Passes a command the
32
31
  * capture did not wrap through unchanged. */
@@ -76,7 +75,8 @@ function classifyTurnFailed(parsed) {
76
75
  const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
77
76
  const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
78
77
  const resetsAt = nonNegativeNumber(parsed.resets_at, undefined);
79
- const retryAfter = resetsAt === undefined ? undefined : Math.max(0, Math.round(resetsAt - Date.now() / 1000));
78
+ // secondsUntil folds the unix-seconds resets stamp (×1000 to epoch-ms), clamped at 0.
79
+ const retryAfter = resetsAt === undefined ? undefined : secondsUntil(resetsAt * 1000, Date.now());
80
80
  kind.kind = "transientRateLimit";
81
81
  kind.rateLimit = {
82
82
  type: "rate_limit",
@@ -94,7 +94,7 @@ function classifyTurnFailed(parsed) {
94
94
  kind.kind = "configError";
95
95
  return kind;
96
96
  }
97
- if (status !== undefined && SERVER_STATUS.has(status)) {
97
+ if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status)) {
98
98
  kind.kind = "transientBackend";
99
99
  kind.backendKind = "server";
100
100
  return kind;
@@ -183,187 +183,210 @@ export function codexTranslator(opts = {}) {
183
183
  }
184
184
  return out;
185
185
  };
186
+ /** The `thread.started` arm: announces the session and the agent start, or records the
187
+ * unmapped event once when the thread id is missing. */
188
+ const handleThreadStarted = (event) => {
189
+ const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
190
+ if (id === undefined) {
191
+ recordEvent("thread.started");
192
+ return [];
193
+ }
194
+ sessionIdValue = id;
195
+ const out = [];
196
+ out.push(JSON.stringify({
197
+ type: "session",
198
+ id,
199
+ ...(cwd !== undefined ? { cwd } : {}),
200
+ }));
201
+ out.push(JSON.stringify({ type: "agent_start" }));
202
+ return out;
203
+ };
204
+ /** The `turn.completed` arm: folds the usage block and abandons the open calls. */
205
+ const handleTurnCompleted = (event) => {
206
+ sawTurnCompleted = true;
207
+ const usage = objectOrEmpty(event.usage);
208
+ turnUsage =
209
+ usage.input_tokens !== undefined || usage.output_tokens !== undefined
210
+ ? {
211
+ input: nonNegativeNumber(usage.input_tokens, 0),
212
+ cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
213
+ cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
214
+ output: nonNegativeNumber(usage.output_tokens, 0),
215
+ }
216
+ : undefined;
217
+ return abandonOpenCalls();
218
+ };
219
+ /** The `turn.failed` arm: classifies the inner-JSON error and closes the turn. */
220
+ const handleTurnFailed = (event) => {
221
+ sawTurnFailed = true;
222
+ // The error message is itself a JSON string (finding 11); fall back to the held
223
+ // top-level error text when it is not.
224
+ const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
225
+ ? event.error.message
226
+ : undefined;
227
+ const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
228
+ const cls = inner ? classifyTurnFailed(inner) : {};
229
+ const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
230
+ const closes = abandonOpenCalls();
231
+ const message = { role: "assistant", content: [], stopReason: "error" };
232
+ if (errorMessage !== undefined)
233
+ message.errorMessage = errorMessage;
234
+ const end = {
235
+ type: "message_end",
236
+ message,
237
+ ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
238
+ ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
239
+ };
240
+ if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
241
+ end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
242
+ return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
243
+ };
244
+ /** The top-level `error` arm: a reconnect text emits an `auto_retry_start`; any other
245
+ * message is held as the next `turn.failed`'s fallback text. */
246
+ const handleErrorLine = (event) => {
247
+ const message = typeof event.message === "string" ? event.message : undefined;
248
+ const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
249
+ if (reconnect) {
250
+ return [
251
+ JSON.stringify({
252
+ type: "auto_retry_start",
253
+ attempt: Number(reconnect[1]),
254
+ maxAttempts: Number(reconnect[2]),
255
+ errorMessage: message,
256
+ }),
257
+ ];
258
+ }
259
+ // Other top-level `error` lines are not fatal (design §5): held as the fallback text
260
+ // for a following `turn.failed`.
261
+ if (message !== undefined)
262
+ pendingFailureText = message;
263
+ return [];
264
+ };
265
+ /** The `item.started`/`item.completed` arm: reads the item and delegates by type to the
266
+ * item handlers, dropping the silent types and warning once on the unknown tail. */
267
+ const handleItem = (event, started) => {
268
+ const item = objectOrEmpty(event.item);
269
+ const itemType = typeof item.type === "string" ? item.type : "unknown";
270
+ if (itemType === "command_execution")
271
+ return handleCommandItem(item, started);
272
+ if (itemType === "file_change")
273
+ return handleFileChangeItem(item, started);
274
+ if (itemType === "error")
275
+ return handleErrorItem(item);
276
+ if (itemType === "agent_message")
277
+ return handleAgentMessageItem(item);
278
+ if (DROPPED_ITEM_TYPES.has(itemType))
279
+ return [];
280
+ // mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
281
+ // once-per-type warning only (design §5).
282
+ recordItem(item);
283
+ return [];
284
+ };
285
+ /** The `command_execution` item arm: opens or closes the bash call, capping the result. */
286
+ const handleCommandItem = (item, started) => {
287
+ const command = typeof item.command === "string" ? item.command : "";
288
+ const out = [];
289
+ if (started) {
290
+ const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
291
+ openCalls.set(id, "command_execution");
292
+ const inner = innerCommand(command);
293
+ out.push(JSON.stringify({
294
+ type: "message_end",
295
+ message: {
296
+ role: "assistant",
297
+ content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
298
+ },
299
+ }));
300
+ out.push(JSON.stringify({
301
+ type: "tool_execution_start",
302
+ toolCallId: id,
303
+ toolName: "bash",
304
+ args: { command: inner },
305
+ }));
306
+ }
307
+ else {
308
+ const id = typeof item.id === "string" ? item.id : "";
309
+ openCalls.delete(id);
310
+ const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
311
+ const exitCode = nonNegativeNumber(item.exit_code, undefined);
312
+ out.push(JSON.stringify({
313
+ type: "tool_execution_end",
314
+ toolCallId: id,
315
+ isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
316
+ result: boundText(aggregated, bashLimit),
317
+ }));
318
+ }
319
+ return out;
320
+ };
321
+ /** The `file_change` item arm: mirrors each change as an `edit` tool call. */
322
+ const handleFileChangeItem = (item, started) => {
323
+ const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
324
+ const id = typeof item.id === "string" ? item.id : "";
325
+ const out = [];
326
+ if (!started) {
327
+ const isError = typeof item.status === "string" && item.status !== "completed";
328
+ for (let i = 0; i < changes.length; i++) {
329
+ const callId = `${id}-${i}`;
330
+ openCalls.delete(callId);
331
+ out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
332
+ }
333
+ return out;
334
+ }
335
+ const content = changes.map((c, i) => ({
336
+ type: "toolCall",
337
+ id: `${id}-${i}`,
338
+ name: "edit",
339
+ arguments: {
340
+ path: typeof c.path === "string" ? c.path : undefined,
341
+ kind: typeof c.kind === "string" ? c.kind : undefined,
342
+ },
343
+ }));
344
+ if (content.length === 0)
345
+ return out; // no changes to mirror, nothing else to emit
346
+ out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
347
+ for (let i = 0; i < content.length; i++) {
348
+ const callId = `${id}-${i}`;
349
+ openCalls.set(callId, "file_change");
350
+ out.push(JSON.stringify({
351
+ type: "tool_execution_start",
352
+ toolCallId: callId,
353
+ toolName: "edit",
354
+ args: content[i].arguments,
355
+ }));
356
+ }
357
+ return out;
358
+ };
359
+ /** The `item.type === "error"` arm: a non-fatal notice warned once per distinct message. */
360
+ const handleErrorItem = (item) => {
361
+ const message = typeof item.message === "string" ? item.message : undefined;
362
+ if (message !== undefined)
363
+ recordErrorItem(message);
364
+ return [];
365
+ };
366
+ /** The `agent_message` arm: the model's reply as one canonical `message_end`. */
367
+ const handleAgentMessageItem = (item) => {
368
+ const text = typeof item.text === "string" ? item.text : "";
369
+ return [
370
+ JSON.stringify({
371
+ type: "message_end",
372
+ message: { role: "assistant", content: [{ type: "text", text }] },
373
+ }),
374
+ ];
375
+ };
186
376
  const translate = (line) => {
187
377
  const parsed = parseJsonObject(line);
188
378
  if (!parsed)
189
379
  return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
190
380
  const event = parsed;
191
381
  const type = typeof event.type === "string" ? event.type : "unknown";
192
- const out = [];
193
382
  switch (type) {
194
- case "thread.started": {
195
- const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
196
- if (id === undefined) {
197
- recordEvent(type);
198
- return [];
199
- }
200
- sessionIdValue = id;
201
- out.push(JSON.stringify({
202
- type: "session",
203
- id,
204
- ...(cwd !== undefined ? { cwd } : {}),
205
- }));
206
- out.push(JSON.stringify({ type: "agent_start" }));
207
- return out;
208
- }
209
- case "turn.started":
210
- return [];
211
- case "turn.completed": {
212
- sawTurnCompleted = true;
213
- const usage = objectOrEmpty(event.usage);
214
- turnUsage =
215
- usage.input_tokens !== undefined || usage.output_tokens !== undefined
216
- ? {
217
- input: nonNegativeNumber(usage.input_tokens, 0),
218
- cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
219
- cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
220
- output: nonNegativeNumber(usage.output_tokens, 0),
221
- }
222
- : undefined;
223
- return abandonOpenCalls();
224
- }
225
- case "turn.failed": {
226
- sawTurnFailed = true;
227
- // The error message is itself a JSON string (finding 11); fall back to the held
228
- // top-level error text when it is not.
229
- const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
230
- ? event.error.message
231
- : undefined;
232
- const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
233
- const cls = inner ? classifyTurnFailed(inner) : {};
234
- const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
235
- const closes = abandonOpenCalls();
236
- const message = { role: "assistant", content: [], stopReason: "error" };
237
- if (errorMessage !== undefined)
238
- message.errorMessage = errorMessage;
239
- const end = {
240
- type: "message_end",
241
- message,
242
- ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
243
- ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
244
- };
245
- if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
246
- end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
247
- return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
248
- }
249
- case "error": {
250
- const message = typeof event.message === "string" ? event.message : undefined;
251
- const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
252
- if (reconnect) {
253
- return [
254
- JSON.stringify({
255
- type: "auto_retry_start",
256
- attempt: Number(reconnect[1]),
257
- maxAttempts: Number(reconnect[2]),
258
- errorMessage: message,
259
- }),
260
- ];
261
- }
262
- // Other top-level `error` lines are not fatal (design §5): held as the fallback text
263
- // for a following `turn.failed`.
264
- if (message !== undefined)
265
- pendingFailureText = message;
266
- return [];
267
- }
383
+ case "thread.started": return handleThreadStarted(event);
384
+ case "turn.started": return [];
385
+ case "turn.completed": return handleTurnCompleted(event);
386
+ case "turn.failed": return handleTurnFailed(event);
387
+ case "error": return handleErrorLine(event);
268
388
  case "item.started":
269
- case "item.completed": {
270
- const item = objectOrEmpty(event.item);
271
- const itemType = typeof item.type === "string" ? item.type : "unknown";
272
- const started = type === "item.started";
273
- if (itemType === "command_execution") {
274
- const command = typeof item.command === "string" ? item.command : "";
275
- if (started) {
276
- const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
277
- openCalls.set(id, "command_execution");
278
- const inner = innerCommand(command);
279
- out.push(JSON.stringify({
280
- type: "message_end",
281
- message: {
282
- role: "assistant",
283
- content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
284
- },
285
- }));
286
- out.push(JSON.stringify({
287
- type: "tool_execution_start",
288
- toolCallId: id,
289
- toolName: "bash",
290
- args: { command: inner },
291
- }));
292
- }
293
- else {
294
- const id = typeof item.id === "string" ? item.id : "";
295
- openCalls.delete(id);
296
- const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
297
- const exitCode = nonNegativeNumber(item.exit_code, undefined);
298
- out.push(JSON.stringify({
299
- type: "tool_execution_end",
300
- toolCallId: id,
301
- isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
302
- result: boundText(aggregated, bashLimit),
303
- }));
304
- }
305
- return out;
306
- }
307
- if (itemType === "file_change") {
308
- const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
309
- const id = typeof item.id === "string" ? item.id : "";
310
- if (!started) {
311
- const isError = typeof item.status === "string" && item.status !== "completed";
312
- for (let i = 0; i < changes.length; i++) {
313
- const callId = `${id}-${i}`;
314
- openCalls.delete(callId);
315
- out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
316
- }
317
- return out;
318
- }
319
- const content = changes.map((c, i) => ({
320
- type: "toolCall",
321
- id: `${id}-${i}`,
322
- name: "edit",
323
- arguments: {
324
- path: typeof c.path === "string" ? c.path : undefined,
325
- kind: typeof c.kind === "string" ? c.kind : undefined,
326
- },
327
- }));
328
- if (content.length === 0)
329
- return out; // no changes to mirror, nothing else to emit
330
- out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
331
- for (let i = 0; i < content.length; i++) {
332
- const callId = `${id}-${i}`;
333
- openCalls.set(callId, "file_change");
334
- out.push(JSON.stringify({
335
- type: "tool_execution_start",
336
- toolCallId: callId,
337
- toolName: "edit",
338
- args: content[i].arguments,
339
- }));
340
- }
341
- return out;
342
- }
343
- if (itemType === "error") {
344
- // An error item is a non-fatal notice (finding 11); warned once per distinct message.
345
- const message = typeof item.message === "string" ? item.message : undefined;
346
- if (message !== undefined)
347
- recordErrorItem(message);
348
- return [];
349
- }
350
- if (itemType === "agent_message") {
351
- // The model's reply: one canonical `message_end` with one text block.
352
- const text = typeof item.text === "string" ? item.text : "";
353
- return [
354
- JSON.stringify({
355
- type: "message_end",
356
- message: { role: "assistant", content: [{ type: "text", text }] },
357
- }),
358
- ];
359
- }
360
- if (DROPPED_ITEM_TYPES.has(itemType))
361
- return [];
362
- // mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
363
- // once-per-type warning only (design §5).
364
- recordItem(item);
365
- return [];
366
- }
389
+ case "item.completed": return handleItem(event, type === "item.started");
367
390
  default:
368
391
  recordEvent(type);
369
392
  return [];
@@ -1,5 +1,6 @@
1
1
  /** The PreToolUse hook codex runs before each `apply_patch` in a team run (part 6/9 installs it
2
- * through `-c`): `node write-guard-hook.js` with codex's hook payload on stdin. To block the patch
2
+ * through `-c`): `node codex-write-guard-hook.js` with codex's hook payload on stdin. To block
3
+ * the patch
3
4
  * it prints a `deny` decision with the refusal as its reason, in the shape finding 5 showed codex
4
5
  * accepts: the model reads only the reason, so a denial by exit code would not carry it. It prints
5
6
  * nothing to let the patch run, and exits 0 either way. Any failure of its own (bad JSON, a throw)
@@ -1,6 +1,6 @@
1
1
  /** codex's half of the write guard (src/agent/write-guard.ts holds the rule): a PreToolUse hook on
2
2
  * `apply_patch`, installed by part 6/9, refuses an `apply_patch` that writes under the team root
3
- * but outside the run's worktree. The entry point is write-guard-hook.ts beside this file.
3
+ * but outside the run's worktree. The entry point is codex-write-guard-hook.ts beside this file.
4
4
  *
5
5
  * Codex's hook takes no argument naming the worktree, unlike claude's: codex trusts a hook by a
6
6
  * hash of its whole definition, so a command constant per build needs one trust lookup, while a
@@ -63,11 +63,11 @@ export function tomlString(s) {
63
63
  return out + '"';
64
64
  }
65
65
  /** The codex write guard's definition: the PreToolUse hook on `apply_patch` that runs the
66
- * compiled write-guard-hook.js with this process's node, falling back to `node` on PATH when
66
+ * compiled codex-write-guard-hook.js with this process's node, falling back to `node` on PATH when
67
67
  * that binary has moved (as claude's and the notes shim do). The hook path resolves from this
68
68
  * module, so a staged build (.tumwater/build/<sha>) runs its own copy and gets its own hash. */
69
69
  export function codexWriteGuardDefinition(nodeBin = process.execPath) {
70
- const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
70
+ const hook = fileURLToPath(new URL("./codex-write-guard-hook.js", import.meta.url));
71
71
  const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)}`;
72
72
  return { matcher: HOOK_MATCHER, command, timeout: HOOK_TIMEOUT_SECONDS };
73
73
  }
@@ -1,5 +1,5 @@
1
1
  /** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
2
- * (plans/notes-command.md "Reaching it from a run"): run-agent calls `ensureNotesShim(root)`
2
+ * (plans/notes-command.md "Reaching it from a run"): run-spawn calls `ensureNotesShim(root)`
3
3
  * before building each invocation, and the backend prepends the returned dir to the run's
4
4
  * PATH (backendBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
5
5
  * script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
@@ -20,7 +20,7 @@ export const NOTES_SHIM_NAME = "tumwater-notes";
20
20
  /** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
21
21
  * The shim is written atomically with mode 0755, only when its content differs from what is
22
22
  * already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
23
- * write is the caller's to absorb — run-agent logs one warning and runs without the shim. */
23
+ * write is the caller's to absorb — run-spawn logs one warning and runs without the shim. */
24
24
  export function ensureNotesShim(root, execPath = process.execPath) {
25
25
  const dir = notesShimDir(root);
26
26
  const target = path.join(dir, NOTES_SHIM_NAME);
@@ -1,8 +1,8 @@
1
1
  /** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
2
2
  * "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
3
3
  * json` into the canonical stream, runs the authoritative session export after the process
4
- * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-agent.ts
5
- * owns the spawn and records this run in the session ledger; opencode mints its own session id
4
+ * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-spawn.ts
5
+ * spawns the run and records this run in the session ledger; opencode mints its own session id
6
6
  * (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
7
7
  * carry it.
8
8
  *
@@ -23,7 +23,7 @@ import path from "node:path";
23
23
  import { ensureParentDir } from "../../files/files.js";
24
24
  import { loadConfigSafe } from "../../config/config.js";
25
25
  import { resolvedBackendConfig } from "../../config/config-views.js";
26
- import { backendBaseEnv, withNotesPath } from "../../process/run-marker.js";
26
+ import { backendRunEnv } from "../../process/run-marker.js";
27
27
  import { resolveBinForBackend } from "../../pi/pi-bin.js";
28
28
  import { opencodeDbPath, teamRootOf } from "../../paths.js";
29
29
  import { resumableSession } from "../session-ledger.js";
@@ -189,7 +189,7 @@ export const opencodeBackend = {
189
189
  // The notes path rides beside the run marker, as the pi and claude adapters' envs do
190
190
  // (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes` command
191
191
  // on PATH, so OpenCode configures no tumwater MCP server for notes.
192
- const base = withNotesPath(backendBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
192
+ const base = backendRunEnv(run);
193
193
  const env = opencodeEnv(base, { dbPath, configContent });
194
194
  const { sessionId } = resolveOpencodeSession(run, env);
195
195
  // The thinking variant (part 6d/7): ride the catalog's effort levels for the resolved
@@ -18,7 +18,7 @@
18
18
  import fs from "node:fs";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
21
- import { readTextOrNull } from "../../files/files.js";
21
+ import { readTextOrNull, xdgConfigHomeDir } from "../../files/files.js";
22
22
  import { isJsonObject } from "../../files/json-object.js";
23
23
  /** The operator's global opencode config lives in `$XDG_CONFIG_HOME/opencode/` (or
24
24
  * `~/.config/opencode/`), in `config.json`, `opencode.json` or `opencode.jsonc`. */
@@ -123,7 +123,7 @@ function stripJsonc(src) {
123
123
  * parse; doctor reports a broken one in part 7/7. `homeDir` is the operator's home directory;
124
124
  * the process's own `XDG_CONFIG_HOME` env, when set, wins over `~/.config` (the XDG default). */
125
125
  export function operatorMcpNames(env, homeDir) {
126
- const configHome = env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
126
+ const configHome = xdgConfigHomeDir(env, homeDir);
127
127
  const dir = path.join(configHome, "opencode");
128
128
  for (const file of OPERATOR_CONFIG_FILES) {
129
129
  const text = readTextOrNull(path.join(dir, file));