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
@@ -1,17 +1,16 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
- import { resolvedAgentConfig } from "../config/config-views.js";
4
- import { envPath, findOnPath } from "../files/files.js";
5
- /** Which agent binary the harness spawns and where that value came from — the per-agent
2
+ import { resolvedBackendConfig } from "../config/config-views.js";
3
+ import { envPath, findOnPath, isExecutableFile } from "../files/files.js";
4
+ /** Which backend binary the harness spawns and where that value came from — the per-backend
6
5
  * resolution `TUMWATER_<ID>_BIN` → `backends.<id>.bin` (pi's legacy `agentBin` alias) → the
7
6
  * backends' defaults (plans/agent-backends.md "Config"; plans/portability.md §5/7), the
8
7
  * executability rule the preflight askers share, and the missing-binary wording they quote.
9
8
  * The spawn (src/agent/run-agent.ts) resolves through each backend's resolveBin; the startup
10
- * gate (gates/startup-gate.ts) walks every enabled seam's agent through the same resolution and
11
- * executability rule, and doctor (doctor/doctor-checks.ts) asks them for every agent in use
12
- * (agentsInUse, the startup gate's rule). Each surface quotes this module's wording, so the
9
+ * gate (gates/startup-gate.ts) walks every enabled seam's backend through the same resolution and
10
+ * executability rule, and doctor (doctor/doctor-checks.ts) asks them for every backend in use
11
+ * (backendsInUse, the startup gate's rule). Each surface quotes this module's wording, so the
13
12
  * executability verdict and the pi text cannot drift between them. Split out of
14
- * gates/readiness.ts, which keeps the repo preconditions' shared wording: the agent binary is
13
+ * gates/readiness.ts, which keeps the repo preconditions' shared wording: the backend binary is
15
14
  * the backends' own concern, so its resolution lives beside the spawn instead of in the gate
16
15
  * layer. */
17
16
  const PI_MISSING_MESSAGE = "pi not found on PATH — install it (https://github.com/badlogic/pi-mono) or add its bin directory to your PATH";
@@ -21,7 +20,7 @@ const PI_MISSING_MESSAGE = "pi not found on PATH — install it (https://github.
21
20
  * default; claude's adapter passes TUMWATER_CLAUDE_BIN), and `configKey` is the tumwater.json
