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
@@ -10,12 +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";
18
+ import { secondsUntilUnixStamp } from "../../text/datetime.js";
19
+ import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
19
20
  /** How codex wraps every shell command — the prefix to strip (with its quoting) so the
20
21
  * canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
21
22
  const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
@@ -25,9 +26,6 @@ const RECONNECT = /^Reconnecting\.\.\. (\d+)\/(\d+) /;
25
26
  /** The item types dropped silently, with no canonical event and no `warnings()` entry
26
27
  * (design §5's `item.*` row): `todo_list` and `web_search`. */
27
28
  const DROPPED_ITEM_TYPES = new Set(["todo_list", "web_search"]);
28
- /** HTTP status classes that mean a transient backend failure (shared with the claude and
29
- * codex classifiers' reading of `codex exec`'s JSON error). */
30
- const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
31
29
  /** Strip codex's `/bin/zsh -lc '<cmd>'` (or `bash -lc`) wrapper and its quoting, returning
32
30
  * the model's own command — the argument suite-rerun detection matches. Passes a command the
33
31
  * capture did not wrap through unchanged. */
@@ -45,31 +43,31 @@ export function windowTypeName(windowMinutes) {
45
43
  return "seven_day";
46
44
  return `${windowMinutes}_minute`;
47
45
  }
