tumwater 0.2.0 → 0.4.0

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 (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -0,0 +1,36 @@
1
+ /** List the paths an `apply_patch` text writes, from the patch text alone (plans/
2
+ * codex-write-guard.md "The hook (parts 1–2)"; PLANS.md "Write guard on Codex, part 1/9").
3
+ * Pure: no filesystem, no imports beyond node built-ins, so a later part can call it from the
4
+ * hook without dragging in a registry or subprocess.
5
+ *
6
+ * Codex's PreToolUse payload carries the patch text, not a path, and a patch is applied even
7
+ * when it is truncated or its `*** Begin Patch` / `*** End Patch` markers are missing — so this
8
+ * scans every line, marker or not, and trusts only lines that begin at column 0 with a header.
9
+ * A hunk line (`+`, `-`, a space, `@@`) never counts as a header even when its text reads like
10
+ * one, because a hunk prefix shifts `*** ` off column 0. Paths are kept as written; the caller
11
+ * resolves relative ones against the run's directory. */
12
+ /** Headers that name a path the patch writes, in the order a line may spell them. */
13
+ const HEADERS = [
14
+ "*** Add File: ",
15
+ "*** Delete File: ",
16
+ "*** Update File: ",
17
+ "*** Move to: ",
18
+ ];
19
+ /** The paths an `apply_patch` text writes, in first-appearance order, without duplicates. */
20
+ export function patchWritePaths(patch) {
21
+ const seen = new Set();
22
+ const paths = [];
23
+ for (const line of patch.split("\n")) {
24
+ for (const header of HEADERS) {
25
+ if (line.startsWith(header)) {
26
+ const path = line.slice(header.length).replace(/[\s\r]+$/, "");
27
+ if (path !== "" && !seen.has(path)) {
28
+ seen.add(path);
29
+ paths.push(path);
30
+ }
31
+ break;
32
+ }
33
+ }
34
+ }
35
+ return paths;
36
+ }
@@ -0,0 +1,179 @@
1
+ /** The codex adapter's rollout reader (plans/codex-backend.md "The rollout read that
2
+ * completes it"). `codex exec --json`'s stream reports usage only once, at the end, counting
3
+ * the WHOLE thread (finding 4) — a resume would otherwise charge its earlier run to the follow-
4
+ * up. The authoritative per-run figures live in `$CODEX_HOME/sessions/…/rollout-*-<id>.jsonl`,
5
+ * Codex's own JSONL record of a session, where each run's `task_started` .. `task_complete`
6
+ * block carries its own `token_usage_record`, `token_count` (the rate limits) and
7
+ * `turn_context` (the model that actually ran). This module reads just that block: what the
8
+ * stream lacks — this run's own usage, the billing and the two usage windows.
9
+ *
10
+ * Pure over text (plus one filesystem walk for `findRollout`): no spawn, no billing math, no
11
+ * session state. Part 7/9's adapter wires the lines into the canonical stream in `afterRun`.
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { isJsonObject, nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
16
+ import { windowTypeName } from "./codex-stream.js";
17
+ function str(value) {
18
+ return typeof value === "string" ? value : undefined;
19
+ }
20
+ function finite(value) {
21
+ return nonNegativeNumber(value, undefined);
22
+ }
23
+ /** The rate-limit status of one window: `rejected` when the account hit it outright,
24
+ * `allowed_warning` when it is at or past 90% full, else `allowed` (design §5, decision 7). */
25
+ function windowStatus(windowType, rateLimitReachedType, usedPercent) {
26
+ if (rateLimitReachedType === windowType)
27
+ return "rejected";
28
+ if (usedPercent !== undefined && usedPercent >= 100)
29
+ return "rejected";
30
+ if (usedPercent !== undefined && usedPercent >= 90)
31
+ return "allowed_warning";
32
+ return "allowed";
33
+ }
34
+ /** Read the records of ONE turn out of a rollout's text. A rollout is one file per thread that
35
+ * appends a new `task_started` block per run, so the adapter's `afterRun` read (which knows
36
+ * only the thread id that names the file, never a turn id) reads the LAST block — everything
37
+ * after the last `task_started`. `sinceTurnStart` (optional) selects a specific turn's block
38
+ * instead, which pins per-turn isolation: the thread total hides a resume's own figures
39
+ * (finding 4). `requestedModel` is the `-m` tumwater passed; a turn whose `turn_context.model`
40
+ * differs reports the switch. */
41
+ export function rolloutLines(text, opts) {
42
+ const { sinceTurnStart, requestedModel } = opts;
43
+ let currentTurn;
44
+ let sawTurn = false;
45
+ let lastUsage;
46
+ let lastRateLimits;
47
+ let model;
48
+ let planType;
49
+ for (const line of text.split("\n")) {
50
+ if (!line.trim())
51
+ continue;
52
+ const parsed = parseJsonObject(line);
53
+ if (!parsed)
54
+ continue; // a torn or foreign line belongs to no turn
55
+ const payload = objectOrEmpty(parsed.payload);
56
+ // `task_started`, `task_complete` and `token_count` come wrapped in an `event_msg` whose
57
+ // `payload.type` discriminates; `turn_context` and `token_usage_record` name themselves in
58
+ // the outer `type`. Read either.
59
+ const ptype = str(parsed.type) === "event_msg" ? str(payload.type) : str(parsed.type);
60
+ if (ptype === "task_started") {
61
+ currentTurn = str(payload.turn_id);
62
+ // Each new task_started begins a fresh turn (a resume appends a second run to the same
63
+ // file), so the collectors restart at the block we are reading. With no turn id (the
64
+ // adapter's read) every task_started restarts them and the last block wins; with one,
65
+ // only the requested turn's does.
66
+ if (sinceTurnStart === undefined || currentTurn === sinceTurnStart) {
67
+ sawTurn = true;
68
+ lastUsage = undefined;
69
+ lastRateLimits = undefined;
70
+ model = undefined;
71
+ planType = undefined;
72
+ }
73
+ continue;
74
+ }
75
+ if (sinceTurnStart !== undefined && currentTurn !== sinceTurnStart)
76
+ continue;
77
+ switch (ptype) {
78
+ case "turn_context":
79
+ if (model === undefined)
80
+ model = str(payload.model);
81
+ break;
82
+ case "token_usage_record": {
83
+ const turnUsage = objectOrEmpty(payload.turn_token_usage);
84
+ if (isJsonObject(turnUsage) && (turnUsage.input_tokens !== undefined || turnUsage.output_tokens !== undefined)) {
85
+ lastUsage = turnUsage;
86
+ }
87
+ break;
88
+ }
89
+ case "token_count": {
90
+ const rateLimits = objectOrEmpty(payload.rate_limits);
91
+ lastRateLimits = rateLimits;
92
+ const plan = str(rateLimits.plan_type);
93
+ if (plan !== undefined)
94
+ planType = plan;
95
+ break;
96
+ }
97
+ default:
98
+ break;
99
+ }
100
+ }
101
+ // No matching task_started, or the turn's usage record is missing or malformed (a truncated
102
+ // or torn file): there is no reading. (currentTurn will have moved on to a later run's
103
+ // task_started in a resume-shared file; sawTurn — not currentTurn — is the real signal.)
104
+ if (!sawTurn || !isJsonObject(lastUsage)) {
105
+ return { lines: [] };
106
+ }
107
+ const lines = [];
108
+ const input = finite(lastUsage.input_tokens) ?? 0;
109
+ const cacheRead = finite(lastUsage.cached_input_tokens) ?? 0;
110
+ lines.push(JSON.stringify({
111
+ type: "run_usage",
112
+ input: Math.max(0, input - cacheRead),
113
+ output: finite(lastUsage.output_tokens) ?? 0,
114
+ cacheRead,
115
+ cacheWrite: finite(lastUsage.cache_write_input_tokens) ?? 0,
116
+ }));
117
+ if (planType !== undefined) {
118
+ // A reading with a plan_type bills on the subscription (finding 5; API-key login is
119
+ // refused, decision 5).
120
+ lines.push(JSON.stringify({ type: "billing", billing: "subscription" }));
121
+ }
122
+ // Two windows when present: the primary (5 h) and the secondary (weekly). A killed run's
123
+ // rollout has a token_usage_record but no token_count, so it yields run_usage with no
124
+ // rate_limit (finding 23).
125
+ if (lastRateLimits !== undefined) {
126
+ const reached = str(lastRateLimits.rate_limit_reached_type);
127
+ for (const key of ["primary", "secondary"]) {
128
+ const windowRaw = lastRateLimits[key];
129
+ if (!isJsonObject(windowRaw))
130
+ continue;
131
+ const minutes = finite(windowRaw.window_minutes);
132
+ if (minutes === undefined)
133
+ continue;
134
+ const typeName = windowTypeName(minutes);
135
+ const usedPercent = finite(windowRaw.used_percent);
136
+ lines.push(JSON.stringify({
137
+ type: "rate_limit",
138
+ rateLimitType: typeName,
139
+ status: windowStatus(typeName, reached, usedPercent),
140
+ ...(usedPercent !== undefined ? { utilization: usedPercent / 100 } : {}),
141
+ ...(finite(windowRaw.resets_at) !== undefined ? { resetsAt: finite(windowRaw.resets_at) } : {}),
142
+ }));
143
+ }
144
+ }
145
+ const switchedTo = model !== undefined && requestedModel !== undefined && model !== requestedModel ? model : undefined;
146
+ return { lines, ...(switchedTo !== undefined ? { switchedTo } : {}) };
147
+ }
148
+ /** Locate the rollout file of `$CODEX_HOME` that names the session `id`: the first
149
+ * `sessions/…/rollout-*-<id>.jsonl` under `home`, or null. It takes no start date and does no
150
+ * date math — the id match alone decides (finding 5: Codex's rollout name carries local time,
151
+ * so a UTC-derived bound can miss the file; the sessions tree is one directory per active day,
152
+ * so the walk stays small). */
153
+ export function findRollout(home, id) {
154
+ const sessionsRoot = path.join(home, "sessions");
155
+ if (!fs.existsSync(sessionsRoot))
156
+ return null;
157
+ const suffix = `-${id}.jsonl`;
158
+ const stack = [sessionsRoot];
159
+ while (stack.length > 0) {
160
+ const dir = stack.pop();
161
+ let entries;
162
+ try {
163
+ entries = fs.readdirSync(dir, { withFileTypes: true });
164
+ }
165
+ catch {
166
+ continue; // an unreadable dir is simply no match, never a failure
167
+ }
168
+ for (const entry of entries) {
169
+ const full = path.join(dir, entry.name);
170
+ if (entry.isDirectory()) {
171
+ stack.push(full);
172
+ }
173
+ else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix)) {
174
+ return full;
175
+ }
176
+ }
177
+ }
178
+ return null;
179
+ }
@@ -0,0 +1,404 @@
1
+ /** The codex adapter's native-to-canonical stream translator (plans/codex-backend.md "The
2
+ * stream translator and the rollout read that completes it"). `codex exec --json` emits one
3
+ * JSON object per line whose vocabulary is nothing like pi's: `thread.started`, then item
4
+ * start/completed events (`agent_message`, `command_execution`, `file_change`, `error`), and
5
+ * finally `turn.completed` or `turn.failed`. Rather than teach ~fifteen readers a third
6
+ * dialect, every native line is translated here into pi's vocabulary — the canonical stream —
7
+ * so `PiStreamParser`, the raw log, the transcript, progress, and the GUI all stay unchanged.
8
+ *
9
+ * Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
10
+ * state (the open tool calls, the turn's usage fallback, the unmapped native types seen), so
11
+ * call it once per run. Unmapped native types, the distinct text of `error` items, and a
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
14
+ * after the run. `sessionId()` reports the thread id once `thread.started` has named it.
15
+ */
16
+ import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
17
+ import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
18
+ import { secondsUntil } from "../../text/datetime.js";
19
+ /** How codex wraps every shell command — the prefix to strip (with its quoting) so the
20
+ * canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
21
+ const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
22
+ /** A top-level `error` line that is Codex's own WebSocket/HTTPS reconnect ("Reconnecting...
23
+ * n/m (reason)") — canonical `auto_retry_start`. */
24
+ const RECONNECT = /^Reconnecting\.\.\. (\d+)\/(\d+) /;
25
+ /** The item types dropped silently, with no canonical event and no `warnings()` entry
26
+ * (design §5's `item.*` row): `todo_list` and `web_search`. */
27
+ 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
+ /** Strip codex's `/bin/zsh -lc '<cmd>'` (or `bash -lc`) wrapper and its quoting, returning
32
+ * the model's own command — the argument suite-rerun detection matches. Passes a command the
33
+ * capture did not wrap through unchanged. */
34
+ function innerCommand(command) {
35
+ const m = SHELL_WRAPPER.exec(command);
36
+ return m ? m[1] : command;
37
+ }
38
+ /** The canonical window type name for a window-minutes value (shared with the rollout reader
39
+ * in codex-rollout.ts): Codex's 300-minute window reads as `five_hour`, its 10080-minute as
40
+ * `seven_day`, so a reserve reads the same on claude and codex (decision 7). */
41
+ export function windowTypeName(windowMinutes) {
42
+ if (windowMinutes === 300)
43
+ return "five_hour";
44
+ if (windowMinutes === 10080)
45
+ return "seven_day";
46
+ return `${windowMinutes}_minute`;
47
+ }
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 = {};
57
+ 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);
63
+ // The context-window signal is tested BEFORE the generic 400 `invalid_request_error`
64
+ // branch (as claude's classifyResult tests "prompt is too long" before CONFIG_STATUS): a
65
+ // `context_window_exceeded` code, or a 400 whose message names the context window, is
66
+ // `contextExceeded`, never `configError`.
67
+ const namesContextWindow = (t) => t !== undefined &&
68
+ /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
+ }
73
+ if (code === "usage_limit_reached" || code === "rate_limit_exceeded" || status === 429) {
74
+ // Which window the account hit, from the error's own `rate_limit_reached_type` (else the
75
+ // five-hour window, the one Plus exhausts first). The rejected reading's wait comes from
76
+ // `resets_at`.
77
+ const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
78
+ const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
79
+ 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 = {
84
+ type: "rate_limit",
85
+ rateLimitType: typeof named === "string" && /_minute$|_hour$|_day$/.test(named)
86
+ ? named
87
+ : windowTypeName(minutes ?? 300),
88
+ status: "rejected",
89
+ utilization: 1,
90
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
91
+ ...(retryAfter !== undefined ? { retryAfterSeconds: retryAfter } : {}),
92
+ };
93
+ return kind;
94
+ }
95
+ if (code === "usage_not_included") {
96
+ kind.kind = "configError";
97
+ return kind;
98
+ }
99
+ if (status !== undefined && SERVER_STATUS.has(status)) {
100
+ kind.kind = "transientBackend";
101
+ kind.backendKind = "server";
102
+ return kind;
103
+ }
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 ?? "")) {
107
+ kind.kind = "configError";
108
+ return kind;
109
+ }
110
+ if (status === 401) {
111
+ kind.kind = "configError";
112
+ return kind;
113
+ }
114
+ if (status === 400 && errType === "invalid_request_error") {
115
+ // A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
116
+ kind.kind = "configError";
117
+ return kind;
118
+ }
119
+ return kind;
120
+ }
121
+ /** Build a fresh per-run codex translator. `cwd` (optional) lands on the canonical `session`
122
+ * event (design §5's `session {id, cwd}`) so part 7/9's adapter can stamp the run's own
123
+ * directory; when omitted the session carries no `cwd`, like the opencode translator. */
124
+ export function codexTranslator(opts = {}) {
125
+ const bashLimit = outputLimits(opts.contextWindow).bash;
126
+ const cwd = opts.cwd;
127
+ let sessionIdValue;
128
+ let sawTurnCompleted = false;
129
+ let sawTurnFailed = false;
130
+ /** Tool calls open on this run, keyed by the item id that started them: command_execution
131
+ * and file_change items. A call still open when the turn ends was abandoned (finding 19).
132
+ * The value is the item type, so the abandonment close can name a command vs an edit. */
133
+ const openCalls = new Map();
134
+ let warnedAbandoned = false;
135
+ /** The last non-reconnecting top-level `error` message, held as the fallback text for a
136
+ * following `turn.failed` whose own message is absent (design §5). */
137
+ let pendingFailureText;
138
+ let turnUsage;
139
+ const seenEvents = new Set();
140
+ const seenItems = new Set();
141
+ const warnings = new Set();
142
+ const recordEvent = (type) => {
143
+ if (seenEvents.has(type))
144
+ return;
145
+ seenEvents.add(type);
146
+ warnings.add(type);
147
+ };
148
+ const recordItem = (item) => {
149
+ const key = `item:${typeof item.type === "string" ? item.type : "unknown"}`;
150
+ if (seenItems.has(key))
151
+ return;
152
+ seenItems.add(key);
153
+ warnings.add(key);
154
+ };
155
+ /** Record one distinct `error` item message as a run warning (never a failure). */
156
+ const recordErrorItem = (message) => {
157
+ if (warnings.has(message))
158
+ return;
159
+ warnings.add(message);
160
+ };
161
+ /** Close every still-open tool call as abandoned (the turn ended while they ran, finding
162
+ * 19): the command's open `sleep` never completed. Returns the canonical close lines.
163
+ * The `codex_command_abandoned` warning is recorded only when an open `command_execution`
164
+ * was actually closed (design §5), so a clean run's `warnings()` stays empty and a
165
+ * file_change abandoned on its own does not claim a command was. */
166
+ const abandonOpenCalls = () => {
167
+ const out = [];
168
+ let abandonedCommand = false;
169
+ for (const [id, type] of openCalls) {
170
+ openCalls.delete(id);
171
+ if (type === "command_execution")
172
+ abandonedCommand = true;
173
+ out.push(JSON.stringify({
174
+ type: "tool_execution_end",
175
+ toolCallId: id,
176
+ isError: true,
177
+ ...(type === "command_execution"
178
+ ? { result: "abandoned: the run ended while this command was still running" }
179
+ : {}),
180
+ }));
181
+ }
182
+ if (abandonedCommand && !warnedAbandoned) {
183
+ warnedAbandoned = true;
184
+ warnings.add("codex_command_abandoned");
185
+ }
186
+ return out;
187
+ };
188
+ const translate = (line) => {
189
+ const parsed = parseJsonObject(line);
190
+ if (!parsed)
191
+ return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
192
+ const event = parsed;
193
+ const type = typeof event.type === "string" ? event.type : "unknown";
194
+ const out = [];
195
+ 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
+ }
270
+ 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
+ }
369
+ default:
370
+ recordEvent(type);
371
+ return [];
372
+ }
373
+ };
374
+ return {
375
+ translate,
376
+ sessionId() {
377
+ return sessionIdValue;
378
+ },
379
+ turnCompletedUsage() {
380
+ return turnUsage;
381
+ },
382
+ flagUsagePartial() {
383
+ warnings.add("codex_usage_partial");
384
+ },
385
+ flagModelSwitched(requested, ran) {
386
+ warnings.add(`codex_model_switched requested=${requested} ran=${ran}`);
387
+ },
388
+ finalize() {
389
+ // The stream ended with neither turn.completed nor turn.failed (a SIGTERM the harness
390
+ // did not send, finding 12): report the turn never finished.
391
+ if (sawTurnCompleted || sawTurnFailed)
392
+ return [];
393
+ return [
394
+ JSON.stringify({
395
+ type: "message_end",
396
+ message: { role: "assistant", content: [], stopReason: "error", errorMessage: "codex exited without finishing its turn" },
397
+ }),
398
+ ];
399
+ },
400
+ warnings() {
401
+ return [...warnings];
402
+ },
403
+ };
404
+ }