22
21
  * key the "config" source names (pi's legacy `agentBin`, or `backends.<id>.bin`), so a claude
23
22
  * failure names its own var and key rather than pi's. */
24
- export function agentBinSourceLabel(source, binEnv = "TUMWATER_PI_BIN", configKey = "agentBin") {
23
+ export function backendBinSourceLabel(source, binEnv = "TUMWATER_PI_BIN", configKey = "agentBin") {
25
24
  switch (source) {
26
25
  case "env":
27
26
  return binEnv;
@@ -31,7 +30,7 @@ export function agentBinSourceLabel(source, binEnv = "TUMWATER_PI_BIN", configKe
31
30
  return "the PATH default";
32
31
  }
33
32
  }
34
- /** The missing-binary message for a resolved agent binary. The default source keeps
33
+ /** The missing-binary message for a resolved backend binary. The default source keeps
35
34
  * PI_MISSING_MESSAGE byte-identical (pinned by test/cli.test.ts and test/doctor.test.ts);
36
35
  * a configured source gains the resolved value and where it came from, so a wrong
37
36
  * `agentBin` never reads as "pi is not installed" — and keeps the install hint, since a
@@ -39,68 +38,60 @@ export function agentBinSourceLabel(source, binEnv = "TUMWATER_PI_BIN", configKe
39
38
  export function piMissingMessage(resolved) {
40
39
  if (resolved.source === "default")
41
40
  return PI_MISSING_MESSAGE;
42
- const from = agentBinSourceLabel(resolved.source);
41
+ const from = backendBinSourceLabel(resolved.source);
43
42
  return `pi not found — resolved "${resolved.bin}" from ${from} is not an executable — install it (https://github.com/badlogic/pi-mono) or point ${from} at a working pi binary`;
44
43
  }
45
- /** The missing-binary message for any agent's resolved binary, used by the startup gate's walk
46
- * over every enabled seam's agent. pi delegates to piMissingMessage so its pinned wording is
47
- * untouched; any other agent names itself, the resolved value, and where it came from, so a
44
+ /** The missing-binary message for any backend's resolved binary, used by the startup gate's walk
45
+ * over every enabled seam's backend. pi delegates to piMissingMessage so its pinned wording is
46
+ * untouched; any other backend names itself, the resolved value, and where it came from, so a
48
47
  * wrong `backends.<id>.bin` or `TUMWATER_<ID>_BIN` never reads as a missing pi. */
49
- export function agentMissingMessage(resolved, agent) {
50
- if (agent === "pi")
48
+ export function backendMissingMessage(resolved, backend) {
49
+ if (backend === "pi")
51
50
  return piMissingMessage(resolved);
52
51
  if (resolved.source === "default") {
53
- return `${agent} not found on PATH — install it or add its bin directory to your PATH`;
52
+ return `${backend} not found on PATH — install it or add its bin directory to your PATH`;
54
53
  }
55
- const from = agentBinSourceLabelFor(agent, resolved.source);
56
- return `${agent} not found — resolved "${resolved.bin}" from ${from} is not an executable — install it or point ${from} at a working ${agent} binary`;
54
+ const from = backendBinSourceLabelFor(backend, resolved.source);
55
+ return `${backend} not found — resolved "${resolved.bin}" from ${from} is not an executable — install it or point ${from} at a working ${backend} binary`;
57
56
  }
58
- /** The environment variable that overrides one agent's binary: `TUMWATER_<ID>_BIN` with the id
59
- * uppercased — the convention resolveAgentBin and agentMissingMessage share. */
60
- function agentBinEnv(agent) {
61
- return `TUMWATER_${agent.toUpperCase()}_BIN`;
57
+ /** The environment variable that overrides one backend's binary: `TUMWATER_<ID>_BIN` with the id
58
+ * uppercased — the convention resolveBackendBin and backendMissingMessage share. */
59
+ function backendBinEnv(backend) {
60
+ return `TUMWATER_${backend.toUpperCase()}_BIN`;
62
61
  }
63
- /** The tumwater.json key that names one agent's binary: pi's legacy `agentBin`, or the
64
- * per-agent `backends.<id>.bin`. Shared by the missing-binary and spawn-error wording so a
62
+ /** The tumwater.json key that names one backend's binary: pi's legacy `agentBin`, or the
63
+ * per-backend `backends.<id>.bin`. Shared by the missing-binary and spawn-error wording so a
65
64
  * non-pi failure never names pi's key. */
66
- export function agentBinConfigKey(agent) {
67
- return agent === "pi" ? "agentBin" : `backends.${agent}.bin`;
65
+ export function backendBinConfigKey(backend) {
66
+ return backend === "pi" ? "agentBin" : `backends.${backend}.bin`;
68
67
  }
69
- /** The source label for one agent's resolved binary, deriving the env var and config key from
70
- * the agent id when the caller has no backend defaults in hand. pi keeps its pinned legacy
71
- * wording; any other agent names its own `TUMWATER_<ID>_BIN` and `backends.<id>.bin`. One home
72
- * for the convention agentMissingMessage and doctor's ok detail both read. */
73
- export function agentBinSourceLabelFor(agent, source) {
74
- return agent === "pi"
75
- ? agentBinSourceLabel(source)
76
- : agentBinSourceLabel(source, agentBinEnv(agent), agentBinConfigKey(agent));
68
+ /** The source label for one backend's resolved binary, deriving the env var and config key from
69
+ * the backend id when the caller has no backend defaults in hand. pi keeps its pinned legacy
70
+ * wording; any other backend names its own `TUMWATER_<ID>_BIN` and `backends.<id>.bin`. One home
71
+ * for the convention backendMissingMessage and doctor's ok detail both read. */
72
+ export function backendBinSourceLabelFor(backend, source) {
73
+ return backend === "pi"
74
+ ? backendBinSourceLabel(source)
75
+ : backendBinSourceLabel(source, backendBinEnv(backend), backendBinConfigKey(backend));
77
76
  }
78
- /** Is the resolved agent binary executable right now? Returns the usable path, or null when
77
+ /** Is the resolved backend binary executable right now? Returns the usable path, or null when
79
78
  * the startup gate should refuse to boot and doctor should fail its check. This is the whole
80
79
  * executability rule in one place so the two askers cannot drift: a path-shaped value (already
81
- * normalized against the process cwd by resolveAgentBin) is tested directly with
80
+ * normalized against the process cwd by resolveBackendBin) is tested directly with
82
81
  * accessSync(X_OK) and required to be a regular file — a directory passes X_OK, but can never
83
82
  * be spawned, so it is rejected here with exactly the file requirement findOnPath's bare-name
84
83
  * branch applies (a bin pointed at a directory would otherwise pass the preflight and die
85
84
  * mid-run at the spawn instead of failing the gate with the "not an executable" message). A
86
85
  * bare name resolves through PATH. `pathEnv` is injectable so tests need no PATH mutation —
87
86
  * the gate and doctor both default it to the process PATH. */
88
- export function findAgentBinary(resolved, pathEnv = envPath()) {
87
+ export function findBackendBinary(resolved, pathEnv = envPath()) {
89
88
  if (resolved.bin.includes("/")) {
90
- try {
91
- fs.accessSync(resolved.bin, fs.constants.X_OK); // Directories pass X_OK; require a file.
92
- if (fs.statSync(resolved.bin).isFile())
93
- return resolved.bin;
94
- return null;
95
- }
96
- catch {
97
- return null;
98
- }
89
+ return isExecutableFile(resolved.bin) ? resolved.bin : null;
99
90
  }
100
91
  return findOnPath(resolved.bin, pathEnv);
101
92
  }
102
- /** Normalize a path-shaped agent binary against the harness process's cwd NOW, at
103
- * resolution time: the spawn runs with each tick's worktree as cwd, so a relative value
93
+ /** Normalize a path-shaped backend binary against the harness process's cwd NOW, at
94
+ * resolution time: the spawn runs with each run's worktree as cwd, so a relative value
104
95
  * left as given would name a different file there than the run preflight and doctor (which
105
96
  * evaluate against the process cwd) had already accepted. An absolute path comes back
106
97
  * normalized; a bare name — no separator — is left for PATH resolution exactly as before.
@@ -111,10 +102,10 @@ function absBin(value) {
111
102
  }
112
103
  /** The precedence every backend shares, parameterized by its own env override and default
113
104
  * binary name: `<binEnv>` env → the configured entry's bin → `defaultBin`. An empty or
114
- * whitespace value falls through to the next source, so `TUMWATER_PI_BIN= tumwater run` cannot
105
+ * whitespace value falls through to the next source, so `TUMWATER_PI_BIN= tumwater start` cannot
115
106
  * wedge the team on a typo'd export. This is precedence only — resolvability is checked at the
116
- * preflight sites (cmdRun and doctor), never here. Private: the backends and the gate resolve
117
- * through resolveAgentBin, its one caller. */
107
+ * preflight sites (cmdStart and doctor), never here. Private: the backends and the gate resolve
108
+ * through resolveBackendBin, its one caller. */
118
109
  function resolveBinFrom(binEnv, defaultBin, configured) {
119
110
  const env = process.env[binEnv]?.trim();
120
111
  if (env)
@@ -124,21 +115,21 @@ function resolveBinFrom(binEnv, defaultBin, configured) {
124
115
  return { bin: absBin(cfg), source: "config" };
125
116
  return { bin: defaultBin, source: "default" };
126
117
  }
127
- /** Resolve which binary one agent spawns: `TUMWATER_<ID>_BIN` → the effective
128
- * `backends.<id>.bin` (config-views.ts resolvedAgentConfig, so pi's legacy `agentBin` still
129
- * names pi) → the agent's default. `agent` defaults to pi, the historical backend; the
118
+ /** Resolve which binary one backend spawns: `TUMWATER_<ID>_BIN` → the effective
119
+ * `backends.<id>.bin` (config-views.ts resolvedBackendConfig, so pi's legacy `agentBin` still
120
+ * names pi) → the backend's default. `backend` defaults to pi, the historical backend; the
130
121
  * backends pass their own id, binEnv and defaultBin, while the startup gate derives the
131
- * reserved agents' conventional env name and default binary when it walks every enabled seam. */
132
- export function resolveAgentBin(config, agent = "pi", defaults = {}) {
133
- const binEnv = defaults.binEnv ?? agentBinEnv(agent);
134
- const defaultBin = defaults.defaultBin ?? agent;
135
- return resolveBinFrom(binEnv, defaultBin, resolvedAgentConfig(config, agent).bin);
122
+ * reserved backends' conventional env name and default binary when it walks every enabled seam. */
123
+ export function resolveBackendBin(config, backend = "pi", defaults = {}) {
124
+ const binEnv = defaults.binEnv ?? backendBinEnv(backend);
125
+ const defaultBin = defaults.defaultBin ?? backend;
126
+ return resolveBinFrom(binEnv, defaultBin, resolvedBackendConfig(config, backend).bin);
136
127
  }
137
128
  /** The shared body of a backend's `resolveBin` adapter: resolve this backend's binary through
138
- * resolveAgentBin above using the backend's own id, binEnv and defaultBin — never pi's, so a
129
+ * resolveBackendBin above using the backend's own id, binEnv and defaultBin — never pi's, so a
139
130
  * claude failure names TUMWATER_CLAUDE_BIN/the claude default. The two registered backends
140
131
  * (pi-backend.ts, claude-backend.ts) each expose a one-line `resolveBin` delegating here, so
141
132
  * the call convention lives in one place and cannot drift between them. */
142
- export function resolveBackendBin(config, backend) {
143
- return resolveAgentBin(config, backend.id, { binEnv: backend.binEnv, defaultBin: backend.defaultBin });
133
+ export function resolveBinForBackend(config, backend) {
134
+ return resolveBackendBin(config, backend.id, { binEnv: backend.binEnv, defaultBin: backend.defaultBin });
144
135
  }
@@ -0,0 +1,53 @@
1
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
+ import { piListedModelsPath } from "../paths.js";
3
+ /** The whitespace-split rows of the table `pi --list-models` prints, header first. Null unless
4
+ * the first non-blank line is the `provider model …` header, so other text (a "No models
5
+ * matching" reply) is never taken for an empty table. */
6
+ export function parsePiModelTable(stdout) {
7
+ const rows = stdout.split("\n").map((line) => line.trim().split(/\s+/)).filter((t) => t[0] !== "");
8
+ const header = rows.shift();
9
+ if (header?.[0] !== "provider" || header[1] !== "model")
10
+ return null;
11
+ return { header, rows };
12
+ }
13
+ /** Read the `images` column of the table `pi --list-models` prints. Null unless the first
14
+ * non-blank line is the `provider model …` header and it has an `images` column. A row's
15
+ * `yes`/`no` maps to a boolean; any other value is skipped. */
16
+ export function parsePiModelImages(stdout) {
17
+ const table = parsePiModelTable(stdout);
18
+ if (table === null)
19
+ return null;
20
+ const { header, rows } = table;
21
+ const column = header.indexOf("images");
22
+ if (column < 0)
23
+ return null;
24
+ const images = {};
25
+ for (const row of rows) {
26
+ const [provider, model] = row;
27
+ if (model === undefined)
28
+ continue;
29
+ if (row[column] === "yes")
30
+ images[`${provider}/${model}`] = true;
31
+ else if (row[column] === "no")
32
+ images[`${provider}/${model}`] = false;
33
+ }
34
+ return images;
35
+ }
36
+ /** The saved listing, or null when the file is missing, torn, or has no `images` object. */
37
+ export function readPiListedModels(root) {
38
+ const saved = readJsonFile(piListedModelsPath(root));
39
+ const images = saved?.images;
40
+ if (typeof images !== "object" || images === null || Array.isArray(images))
41
+ return null;
42
+ return { checkedAt: typeof saved?.checkedAt === "number" ? saved.checkedAt : 0, images };
43
+ }
44
+ export function writePiListedModels(root, images, now = Date.now()) {
45
+ writeJsonAtomic(piListedModelsPath(root), { checkedAt: now, images });
46
+ }
47
+ /** Whether pi lists `provider/model` as reading images; null when unlisted or an argument is
48
+ * missing. No TTL: every doctor run that lists the models rewrites the file. */
49
+ export function piListedImageInput(listed, provider, model) {
50
+ if (!listed || provider === undefined || model === undefined)
51
+ return null;
52
+ return listed.images[`${provider}/${model}`] ?? null;
53
+ }
@@ -1,7 +1,7 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
- import { enabledRoleIds } from "../config/config.js";
4
- import { configForRole, resolverConfig, reviewConfig } from "../config/config-views.js";
3
+ import { enabledRoleIds, loadConfigSafe } from "../config/config.js";
4
+ import { configForRole, resolverConfig, resolvedBackendConfig, reviewConfig } from "../config/config-views.js";
5
5
  import { fallbackPair } from "../config/config-fallbacks.js";
6
6
  import { seamAgent } from "../config/model-selector.js";
7
7
  import { cachedByStat } from "../files/stat-cache.js";
@@ -11,6 +11,7 @@ import { expandHomeTilde } from "../files/files.js";
11
11
  import { declaredWindow } from "../pi-extension/context-window.js";
12
12
  import { piSettingsPath, readPiDefaultModel } from "./pi-settings.js";
13
13
  import { opencodeModelFree, opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
14
+ import { codexHome, readCodexCatalog } from "../agent/codex/codex-catalog.js";
14
15
  /** pi's model definitions — the custom providers and models they serve, with each model's
15
16
  * declared cost. This is where a local (free) team differs from an API one: unpriced or
16
17
  * zero-cost models never accumulate spend against the daily budget cap, so the badge can
@@ -103,8 +104,8 @@ export function readPiProviders(modelsPath) {
103
104
  * A missing cache, an unknown model, or an unusable `context` reads null, as an unknown pi
104
105
  * model does. */
105
106
  export function seamContextWindow(seam, projectRoot, modelsPath = piModelsPath()) {
106
- const agent = seamAgent(seam);
107
- switch (agent) {
107
+ const backend = seamAgent(seam);
108
+ switch (backend) {
108
109
  case "pi":
109
110
  let { provider, model } = seam;
110
111
  if (!provider || !model) {
@@ -121,6 +122,21 @@ export function seamContextWindow(seam, projectRoot, modelsPath = piModelsPath()
121
122
  if (seam.model === undefined)
122
123
  return null;
123
124
  return declaredWindow(readOpencodeCache(projectRoot).models[opencodeModelKey(seam.model)]?.context);
125
+ case "codex":
126
+ // The model catalog (codex-catalog.ts) holds the window part 8/9 parsed from
127
+ // `context_window × effective_context_window_percent / 100`, read from the team's codex
128
+ // home (backends.codex.configDir) at this project root — the same home the runs use. An
129
+ // unknown model, an absent home or a missing catalog reads null (declaredWindow's safe
130
+ // answer for an unknown window).
131
+ if (seam.model === undefined)
132
+ return null;
133
+ {
134
+ const { config } = loadConfigSafe(projectRoot);
135
+ const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
136
+ if (home === undefined)
137
+ return null;
138
+ return declaredWindow(readCodexCatalog(home).get(seam.model)?.contextWindow);
139
+ }
124
140
  default:
125
141
  return null;
126
142
  }
@@ -193,19 +209,36 @@ function accountUsesOverage(account) {
193
209
  * badge never reads n/a while spend it cannot see could still reach the cap. The one central
194
210
  * rule: the budget gate, the fallback view, and the status free-ness read all judge seams
195
211
  * through it (plans/agent-backends.md "Free-ness" and "Every seam, checked"). The opencode
196
- * `catalog` is threaded the way the claude `account` is: an absent catalog means "no cache to
197
- * read", so an opencode seam reads NOT free, the safe direction for a budget gate. */
198
- export function seamFree(providers, seam, account = null, catalog = null) {
199
- const agent = seamAgent(seam);
200
- // One free-ness rule per backend: pi's pair price, claude's subscription, opencode's catalog,
201
- // and nothing for any other backend (its spend could still reach the cap, so the badge must
202
- // not read n/a). The opencode arm reads the gateway-qualified id (part 6b/7) and changes the
203
- // pi and claude arms nothing.
204
- switch (agent) {
212
+ * `catalog` is threaded the way the subscription `accounts` lookup is: an absent catalog means
213
+ * "no cache to read", so an opencode seam reads NOT free, the safe direction for a budget gate.
214
+ * `accounts` names the per-backend subscription account (part 2a/9): today only a claude seam
215
+ * asks, and it reads the same account it always did via `accounts((backend) => …)` returning
216
+ * claude's account for "claude" and null otherwise. */
217
+ export function seamFree(providers, seam, accounts = () => null, catalog = null) {
218
+ const backend = seamAgent(seam);
219
+ // One free-ness rule per backend: pi's pair price, the subscription account a backend bills
220
+ // (claude today), opencode's catalog, and nothing for any other backend (its spend could
221
+ // still reach the cap, so the badge must not read n/a). The opencode arm reads the
222
+ // gateway-qualified id (part 6b/7) and changes the pi and subscription arms nothing.
223
+ switch (backend) {
205
224
  case "pi":
206
225
  return pairFree(providers, seam.provider, seam.model);
207
- case "claude":
226
+ case "claude": {
227
+ // The generic subscription arm: read the seam's own backend's account (today always
228
+ // "claude") so a future subscription backend is judged by its own lookup, not a captured
229
+ // one. A subscription that is spending into overage is NOT free.
230
+ const account = accounts(seamAgent(seam));
208
231
  return account?.billing === "subscription" && !accountUsesOverage(account);
232
+ }
233
+ case "codex": {
234
+ // The same subscription arm for codex (part 8/9): read the seam's own backend's account
235
+ // ("codex") so a codex seam is free while it bills a ChatGPT subscription and is not
236
+ // spending into overage. API-key billing — which this build refuses at the startup gate —
237
+ // and an absent account read NOT free, so the badge never reads n/a while spend it cannot
238
+ // see could still reach the cap.
239
+ const account = accounts(seamAgent(seam));
240
+ return account?.billing === "subscription" && !accountUsesOverage(account);
241
+ }
209
242
  case "opencode":
210
243
  // A null catalog — no cache to read — and an unknown or billed model must all read NOT
211
244
  // free (opencodeModelFree's safe direction): the badge never reads n/a while spend it
@@ -222,31 +255,31 @@ export function seamFree(providers, seam, account = null, catalog = null) {
222
255
  /** Whether a fallback-config pair may be called free: a `claude/<id>` pair is free while the
223
256
  * account bills a subscription and is not using overage, a pi provider/model is free when
224
257
  * models.json prices it at zero, and an opencode pair is free when the catalog prices it at
225
- * zero (part 6b/7). Any other agent namespace, or an unresolvable pair, is refused — the badge
258
+ * zero (part 6b/7). Any other backend namespace, or an unresolvable pair, is refused — the badge
226
259
  * must not read n/a while a backend whose billing it cannot see runs. Exported so the budget
227
260
  * gate (src/gates/budget-gates.ts), the status pause set (src/status/status-data.ts) and the
228
261
  * free-ness view judge fallback pairs with the same predicate — neither may engage a backend
229
- * their own badge would not call free. `catalog` is the opencode cache, threaded like `account`;
230
- * absent, an opencode pair reads NOT free. */
231
- export function piPairFree(providers, provider, model, account = null, catalog = null) {
232
- return seamFree(providers, { provider, model }, account, catalog);
262
+ * their own badge would not call free. `catalog` is the opencode cache, threaded like the
263
+ * `accounts` lookup; absent, an opencode pair reads NOT free. */
264
+ export function piPairFree(providers, pair, accounts = () => null, catalog = null) {
265
+ return seamFree(providers, { agent: pair.backend, provider: pair.provider, model: pair.model }, accounts, catalog);
233
266
  }
234
267
  /** The free fallback pair the budget gate would engage for the team, or null when none is
235
268
  * usable: the price-aware resolution of the default tier (fallbackPair over
236
269
  * resolveTierFallbacks' borrow order), so a map whose `default` entry is absent or priced
237
270
  * still names the free pair the tier borrows. Null for a missing, unresolvable, or priced
238
271
  * pair — the gate's refusal, and the pair the badge/doctor must not advertise. */
239
- export function fallbackModelPair(config, modelsPath = piModelsPath(), account = null, catalog = null) {
272
+ export function fallbackModelPair(config, modelsPath = piModelsPath(), accounts = () => null, catalog = null) {
240
273
  const providers = readPiProviders(modelsPath);
241
- return fallbackPair(config, (p) => piPairFree(providers, p.provider, p.model, account, catalog));
274
+ return fallbackPair(config, (p) => piPairFree(providers, p, accounts, catalog));
242
275
  }
243
276
  /** True when the configured fallback model (plans/fallback-model.md) is one the budget gate may
244
277
  * actually engage: configured at all, resolvable to a provider/model pair, and priced at zero in
245
278
  * pi's definitions. False for every other case — no fallback, a half-named pair that would fall
246
- * through to pi's own default, an unknown id, or a priced one — and the gate then pauses role
247
- * loops exactly as it did before this feature. */
248
- export function fallbackModelFree(config, modelsPath = piModelsPath(), account = null, catalog = null) {
249
- return fallbackModelPair(config, modelsPath, account, catalog) !== null;
279
+ * through to pi's own default, an unknown id, or a priced one — and the gate then pauses
280
+ * roles exactly as it did before this feature. */
281
+ export function fallbackModelFree(config, modelsPath = piModelsPath(), accounts = () => null, catalog = null) {
282
+ return fallbackModelPair(config, modelsPath, accounts, catalog) !== null;
250
283
  }
251
284
  /** True when every model the team could use is free: each enabled role's effective
252
285
  * provider/model (configForRole — the director included, it is a catalog role, resolved at
@@ -257,10 +290,10 @@ export function fallbackModelFree(config, modelsPath = piModelsPath(), account =
257
290
  * free when models.json prices it at zero, and everything else counts as NOT free — the badge
258
291
  * must never read n/a while spend it is tracking could still reach the cap. With no enabled
259
292
  * roles and review off there are no pairs at all, so the team cannot spend and the answer is
260
- * true. `account` is the persisted claude billing answer (6a/7's reader), absent meaning "no
261
- * subscription", and `catalog` the opencode models cache (absent, an opencode seam reads not
262
- * free). */
263
- export function teamModelsFree(config, modelsPath = piModelsPath(), account = null, catalog = null) {
293
+ * true. `accounts` names each seam's own backend's subscription account (claude today),
294
+ * absent meaning "no subscription", and `catalog` the opencode models cache (absent, an
295
+ * opencode seam reads not free). */
296
+ export function teamModelsFree(config, modelsPath = piModelsPath(), accounts = () => null, catalog = null) {
264
297
  const pairs = [];
265
298
  for (const role of enabledRoleIds(config)) {
266
299
  const rc = configForRole(config, role);
@@ -281,5 +314,5 @@ export function teamModelsFree(config, modelsPath = piModelsPath(), account = nu
281
314
  if (pairs.length === 0)
282
315
  return true;
283
316
  const providers = readPiProviders(modelsPath);
284
- return pairs.every((seam) => seamFree(providers, seam, account, catalog));
317
+ return pairs.every((seam) => seamFree(providers, seam, accounts, catalog));
285
318
  }
@@ -1,7 +1,7 @@
1
1
  /** Whether a run's cost is real money that charges the daily cap (plans/agent-backends.md
2
2
  * part 6a): an API-billed run, or a subscription run that saw overage. A plain subscription
3
3
  * run's cost is notional — API-equivalent spend that never moves the cap. The one predicate
4
- * both the tick-usage fold and the merge accumulator split on, so the two cannot drift. */
4
+ * both the run-usage fold and the merge accumulator split on, so the two cannot drift. */
5
5
  export function runCostIsReal(run) {
6
6
  return run.billing !== "subscription" || run.usedOverage === true;
7
7
  }
@@ -3,7 +3,7 @@ import { cachedByStat } from "../files/stat-cache.js";
3
3
  import { isJsonObject } from "../files/json-object.js";
4
4
  import { readJsonFile } from "../files/json-files.js";
5
5
  /** pi's own saved-default model, read from pi's `settings.json` beside the models catalog —
6
- * the name a loop row shows when `tumwater.json` resolves no model and pi starts on its own
6
+ * the name a role row shows when `tumwater.json` resolves no model and pi starts on its own
7
7
  * default. This mirrors step 3 of pi 1.2.0's start-up model choice, "saved default from
8
8
  * settings" (`findInitialModel` in pi's `dist/core/model-resolver.js`), for a run tumwater
9
9
  * starts without `--provider`/`--model`. The reader does NOT model pi's last step: when the
@@ -101,14 +101,14 @@ export class PiStreamParser {
101
101
  outputTokens = 0;
102
102
  /** Prompt tokens this run sent, summed across assistant turns (usage.input + cacheRead +
103
103
  * cacheWrite). The counterpart to outputTokens: an operator compares the two to see how
104
- * much of a tick's spend is re-sent context versus generation. */
104
+ * much of a run's spend is re-sent context versus generation. */
105
105
  promptTokens = 0;
106
106
  /** The cache-read share of promptTokens (usage.cacheRead summed across turns) — the part
107
107
  * the provider served from its prompt cache, billed at the cache-read rate. */
108
108
  cacheReadTokens = 0;
109
109
  /** Prompt tokens sent up to AND INCLUDING the first assistant turn that carried an edit or
110
110
  * write tool call. Before any such turn every turn accumulates; once one is seen the
111
- * counter freezes, so it measures the context a tick burns before its first change. */
111
+ * counter freezes, so it measures the context a run burns before its first change. */
112
112
  preEditPromptTokens = 0;
113
113
  /** 1-based index of the first assistant turn carrying an edit or write tool call;
114
114
  * undefined when the run never edited. */
@@ -121,7 +121,7 @@ export class PiStreamParser {
121
121
  errorMessage;
122
122
  /** True when any event reports the provider rejecting the context as too large
123
123
  * (LM Studio's "Context size has been exceeded"; oMLX rejects over max_context_window).
124
- * Diagnostic: every tick starts a fresh session, so nothing needs dropping — but the
124
+ * Diagnostic: every run starts a fresh session, so nothing needs dropping — but the
125
125
  * error names the real cause. */
126
126
  contextExceeded = false;
127
127
  /** True when any event reports the model server killing an idle predict stream
@@ -131,7 +131,7 @@ export class PiStreamParser {
131
131
  transientServerTimeout = false;
132
132
  /** True when any event reports the provider rejecting the request with HTTP 429 (rate
133
133
  * limiting). Transient by definition: the session is healthy and a later attempt succeeds —
134
- * the loop's transient retry covers it, waiting out retryAfterSeconds when present. */
134
+ * the runner's transient retry covers it, waiting out retryAfterSeconds when present. */
135
135
  transientRateLimit = false;
136
136
  /** True when any event reports a provider-wide failure that is not rate limiting — the
137
137
  * connection down, a 5xx, the model failing to load (TRANSIENT_BACKEND, in pi-failure.ts).
@@ -187,7 +187,7 @@ export class PiStreamParser {
187
187
  /** True once any assistant message carried real content — a tool call or non-empty text.
188
188
  * A failed run whose every assistant turn is contentless (the provider's error turn has
189
189
  * `content: []` and stopReason "error") produced no work of its own, so a dirty worktree
190
- * belongs to a prior interrupted tick rather than this run (BUGS.md 2026-10-06). Distinct
190
+ * belongs to a prior interrupted run rather than this run (BUGS.md 2026-10-06). Distinct
191
191
  * from finalMessageContentless, which describes only the LAST message. */
192
192
  producedAssistantContent = false;
193
193
  /** True when pi auto-compacted the session during (or at the end of) the run. */
@@ -482,7 +482,7 @@ export class PiStreamParser {
482
482
  this.declaredNothingToDo = true;
483
483
  // The refusal comes only from an anchored `TUMWATER_REFUSED: <reason>` line: a bare
484
484
  // sentinel and a mid-sentence mention are not refusals, and a negating reason ("none",
485
- // "(n/a …)") clears one — BUGS.md 2026-09-23, four ticks ended ordinary replies with
485
+ // "(n/a …)") clears one — BUGS.md 2026-09-23, four runs ended ordinary replies with
486
486
  // `TUMWATER_REFUSED: none` and the harness destroyed their tested work. The last anchored
487
487
  // line wins (the run's final declaration is its verdict); a message WITHOUT a line leaves
488
488
  // an earlier declaration standing, so a closing remark cannot erase it.
@@ -1,4 +1,4 @@
1
- /** The run's watchdog clocks, split out of pi.ts's runPi (2026-10-04, organize): the tick
1
+ /** The run's watchdog clocks, split out of pi.ts's runPi (2026-10-04, organize): the run
2
2
  * deadline and the quiet watchdog (quiet-kill plus the stalled-tool-call warning) — the
3
3
  * timing policy of one pi run, which changes for different reasons than the spawn, stream,
4
4
  * and result-distillation wiring that drives it (src/agent/run-agent.ts keeps those). The
@@ -6,9 +6,8 @@
6
6
  * window, the zero-byte grace, the warned-call set) and exposes the flags the run's close
7
7
  * handler reads back; run-agent.ts feeds the parser live through the deps accessors, so no
8
8
  * notification path is needed when events land. */
9
- import { bufferedCommandStallMs, commandBuffersOutput } from "./command-shape.js";
10
- import { streamsToolOutput } from "../agent/backend-traits.js";
11
- /** Start one pi run's two watchdog clocks: the tick deadline (kills the process tree when it
9
+ import { bufferedCommandStallMs, callBuffersOutput } from "./command-shape.js";
10
+ /** Start one pi run's two watchdog clocks: the run deadline (kills the process tree when it
12
11
  * fires, recording whether the run was still progressing) and the quiet/stall interval
13
12
  * (quiet-kills a run that stopped making progress and warns on stalled tool calls). Returns
14
13
  * the live flags the run's close handler reads and the note/stop calls its data handlers and
@@ -24,14 +23,14 @@ export function startPiWatchdogs(deps) {
24
23
  // diff's review can finish instead of timing out mid-verification and being re-read from
25
24
  // scratch on the next attempt (BUGS.md 2026-10-07). "Still making progress" means a
26
25
  // progress event landed within `progressWindowMs` of the deadline — the quiet watchdog's
27
- // own recency window, but never more than half the tick budget, so a quiet window
26
+ // own recency window, but never more than half the run budget, so a quiet window
28
27
  // configured longer than the budget cannot let a run that stopped progressing past the
29
28
  // base deadline. The window does not depend on the quiet watchdog being enabled: a
30
29
  // progressing gate run earns its extension even when quietTimeoutSeconds is 0. One
31
30
  // checkpoint timer snapshots the progress count `progressWindowMs` before the deadline;
32
31
  // only progress since that snapshot counts, so a run that progressed early and then
33
32
  // stalled keeps today's bound.
34
- const progressWindowMs = deps.quietMs > 0 ? Math.min(deps.quietMs, deps.tickTimeoutMs / 2) : deps.tickTimeoutMs / 2;
33
+ const progressWindowMs = deps.quietMs > 0 ? Math.min(deps.quietMs, deps.runTimeoutMs / 2) : deps.runTimeoutMs / 2;
35
34
  let checkpointProgress = 0;
36
35
  // Arm (or re-arm) the checkpoint `progressWindowMs` before the upcoming deadline: the
37
36
  // progress count at that instant is the bar a run must beat to count as progressing. One
@@ -45,35 +44,35 @@ export function startPiWatchdogs(deps) {
45
44
  checkpointProgress = deps.progressCount();
46
45
  }, Math.max(0, deadlineMs - progressWindowMs));
47
46
  };
48
- armCheckpoint(deps.tickTimeoutMs);
47
+ armCheckpoint(deps.runTimeoutMs);
49
48
  let extended = false;
50
- let timeoutBudgetMs = deps.tickTimeoutMs;
49
+ let timeoutBudgetMs = deps.runTimeoutMs;
51
50
  let timeout;
52
51
  const fireDeadline = () => {
53
52
  if (deps.extendOnProgress && !extended && deps.progressCount() > checkpointProgress) {
54
53
  extended = true;
55
- timeoutBudgetMs = deps.tickTimeoutMs * 2;
56
- armCheckpoint(deps.tickTimeoutMs); // the extended deadline gets its own progress window
57
- timeout = setTimeout(fireDeadline, deps.tickTimeoutMs); // one more slice; the next firing is final
54
+ timeoutBudgetMs = deps.runTimeoutMs * 2;
55
+ armCheckpoint(deps.runTimeoutMs); // the extended deadline gets its own progress window
56
+ timeout = setTimeout(fireDeadline, deps.runTimeoutMs); // one more slice; the next firing is final
58
57
  return;
59
58
  }
60
59
  timedOut = true;
61
60
  // A deadline that fires on a run still making progress killed a slow run, not a hung
62
61
  // one: "recent progress" is progress since the checkpoint `progressWindowMs` before this
63
62
  // deadline — the same window the extension test uses, so the kill's classification
64
- // cannot contradict the extension decision when the quiet window is longer than the tick
63
+ // cannot contradict the extension decision when the quiet window is longer than the run
65
64
  // budget (BUGS.md 2026-09-29, 2026-10-09). A run with no progress event at all — never
66
65
  // emitted a byte, or bytes without one structured event — keeps today's discard path: it
67
66
  // has not demonstrably begun.
68
67
  timedOutProgressing = deps.progressCount() > 0 && deps.progressCount() > checkpointProgress;
69
68
  deps.kill();
70
69
  };
71
- timeout = setTimeout(fireDeadline, deps.tickTimeoutMs);
70
+ timeout = setTimeout(fireDeadline, deps.runTimeoutMs);
72
71
  // Quiet watchdog: a healthy run makes PROGRESS continuously even when slow — messages
73
72
  // start and end, turns and tool calls complete. Prolonged lack of progress means a hung
74
73
  // tool (an interactive command waiting for input) or a zombie stream (a dead generation
75
74
  // whose connection drips content-free keepalive updates for hours) that would otherwise
76
- // burn the whole tick timeout. Raw output bytes deliberately do NOT reset the clock:
75
+ // burn the whole run timeout. Raw output bytes deliberately do NOT reset the clock:
77
76
  // keepalives are bytes without progress. Checked on an interval against the wall clock,
78
77
  // so it also fires promptly after a machine sleep rather than pausing with a suspended
79
78
  // timer. The stall warning below rides the same interval: it needs no kill of its own,
@@ -87,14 +86,14 @@ export function startPiWatchdogs(deps) {
87
86
  // first bytes entirely — fork/exec starved by the same load, or bytes already written
88
87
  // but not yet drained (a firing timer phase precedes the poll phase that delivers
89
88
  // stdout, and a suspended machine resumes the same way). The 2026-09-23 fix therefore
90
- // gave a byte-silent run no quiet kill at all, delegating its bound to the tick timeout
89
+ // gave a byte-silent run no quiet kill at all, delegating its bound to the run timeout
91
90
  // — which holds only while runTimeoutSeconds is near the quiet window's scale (BUGS.md
92
91
  // 2026-09-29: the live 54000 s config left a wedged model connection 15 hours in a
93
92
  // concurrency slot). A zero-byte run now gets a finite bound of its own, independent of
94
- // the tick timeout: max(two quiet windows, 30 min). Startup latency under healthy load
93
+ // the run timeout: max(two quiet windows, 30 min). Startup latency under healthy load
95
94
  // stays far below it, so the runs the 2026-09-23 fix protected are still protected; a
96
95
  // run that is genuinely wedged before its first byte (dead connection, unscheduled
97
- // fork/exec) is reaped at half an hour instead of at the tick timeout. The false
96
+ // fork/exec) is reaped at half an hour instead of at the run timeout. The false
98
97
  // positive it can cost — a machine so loaded the child has not been scheduled in 30
99
98
  // minutes — is a quiet kill, which resumes the session and the worktree's edits like
100
99
  // an interruption: a restart, not discarded work. Once bytes have flowed the run has
@@ -114,7 +113,7 @@ export function startPiWatchdogs(deps) {
114
113
  continue;
115
114
  // A command whose stdout is piped or redirected holds its bytes away from
116
115
  // pi until it exits, so "no output" there is the prescribed shape, not
117
- // evidence of a hang (BUGS.md 2026-09-28: the tick prompt tells every loop
116
+ // evidence of a hang (BUGS.md 2026-09-28: the run prompt tells every role
118
117
  // to pipe its verification through `tail`, and the resulting false alarms
119
118
  // were the digest's top warning cluster, drowning real hangs). Silence is
120
119
  // not the only way such a command hangs, though: a leaked grandchild
@@ -129,7 +128,7 @@ export function startPiWatchdogs(deps) {
129
128
  // ANY of its calls — every one is judged by the buffered wall-clock threshold
130
129
  // (plans/agent-backends.md "Watchdogs and progress"); pi keeps its per-command
131
130
  // classification, where only a piped or redirected command is buffered.
132
- if (!streamsToolOutput(deps.agent) || commandBuffersOutput(call.command || call.label)) {
131
+ if (callBuffersOutput(deps.agent, call.command || call.label)) {
133
132
  const openMs = Date.now() - call.lastActivityAt;
134
133
  const bufferedStallMs = bufferedCommandStallMs(deps.stallMs);
135
134
  if (openMs >= bufferedStallMs) {