48
- /** One codex `error` item's distinct message → a run warning; repeated identical messages are
49
- * warned once (finding 11). */
50
- /** How a `turn.failed` error message classifies into the canonical `tumwaterKind` (and, for a
51
- * transient backend, its kind), trusted by `PiStreamParser` before its text regexes. The inner
52
- * `message` is a JSON string carrying `status` and `error.type` / `error.code` (finding 11).
53
- * Returns the canonical lines to emit beside the closing `message_end`: a rejected rate-limit
54
- * reading when the error names a used-up window. */
55
- function classifyTurnFailed(parsed) {
56
- const kind = {};
46
+ /** Read the `turn.failed` inner JSON's four deciding fields. The inner `message` is a JSON
47
+ * string carrying `status` and `error.type` / `error.code` (finding 11). */
48
+ function readTurnFailedFields(parsed) {
57
49
  const error = objectOrEmpty(parsed.error);
58
- const code = typeof error.code === "string" ? error.code : undefined;
59
- const errType = typeof error.type === "string" ? error.type : undefined;
60
- const message = typeof error.message === "string" ? error.message : typeof parsed.message === "string" ? parsed.message : undefined;
61
- kind.errorMessage = message;
62
- const status = nonNegativeNumber(parsed.status, undefined);
50
+ return {
51
+ code: typeof error.code === "string" ? error.code : undefined,
52
+ errType: typeof error.type === "string" ? error.type : undefined,
53
+ message: typeof error.message === "string" ? error.message : typeof parsed.message === "string" ? parsed.message : undefined,
54
+ status: nonNegativeNumber(parsed.status, undefined),
55
+ };
56
+ }
57
+ /** Owns the context-window arm: a `context_window_exceeded` code, or a 400 whose message names
58
+ * the context window, classifies as `contextExceeded`. */
59
+ function isContextExceeded({ code, errType, message, status }) {
63
60
  // The context-window signal is tested BEFORE the generic 400 `invalid_request_error`
64
61
  // branch (as claude's classifyResult tests "prompt is too long" before CONFIG_STATUS): a
65
62
  // `context_window_exceeded` code, or a 400 whose message names the context window, is
66
63
  // `contextExceeded`, never `configError`.
67
64
  const namesContextWindow = (t) => t !== undefined &&
68
65
  /context window|context length|too long|exceeds? (the )?(model'?s )?context|prompt is too long/i.test(t);
69
- if (code === "context_window_exceeded" || (status === 400 && errType === "invalid_request_error" && namesContextWindow(message))) {
70
- kind.kind = "contextExceeded";
71
- return kind;
72
- }
66
+ return code === "context_window_exceeded" || (status === 400 && errType === "invalid_request_error" && namesContextWindow(message));
67
+ }
68
+ /** Owns the transient rate-limit arm: a `usage_limit_reached` / `rate_limit_exceeded` code or a
69
+ * 429 returns the rejected reading naming the used-up window, else undefined. */
70
+ function rateLimitReading(parsed, { code, status }) {
73
71
  if (code === "usage_limit_reached" || code === "rate_limit_exceeded" || status === 429) {
74
72
  // Which window the account hit, from the error's own `rate_limit_reached_type` (else the
75
73
  // five-hour window, the one Plus exhausts first). The rejected reading's wait comes from
@@ -77,42 +75,75 @@ function classifyTurnFailed(parsed) {
77
75
  const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
78
76
  const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
79
77
  const resetsAt = nonNegativeNumber(parsed.resets_at, undefined);
80
- // secondsUntil folds the unix-seconds resets stamp (×1000 to epoch-ms), clamped at 0.
81
- const retryAfter = resetsAt === undefined ? undefined : secondsUntil(resetsAt * 1000, Date.now());
82
- kind.kind = "transientRateLimit";
83
- kind.rateLimit = {
78
+ return {
84
79
  type: "rate_limit",
85
80
  rateLimitType: typeof named === "string" && /_minute$|_hour$|_day$/.test(named)
86
81
  ? named
87
82
  : windowTypeName(minutes ?? 300),
88
83
  status: "rejected",
89
84
  utilization: 1,
90
- ...(resetsAt !== undefined ? { resetsAt } : {}),
91
- ...(retryAfter !== undefined ? { retryAfterSeconds: retryAfter } : {}),
85
+ // secondsUntilUnixStamp folds the unix-seconds resets stamp to epoch-ms, clamped at 0.
86
+ ...(resetsAt === undefined
87
+ ? {}
88
+ : { resetsAt, retryAfterSeconds: secondsUntilUnixStamp(resetsAt, Date.now()) }),
92
89
  };
93
- return kind;
94
90
  }
95
- if (code === "usage_not_included") {
96
- kind.kind = "configError";
91
+ return undefined;
92
+ }
93
+ /** Owns the used-up-account arm: `usage_not_included` is a permanent config error, never a
94
+ * transient retry. */
95
+ function isUsageNotIncluded({ code }) {
96
+ return code === "usage_not_included";
97
+ }
98
+ /** Owns the server-status arm: any `SERVER_TRANSIENT_STATUS` is a transient backend. */
99
+ function isTransientServer({ status }) {
100
+ return status !== undefined && SERVER_TRANSIENT_STATUS.has(status);
101
+ }
102
+ /** Owns the permanent-login arms: a reused/invalidated refresh token, a 401, and a 400
103
+ * `invalid_request_error` are permanent config errors. */
104
+ function isPermanentLogin({ message, status, errType }) {
105
+ // A refresh-token failure and an absent/invalid login are permanent config errors, which
106
+ // the doctor names with the login command (design §5).
107
+ if (/refresh_token_reused|invalidated refresh token/i.test(message ?? ""))
108
+ return true;
109
+ if (status === 401)
110
+ return true;
111
+ if (status === 400 && errType === "invalid_request_error") {
112
+ // A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ /** How a `turn.failed` error message classifies into the canonical `tumwaterKind` (and, for a
118
+ * transient backend, its kind), trusted by `PiStreamParser` before its text regexes. The inner
119
+ * `message` is a JSON string carrying `status` and `error.type` / `error.code` (finding 11).
120
+ * Returns the canonical lines to emit beside the closing `message_end`: a rejected rate-limit
121
+ * reading when the error names a used-up window. The body is an ordered composition; each arm
122
+ * lives in its named helper: `readTurnFailedFields` (the field reads), `isContextExceeded`,
123
+ * `rateLimitReading`, `isUsageNotIncluded`, `isTransientServer` and `isPermanentLogin`. */
124
+ function classifyTurnFailed(parsed) {
125
+ const fields = readTurnFailedFields(parsed);
126
+ const kind = { errorMessage: fields.message };
127
+ if (isContextExceeded(fields)) {
128
+ kind.kind = "contextExceeded";
97
129
  return kind;
98
130
  }
99
- if (status !== undefined && SERVER_STATUS.has(status)) {
100
- kind.kind = "transientBackend";
101
- kind.backendKind = "server";
131
+ const rateLimit = rateLimitReading(parsed, fields);
132
+ if (rateLimit !== undefined) {
133
+ kind.kind = "transientRateLimit";
134
+ kind.rateLimit = rateLimit;
102
135
  return kind;
103
136
  }
104
- // A refresh-token failure and an absent/invalid login are permanent config errors, which
105
- // the doctor names with the login command (design §5).
106
- if (/refresh_token_reused|invalidated refresh token/i.test(message ?? "")) {
137
+ if (isUsageNotIncluded(fields)) {
107
138
  kind.kind = "configError";
108
139
  return kind;
109
140
  }
110
- if (status === 401) {
111
- kind.kind = "configError";
141
+ if (isTransientServer(fields)) {
142
+ kind.kind = "transientBackend";
143
+ kind.backendKind = "server";
112
144
  return kind;
113
145
  }
114
- if (status === 400 && errType === "invalid_request_error") {
115
- // A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
146
+ if (isPermanentLogin(fields)) {
116
147
  kind.kind = "configError";
117
148
  return kind;
118
149
  }
@@ -185,187 +216,210 @@ export function codexTranslator(opts = {}) {
185
216
  }
186
217
  return out;
187
218
  };
219
+ /** The `thread.started` arm: announces the session and the agent start, or records the
220
+ * unmapped event once when the thread id is missing. */
221
+ const handleThreadStarted = (event) => {
222
+ const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
223
+ if (id === undefined) {
224
+ recordEvent("thread.started");
225
+ return [];
226
+ }
227
+ sessionIdValue = id;
228
+ const out = [];
229
+ out.push(JSON.stringify({
230
+ type: "session",
231
+ id,
232
+ ...(cwd !== undefined ? { cwd } : {}),
233
+ }));
234
+ out.push(JSON.stringify({ type: "agent_start" }));
235
+ return out;
236
+ };
237
+ /** The `turn.completed` arm: folds the usage block and abandons the open calls. */
238
+ const handleTurnCompleted = (event) => {
239
+ sawTurnCompleted = true;
240
+ const usage = objectOrEmpty(event.usage);
241
+ turnUsage =
242
+ usage.input_tokens !== undefined || usage.output_tokens !== undefined
243
+ ? {
244
+ input: nonNegativeNumber(usage.input_tokens, 0),
245
+ cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
246
+ cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
247
+ output: nonNegativeNumber(usage.output_tokens, 0),
248
+ }
249
+ : undefined;
250
+ return abandonOpenCalls();
251
+ };
252
+ /** The `turn.failed` arm: classifies the inner-JSON error and closes the turn. */
253
+ const handleTurnFailed = (event) => {
254
+ sawTurnFailed = true;
255
+ // The error message is itself a JSON string (finding 11); fall back to the held
256
+ // top-level error text when it is not.
257
+ const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
258
+ ? event.error.message
259
+ : undefined;
260
+ const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
261
+ const cls = inner ? classifyTurnFailed(inner) : {};
262
+ const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
263
+ const closes = abandonOpenCalls();
264
+ const message = { role: "assistant", content: [], stopReason: "error" };
265
+ if (errorMessage !== undefined)
266
+ message.errorMessage = errorMessage;
267
+ const end = {
268
+ type: "message_end",
269
+ message,
270
+ ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
271
+ ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
272
+ };
273
+ if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
274
+ end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
275
+ return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
276
+ };
277
+ /** The top-level `error` arm: a reconnect text emits an `auto_retry_start`; any other
278
+ * message is held as the next `turn.failed`'s fallback text. */
279
+ const handleErrorLine = (event) => {
280
+ const message = typeof event.message === "string" ? event.message : undefined;
281
+ const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
282
+ if (reconnect) {
283
+ return [
284
+ JSON.stringify({
285
+ type: "auto_retry_start",
286
+ attempt: Number(reconnect[1]),
287
+ maxAttempts: Number(reconnect[2]),
288
+ errorMessage: message,
289
+ }),
290
+ ];
291
+ }
292
+ // Other top-level `error` lines are not fatal (design §5): held as the fallback text
293
+ // for a following `turn.failed`.
294
+ if (message !== undefined)
295
+ pendingFailureText = message;
296
+ return [];
297
+ };
298
+ /** The `item.started`/`item.completed` arm: reads the item and delegates by type to the
299
+ * item handlers, dropping the silent types and warning once on the unknown tail. */
300
+ const handleItem = (event, started) => {
301
+ const item = objectOrEmpty(event.item);
302
+ const itemType = typeof item.type === "string" ? item.type : "unknown";
303
+ if (itemType === "command_execution")
304
+ return handleCommandItem(item, started);
305
+ if (itemType === "file_change")
306
+ return handleFileChangeItem(item, started);
307
+ if (itemType === "error")
308
+ return handleErrorItem(item);
309
+ if (itemType === "agent_message")
310
+ return handleAgentMessageItem(item);
311
+ if (DROPPED_ITEM_TYPES.has(itemType))
312
+ return [];
313
+ // mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
314
+ // once-per-type warning only (design §5).
315
+ recordItem(item);
316
+ return [];
317
+ };
318
+ /** The `command_execution` item arm: opens or closes the bash call, capping the result. */
319
+ const handleCommandItem = (item, started) => {
320
+ const command = typeof item.command === "string" ? item.command : "";
321
+ const out = [];
322
+ if (started) {
323
+ const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
324
+ openCalls.set(id, "command_execution");
325
+ const inner = innerCommand(command);
326
+ out.push(JSON.stringify({
327
+ type: "message_end",
328
+ message: {
329
+ role: "assistant",
330
+ content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
331
+ },
332
+ }));
333
+ out.push(JSON.stringify({
334
+ type: "tool_execution_start",
335
+ toolCallId: id,
336
+ toolName: "bash",
337
+ args: { command: inner },
338
+ }));
339
+ }
340
+ else {
341
+ const id = typeof item.id === "string" ? item.id : "";
342
+ openCalls.delete(id);
343
+ const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
344
+ const exitCode = nonNegativeNumber(item.exit_code, undefined);
345
+ out.push(JSON.stringify({
346
+ type: "tool_execution_end",
347
+ toolCallId: id,
348
+ isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
349
+ result: boundText(aggregated, bashLimit),
350
+ }));
351
+ }
352
+ return out;
353
+ };
354
+ /** The `file_change` item arm: mirrors each change as an `edit` tool call. */
355
+ const handleFileChangeItem = (item, started) => {
356
+ const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
357
+ const id = typeof item.id === "string" ? item.id : "";
358
+ const out = [];
359
+ if (!started) {
360
+ const isError = typeof item.status === "string" && item.status !== "completed";
361
+ for (let i = 0; i < changes.length; i++) {
362
+ const callId = `${id}-${i}`;
363
+ openCalls.delete(callId);
364
+ out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
365
+ }
366
+ return out;
367
+ }
368
+ const content = changes.map((c, i) => ({
369
+ type: "toolCall",
370
+ id: `${id}-${i}`,
371
+ name: "edit",
372
+ arguments: {
373
+ path: typeof c.path === "string" ? c.path : undefined,
374
+ kind: typeof c.kind === "string" ? c.kind : undefined,
375
+ },
376
+ }));
377
+ if (content.length === 0)
378
+ return out; // no changes to mirror, nothing else to emit
379
+ out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
380
+ for (let i = 0; i < content.length; i++) {
381
+ const callId = `${id}-${i}`;
382
+ openCalls.set(callId, "file_change");
383
+ out.push(JSON.stringify({
384
+ type: "tool_execution_start",
385
+ toolCallId: callId,
386
+ toolName: "edit",
387
+ args: content[i].arguments,
388
+ }));
389
+ }
390
+ return out;
391
+ };
392
+ /** The `item.type === "error"` arm: a non-fatal notice warned once per distinct message. */
393
+ const handleErrorItem = (item) => {
394
+ const message = typeof item.message === "string" ? item.message : undefined;
395
+ if (message !== undefined)
396
+ recordErrorItem(message);
397
+ return [];
398
+ };
399
+ /** The `agent_message` arm: the model's reply as one canonical `message_end`. */
400
+ const handleAgentMessageItem = (item) => {
401
+ const text = typeof item.text === "string" ? item.text : "";
402
+ return [
403
+ JSON.stringify({
404
+ type: "message_end",
405
+ message: { role: "assistant", content: [{ type: "text", text }] },
406
+ }),
407
+ ];
408
+ };
188
409
  const translate = (line) => {
189
410
  const parsed = parseJsonObject(line);
190
411
  if (!parsed)
191
412
  return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
192
413
  const event = parsed;
193
414
  const type = typeof event.type === "string" ? event.type : "unknown";
194
- const out = [];
195
415
  switch (type) {
196
- case "thread.started": {
197
- const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
198
- if (id === undefined) {
199
- recordEvent(type);
200
- return [];
201
- }
202
- sessionIdValue = id;
203
- out.push(JSON.stringify({
204
- type: "session",
205
- id,
206
- ...(cwd !== undefined ? { cwd } : {}),
207
- }));
208
- out.push(JSON.stringify({ type: "agent_start" }));
209
- return out;
210
- }
211
- case "turn.started":
212
- return [];
213
- case "turn.completed": {
214
- sawTurnCompleted = true;
215
- const usage = objectOrEmpty(event.usage);
216
- turnUsage =
217
- usage.input_tokens !== undefined || usage.output_tokens !== undefined
218
- ? {
219
- input: nonNegativeNumber(usage.input_tokens, 0),
220
- cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
221
- cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
222
- output: nonNegativeNumber(usage.output_tokens, 0),
223
- }
224
- : undefined;
225
- return abandonOpenCalls();
226
- }
227
- case "turn.failed": {
228
- sawTurnFailed = true;
229
- // The error message is itself a JSON string (finding 11); fall back to the held
230
- // top-level error text when it is not.
231
- const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
232
- ? event.error.message
233
- : undefined;
234
- const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
235
- const cls = inner ? classifyTurnFailed(inner) : {};
236
- const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
237
- const closes = abandonOpenCalls();
238
- const message = { role: "assistant", content: [], stopReason: "error" };
239
- if (errorMessage !== undefined)
240
- message.errorMessage = errorMessage;
241
- const end = {
242
- type: "message_end",
243
- message,
244
- ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
245
- ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
246
- };
247
- if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
248
- end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
249
- return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
250
- }
251
- case "error": {
252
- const message = typeof event.message === "string" ? event.message : undefined;
253
- const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
254
- if (reconnect) {
255
- return [
256
- JSON.stringify({
257
- type: "auto_retry_start",
258
- attempt: Number(reconnect[1]),
259
- maxAttempts: Number(reconnect[2]),
260
- errorMessage: message,
261
- }),
262
- ];
263
- }
264
- // Other top-level `error` lines are not fatal (design §5): held as the fallback text
265
- // for a following `turn.failed`.
266
- if (message !== undefined)
267
- pendingFailureText = message;
268
- return [];
269
- }
416
+ case "thread.started": return handleThreadStarted(event);
417
+ case "turn.started": return [];
418
+ case "turn.completed": return handleTurnCompleted(event);
419
+ case "turn.failed": return handleTurnFailed(event);
420
+ case "error": return handleErrorLine(event);
270
421
  case "item.started":
271
- case "item.completed": {
272
- const item = objectOrEmpty(event.item);
273
- const itemType = typeof item.type === "string" ? item.type : "unknown";
274
- const started = type === "item.started";
275
- if (itemType === "command_execution") {
276
- const command = typeof item.command === "string" ? item.command : "";
277
- if (started) {
278
- const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
279
- openCalls.set(id, "command_execution");
280
- const inner = innerCommand(command);
281
- out.push(JSON.stringify({
282
- type: "message_end",
283
- message: {
284
- role: "assistant",
285
- content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
286
- },
287
- }));
288
- out.push(JSON.stringify({
289
- type: "tool_execution_start",
290
- toolCallId: id,
291
- toolName: "bash",
292
- args: { command: inner },
293
- }));
294
- }
295
- else {
296
- const id = typeof item.id === "string" ? item.id : "";
297
- openCalls.delete(id);
298
- const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
299
- const exitCode = nonNegativeNumber(item.exit_code, undefined);
300
- out.push(JSON.stringify({
301
- type: "tool_execution_end",
302
- toolCallId: id,
303
- isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
304
- result: boundText(aggregated, bashLimit),
305
- }));
306
- }
307
- return out;
308
- }
309
- if (itemType === "file_change") {
310
- const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
311
- const id = typeof item.id === "string" ? item.id : "";
312
- if (!started) {
313
- const isError = typeof item.status === "string" && item.status !== "completed";
314
- for (let i = 0; i < changes.length; i++) {
315
- const callId = `${id}-${i}`;
316
- openCalls.delete(callId);
317
- out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
318
- }
319
- return out;
320
- }
321
- const content = changes.map((c, i) => ({
322
- type: "toolCall",
323
- id: `${id}-${i}`,
324
- name: "edit",
325
- arguments: {
326
- path: typeof c.path === "string" ? c.path : undefined,
327
- kind: typeof c.kind === "string" ? c.kind : undefined,
328
- },
329
- }));
330
- if (content.length === 0)
331
- return out; // no changes to mirror, nothing else to emit
332
- out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
333
- for (let i = 0; i < content.length; i++) {
334
- const callId = `${id}-${i}`;
335
- openCalls.set(callId, "file_change");
336
- out.push(JSON.stringify({
337
- type: "tool_execution_start",
338
- toolCallId: callId,
339
- toolName: "edit",
340
- args: content[i].arguments,
341
- }));
342
- }
343
- return out;
344
- }
345
- if (itemType === "error") {
346
- // An error item is a non-fatal notice (finding 11); warned once per distinct message.
347
- const message = typeof item.message === "string" ? item.message : undefined;
348
- if (message !== undefined)
349
- recordErrorItem(message);
350
- return [];
351
- }
352
- if (itemType === "agent_message") {
353
- // The model's reply: one canonical `message_end` with one text block.
354
- const text = typeof item.text === "string" ? item.text : "";
355
- return [
356
- JSON.stringify({
357
- type: "message_end",
358
- message: { role: "assistant", content: [{ type: "text", text }] },
359
- }),
360
- ];
361
- }
362
- if (DROPPED_ITEM_TYPES.has(itemType))
363
- return [];
364
- // mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
365
- // once-per-type warning only (design §5).
366
- recordItem(item);
367
- return [];
368
- }
422
+ case "item.completed": return handleItem(event, type === "item.started");
369
423
  default:
370
424
  recordEvent(type);
371
425
  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
  }