tumwater 0.1.3 → 0.3.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 (482) hide show
  1. package/README.md +77 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +23 -0
  4. package/dist/src/agent/agent-backend.js +9 -7
  5. package/dist/src/agent/backend-traits.js +77 -0
  6. package/dist/src/agent/claude/claude-account.js +81 -161
  7. package/dist/src/agent/claude/claude-args.js +19 -19
  8. package/dist/src/agent/claude/claude-backend.js +58 -48
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +11 -19
  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 +402 -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 +112 -0
  25. package/dist/src/agent/notes-shim.js +40 -0
  26. package/dist/src/agent/opencode/opencode-args.js +60 -0
  27. package/dist/src/agent/opencode/opencode-backend.js +258 -0
  28. package/dist/src/agent/opencode/opencode-catalog.js +210 -0
  29. package/dist/src/agent/opencode/opencode-config.js +193 -0
  30. package/dist/src/agent/opencode/opencode-env.js +35 -0
  31. package/dist/src/agent/opencode/opencode-session-id.js +36 -0
  32. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +16 -33
  35. package/dist/src/agent/run-agent.js +475 -381
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +142 -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 +487 -0
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +384 -0
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +354 -132
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +132 -22
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +89 -11
  50. package/dist/src/backlog/backlog-structure.js +271 -125
  51. package/dist/src/backlog/backlog-write.js +121 -471
  52. package/dist/src/backlog/backlog.js +129 -17
  53. package/dist/src/backlog/question-answer.js +345 -0
  54. package/dist/src/baseline/main-baseline.js +157 -101
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +164 -55
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +41 -0
  61. package/dist/src/{brief.js → brief/brief.js} +38 -33
  62. package/dist/src/budget/budget.js +55 -28
  63. package/dist/src/budget/fallback-breaker.js +31 -31
  64. package/dist/src/build/build-check-detect.js +25 -16
  65. package/dist/src/build/build-check-events.js +87 -22
  66. package/dist/src/build/build-check-report.js +66 -27
  67. package/dist/src/build/build-check-scoped.js +64 -68
  68. package/dist/src/build/build-check.js +17 -15
  69. package/dist/src/build/build-info.js +3 -3
  70. package/dist/src/build/build-stage.js +31 -35
  71. package/dist/src/build/dep-install.js +21 -21
  72. package/dist/src/build/host-sleep.js +1 -1
  73. package/dist/src/change/change-data.js +131 -37
  74. package/dist/src/change/change-render.js +56 -14
  75. package/dist/src/cli/brief-command.js +9 -8
  76. package/dist/src/cli/cli-args.js +133 -28
  77. package/dist/src/cli/cli-command-args.js +82 -21
  78. package/dist/src/cli/cli-editor.js +15 -2
  79. package/dist/src/cli/cli-flag-specs.js +180 -50
  80. package/dist/src/cli/cli-init.js +51 -0
  81. package/dist/src/cli/cli-marker-commands.js +14 -11
  82. package/dist/src/cli/cli-query-commands.js +325 -224
  83. package/dist/src/cli/cli-repo-gate.js +12 -2
  84. package/dist/src/cli/cli-role-command.js +420 -0
  85. package/dist/src/cli/cli-start-summary.js +160 -0
  86. package/dist/src/cli/cli-start.js +453 -0
  87. package/dist/src/cli/completion.js +202 -17
  88. package/dist/src/cli/config-commands.js +51 -25
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +413 -157
  91. package/dist/src/cli/log-commands.js +234 -62
  92. package/dist/src/cli/notify-command.js +33 -18
  93. package/dist/src/cli/question-commands.js +215 -231
  94. package/dist/src/cli.js +154 -49
  95. package/dist/src/collections.js +84 -18
  96. package/dist/src/concurrency/check-permit.js +35 -19
  97. package/dist/src/concurrency/lock.js +22 -17
  98. package/dist/src/concurrency/provider-lease.js +105 -0
  99. package/dist/src/concurrency/semaphore.js +344 -46
  100. package/dist/src/config/config-aliases.js +105 -0
  101. package/dist/src/config/config-editable-keys.js +16 -4
  102. package/dist/src/config/config-fallbacks.js +416 -0
  103. package/dist/src/config/config-field-checks.js +58 -19
  104. package/dist/src/config/config-keys.js +182 -0
  105. package/dist/src/config/config-live.js +113 -22
  106. package/dist/src/config/config-request.js +137 -0
  107. package/dist/src/config/config-schema.js +58 -39
  108. package/dist/src/config/config-selector-checks.js +123 -0
  109. package/dist/src/config/config-validation-agents.js +80 -0
  110. package/dist/src/config/config-validation-model.js +116 -0
  111. package/dist/src/config/config-validation.js +201 -175
  112. package/dist/src/config/config-views.js +202 -346
  113. package/dist/src/config/config-write-core.js +135 -0
  114. package/dist/src/config/config-write.js +219 -391
  115. package/dist/src/config/config.js +102 -81
  116. package/dist/src/config/model-selector.js +44 -18
  117. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  118. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  119. package/dist/src/doctor/doctor-backlog.js +191 -161
  120. package/dist/src/doctor/doctor-check-names.js +81 -0
  121. package/dist/src/doctor/doctor-checks.js +229 -305
  122. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  123. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  124. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  125. package/dist/src/doctor/doctor-holds.js +181 -104
  126. package/dist/src/doctor/doctor-install.js +31 -0
  127. package/dist/src/doctor/doctor-launch-services.js +2 -2
  128. package/dist/src/doctor/doctor-model-checks.js +496 -270
  129. package/dist/src/doctor/doctor-notify.js +5 -7
  130. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  131. package/dist/src/doctor/doctor-orphans.js +189 -69
  132. package/dist/src/doctor/doctor-project-checks.js +303 -0
  133. package/dist/src/doctor/doctor-render.js +36 -9
  134. package/dist/src/doctor/doctor.js +145 -50
  135. package/dist/src/doctor/image-probe.js +257 -0
  136. package/dist/src/doctor/outcome.js +3 -2
  137. package/dist/src/doctor/probe.js +60 -0
  138. package/dist/src/events/event-format.js +363 -155
  139. package/dist/src/events/event-labels.js +123 -0
  140. package/dist/src/events/event-legacy.js +164 -0
  141. package/dist/src/events/event-read.js +74 -38
  142. package/dist/src/events/event-tally.js +16 -0
  143. package/dist/src/events/event-window.js +83 -27
  144. package/dist/src/events/events.js +47 -26
  145. package/dist/src/events/notify.js +85 -35
  146. package/dist/src/failure/failure-cluster.js +23 -20
  147. package/dist/src/failure/failure-data.js +140 -92
  148. package/dist/src/failure/failure-render.js +97 -78
  149. package/dist/src/failure/failure-state-change.js +45 -33
  150. package/dist/src/failure/time-spend.js +166 -82
  151. package/dist/src/files/file-queue.js +16 -4
  152. package/dist/src/files/files.js +129 -41
  153. package/dist/src/files/json-files.js +3 -3
  154. package/dist/src/files/json-object.js +45 -9
  155. package/dist/src/files/stat-cache.js +2 -2
  156. package/dist/src/files/tail.js +2 -2
  157. package/dist/src/gates/bootstrap-gates.js +8 -8
  158. package/dist/src/gates/budget-gates.js +361 -52
  159. package/dist/src/gates/disk-gate.js +18 -18
  160. package/dist/src/gates/gate-polls.js +201 -103
  161. package/dist/src/gates/maintenance-quota.js +34 -37
  162. package/dist/src/gates/pause-gates.js +33 -15
  163. package/dist/src/gates/readiness.js +8 -3
  164. package/dist/src/gates/role-cap-gates.js +23 -28
  165. package/dist/src/gates/startup-gate.js +154 -48
  166. package/dist/src/gates/streak-gate.js +67 -41
  167. package/dist/src/git/commit-message.js +80 -48
  168. package/dist/src/git/git-diff.js +29 -23
  169. package/dist/src/git/git-run.js +87 -46
  170. package/dist/src/git/git.js +114 -92
  171. package/dist/src/git/slot-salvage.js +253 -0
  172. package/dist/src/git/slots-state.js +18 -9
  173. package/dist/src/git/worktree-pool.js +150 -49
  174. package/dist/src/git/worktree-use.js +14 -20
  175. package/dist/src/git/worktree.js +108 -51
  176. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  177. package/dist/src/gui/gui-args.js +88 -21
  178. package/dist/src/gui/gui-backlog-commands.js +297 -0
  179. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  180. package/dist/src/gui/gui-command.js +9 -14
  181. package/dist/src/gui/gui-config-commands.js +462 -0
  182. package/dist/src/gui/gui-endpoint-commands.js +17 -716
  183. package/dist/src/gui/gui-endpoint-common.js +49 -0
  184. package/dist/src/gui/gui-endpoints.js +13 -331
  185. package/dist/src/gui/gui-history-endpoints.js +301 -0
  186. package/dist/src/gui/gui-operator-commands.js +240 -0
  187. package/dist/src/gui/gui-prompt-commands.js +231 -0
  188. package/dist/src/gui/gui-report-endpoints.js +90 -0
  189. package/dist/src/gui/gui-server.js +33 -9
  190. package/dist/src/gui/gui-team-commands.js +164 -0
  191. package/dist/src/gui/http-body.js +17 -1
  192. package/dist/src/history/history-data.js +166 -203
  193. package/dist/src/history/history-queued.js +147 -0
  194. package/dist/src/history/history-render.js +96 -0
  195. package/dist/src/history/history.js +206 -103
  196. package/dist/src/inbox/inbox-attachments.js +94 -39
  197. package/dist/src/inbox/inbox-cancel.js +16 -15
  198. package/dist/src/inbox/inbox-edit.js +10 -10
  199. package/dist/src/inbox/inbox-submit.js +54 -14
  200. package/dist/src/inbox/inbox-top.js +99 -0
  201. package/dist/src/inbox/inbox.js +49 -44
  202. package/dist/src/inbox/pending-prompt.js +27 -27
  203. package/dist/src/inbox/prompt-commands.js +154 -100
  204. package/dist/src/inbox/prompt-not-before.js +2 -2
  205. package/dist/src/init/init-templates.js +15 -14
  206. package/dist/src/init/init.js +35 -29
  207. package/dist/src/merge/backlog-conflicts.js +335 -0
  208. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +100 -93
  209. package/dist/src/merge/merge-cancel.js +53 -0
  210. package/dist/src/merge/merge-check-failures.js +209 -0
  211. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +183 -110
  212. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +6 -5
  213. package/dist/src/merge/merge-drain.js +351 -0
  214. package/dist/src/merge/merge-git.js +403 -0
  215. package/dist/src/merge/merge-pipeline.js +111 -0
  216. package/dist/src/merge/merge-post-review.js +90 -0
  217. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  218. package/dist/src/merge/merge-queue-render.js +113 -0
  219. package/dist/src/merge/merge-queue.js +224 -0
  220. package/dist/src/merge/merge-requeue.js +102 -0
  221. package/dist/src/merge/merge-slot.js +385 -0
  222. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +97 -71
  223. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +151 -82
  224. package/dist/src/merge/merge-top.js +69 -0
  225. package/dist/src/merge/merge-verify.js +214 -0
  226. package/dist/src/notes-cli.js +112 -0
  227. package/dist/src/operator/operator-commands.js +268 -89
  228. package/dist/src/operator/operator-intent.js +130 -82
  229. package/dist/src/operator/operator-requests.js +82 -87
  230. package/dist/src/operator/retire.js +49 -31
  231. package/dist/src/operator/revert.js +179 -0
  232. package/dist/src/orchestrator/orchestrator-launch.js +116 -48
  233. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  234. package/dist/src/orchestrator/orchestrator-scheduling.js +78 -348
  235. package/dist/src/orchestrator/orchestrator.js +173 -361
  236. package/dist/src/orchestrator/retention.js +66 -15
  237. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  238. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  239. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  240. package/dist/src/orchestrator/startup-migration-steps.js +52 -0
  241. package/dist/src/orchestrator/startup-migrations.js +44 -0
  242. package/dist/src/paths.js +228 -75
  243. package/dist/src/pi/command-shape.js +12 -1
  244. package/dist/src/pi/pi-args.js +30 -5
  245. package/dist/src/pi/pi-backend.js +19 -27
  246. package/dist/src/pi/pi-bin.js +64 -67
  247. package/dist/src/pi/pi-event-line.js +17 -1
  248. package/dist/src/pi/pi-failure.js +87 -0
  249. package/dist/src/pi/pi-listed-models.js +53 -0
  250. package/dist/src/pi/pi-models.js +163 -42
  251. package/dist/src/pi/pi-run-result.js +1 -1
  252. package/dist/src/pi/pi-settings.js +86 -0
  253. package/dist/src/pi/pi-stream.js +137 -113
  254. package/dist/src/pi/pi-watchdogs.js +20 -20
  255. package/dist/src/pi-extension/bounded-output.js +25 -8
  256. package/dist/src/pi-extension/context-budget.js +5 -3
  257. package/dist/src/pi-extension/context-shake.js +54 -21
  258. package/dist/src/pi-extension/context-window.js +36 -0
  259. package/dist/src/pi-extension/full-output.js +5 -12
  260. package/dist/src/pi-extension/strip-thinking.js +79 -0
  261. package/dist/src/pi-extension/trim-system-prompt.js +37 -0
  262. package/dist/src/pi-extension/write-guard.js +37 -0
  263. package/dist/src/process/launch-services.js +11 -11
  264. package/dist/src/process/node-bin.js +16 -0
  265. package/dist/src/process/process-group.js +8 -18
  266. package/dist/src/process/process-table.js +1 -1
  267. package/dist/src/process/process.js +48 -9
  268. package/dist/src/process/run-marker.js +42 -7
  269. package/dist/src/process/supervisor.js +47 -35
  270. package/dist/src/prompt/dialect.js +46 -11
  271. package/dist/src/prompt/gate-notes.js +203 -0
  272. package/dist/src/prompt/gate-prompts.js +201 -188
  273. package/dist/src/prompt/notes-backlog.js +181 -0
  274. package/dist/src/prompt/principles.js +1 -1
  275. package/dist/src/prompt/prompt-followup.js +43 -29
  276. package/dist/src/prompt/prompt.js +255 -172
  277. package/dist/src/redeploy/redeploy-policy.js +29 -24
  278. package/dist/src/redeploy/redeploy-probes.js +1 -1
  279. package/dist/src/redeploy/redeploy.js +7 -6
  280. package/dist/src/redeploy/redeployer.js +78 -70
  281. package/dist/src/redeploy/self-reload.js +3 -2
  282. package/dist/src/report/report-data.js +374 -285
  283. package/dist/src/report/report-fold.js +430 -0
  284. package/dist/src/report/report-main-red.js +55 -0
  285. package/dist/src/report/report-render.js +321 -61
  286. package/dist/src/report/report-types.js +1 -0
  287. package/dist/src/report/report.js +26 -9
  288. package/dist/src/request-timeouts.js +2 -2
  289. package/dist/src/review/known-flakes.js +133 -14
  290. package/dist/src/review/post-merge-review.js +106 -0
  291. package/dist/src/review/protected-edits.js +7 -7
  292. package/dist/src/review/review-followup.js +21 -9
  293. package/dist/src/review/review-precheck.js +77 -33
  294. package/dist/src/review/review-run.js +395 -0
  295. package/dist/src/review/review-verdict.js +16 -2
  296. package/dist/src/review/review.js +172 -356
  297. package/dist/src/review/secret-leaks.js +147 -0
  298. package/dist/src/review/suite-rerun.js +3 -3
  299. package/dist/src/roles/legacy-role-ids.js +21 -0
  300. package/dist/src/roles/legacy-role-migration.js +135 -0
  301. package/dist/src/roles/role-catalog.js +172 -137
  302. package/dist/src/roles/role-guidance.js +112 -44
  303. package/dist/src/roles/role-labels.js +9 -0
  304. package/dist/src/roles/role-render.js +221 -28
  305. package/dist/src/roles/role-view.js +244 -33
  306. package/dist/src/roles/roles.js +37 -35
  307. package/dist/src/roles/runner-ids.js +97 -0
  308. package/dist/src/run/acceptance-greps.js +119 -0
  309. package/dist/src/run/feedback-filings.js +51 -0
  310. package/dist/src/{tick → run}/qa-coverage.js +10 -27
  311. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +166 -136
  312. package/dist/src/run/run-detail-data.js +179 -0
  313. package/dist/src/run/run-detail-render.js +98 -0
  314. package/dist/src/run/run-detail.js +168 -0
  315. package/dist/src/run/run-finalize.js +154 -0
  316. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -6
  317. package/dist/src/run/run-prompt.js +304 -0
  318. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +80 -35
  319. package/dist/src/run/run-stage.js +295 -0
  320. package/dist/src/run/run-timing.js +198 -0
  321. package/dist/src/run/run-usage.js +180 -0
  322. package/dist/src/run/run-verdict.js +301 -0
  323. package/dist/src/run/selfcheck-digest.js +40 -0
  324. package/dist/src/run/stage-backlog-checks.js +249 -0
  325. package/dist/src/run/stage-check.js +172 -0
  326. package/dist/src/run/stage-path-checks.js +730 -0
  327. package/dist/src/runner/leftover.js +185 -0
  328. package/dist/src/{loop → runner}/model-fallback.js +13 -12
  329. package/dist/src/runner/revision.js +202 -0
  330. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +10 -10
  331. package/dist/src/runner/runner-outcome.js +107 -0
  332. package/dist/src/runner/runner-pi.js +433 -0
  333. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +34 -28
  334. package/dist/src/runner/runner-run.js +625 -0
  335. package/dist/src/runner/runner-state-load.js +268 -0
  336. package/dist/src/runner/runner-state.js +102 -0
  337. package/dist/src/runner/runner.js +350 -0
  338. package/dist/src/scheduling/backoff.js +64 -54
  339. package/dist/src/scheduling/claims.js +46 -26
  340. package/dist/src/scheduling/once-round.js +30 -30
  341. package/dist/src/scheduling/quiet-hours.js +26 -34
  342. package/dist/src/scheduling/scheduling.js +87 -74
  343. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  344. package/dist/src/status/status-budget.js +123 -0
  345. package/dist/src/status/status-data.js +106 -191
  346. package/dist/src/status/status-inbox.js +47 -0
  347. package/dist/src/status/status-install.js +26 -0
  348. package/dist/src/status/status-polls.js +70 -59
  349. package/dist/src/status/status-roles.js +131 -0
  350. package/dist/src/{fleet → team}/error-storm.js +16 -16
  351. package/dist/src/{fleet → team}/failure-spread.js +10 -10
  352. package/dist/src/{fleet → team}/orchestrator-info.js +97 -25
  353. package/dist/src/{fleet → team}/reclaim.js +9 -9
  354. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +60 -26
  355. package/dist/src/team/team-polls.js +299 -0
  356. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +47 -40
  357. package/dist/src/text/datetime.js +102 -16
  358. package/dist/src/text/format.js +49 -3
  359. package/dist/src/text/markdown.js +8 -5
  360. package/dist/src/text/phrases.js +89 -54
  361. package/dist/src/text/suggest.js +3 -2
  362. package/dist/src/text/text.js +68 -14
  363. package/dist/src/ui/badges.js +165 -93
  364. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  365. package/dist/src/ui/gui/gui-client-boot.js +221 -42
  366. package/dist/src/ui/gui/gui-client-composer.js +105 -27
  367. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  368. package/dist/src/ui/gui/gui-client-drawer.js +226 -130
  369. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  370. package/dist/src/ui/gui/gui-client-history.js +492 -68
  371. package/dist/src/ui/gui/gui-client-labels.js +23 -0
  372. package/dist/src/ui/gui/gui-client-markdown.js +1 -1
  373. package/dist/src/ui/gui/gui-client-model.js +84 -76
  374. package/dist/src/ui/gui/gui-client-operator.js +112 -78
  375. package/dist/src/ui/gui/gui-client-pending.js +248 -41
  376. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  377. package/dist/src/ui/gui/gui-client-report.js +122 -179
  378. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +118 -76
  379. package/dist/src/ui/gui/gui-client-settings.js +180 -20
  380. package/dist/src/ui/gui/gui-client-team.js +252 -0
  381. package/dist/src/ui/gui/gui-client.js +48 -40
  382. package/dist/src/ui/gui/gui-icons.js +0 -2
  383. package/dist/src/ui/gui/gui-page.js +25 -24
  384. package/dist/src/ui/gui/gui-styles.js +46 -38
  385. package/dist/src/ui/progress-data.js +11 -11
  386. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +40 -40
  387. package/dist/src/ui/status-model.js +276 -194
  388. package/dist/src/ui/status-payload.js +227 -62
  389. package/dist/src/ui/status-render.js +196 -108
  390. package/dist/src/ui/team-alerts.js +532 -0
  391. package/dist/src/ui/tone.js +11 -11
  392. package/dist/src/ui/transcript-run.js +58 -0
  393. package/dist/src/ui/transcript-tail.js +28 -19
  394. package/dist/src/ui/transcript.js +85 -38
  395. package/dist/src/ui/tui/tui-backlog.js +132 -21
  396. package/dist/src/ui/tui/tui-editor-answer.js +93 -0
  397. package/dist/src/ui/tui/tui-editor-attach.js +105 -0
  398. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  399. package/dist/src/ui/tui/tui-editor-brief.js +105 -0
  400. package/dist/src/ui/tui/tui-editor-budget.js +159 -0
  401. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  402. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  403. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  404. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  405. package/dist/src/ui/tui/tui-editor-entry.js +219 -0
  406. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  407. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  408. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  409. package/dist/src/ui/tui/tui-editor-interval.js +124 -0
  410. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  411. package/dist/src/ui/tui/tui-editor-notes.js +112 -0
  412. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  413. package/dist/src/ui/tui/tui-editor-pause.js +255 -0
  414. package/dist/src/ui/tui/tui-editor-prompt.js +90 -0
  415. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  416. package/dist/src/ui/tui/tui-editor-queued.js +94 -0
  417. package/dist/src/ui/tui/tui-editor-quiet.js +170 -0
  418. package/dist/src/ui/tui/tui-editor-retire.js +81 -0
  419. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  420. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  421. package/dist/src/ui/tui/tui-editor-search.js +85 -0
  422. package/dist/src/ui/tui/tui-editor-session.js +123 -0
  423. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  424. package/dist/src/ui/tui/tui-editors.js +185 -0
  425. package/dist/src/ui/tui/tui-frame.js +107 -58
  426. package/dist/src/ui/tui/tui-history.js +16 -12
  427. package/dist/src/ui/tui/tui-input.js +56 -60
  428. package/dist/src/ui/tui/tui-keys.js +106 -1825
  429. package/dist/src/ui/tui/tui-pane-activity.js +102 -0
  430. package/dist/src/ui/tui/tui-pane-backlog.js +269 -0
  431. package/dist/src/ui/tui/tui-pane-brief.js +29 -0
  432. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  433. package/dist/src/ui/tui/tui-pane-health.js +174 -0
  434. package/dist/src/ui/tui/tui-pane-history.js +636 -0
  435. package/dist/src/ui/tui/tui-pane-pending.js +369 -0
  436. package/dist/src/ui/tui/tui-pane-queued.js +159 -0
  437. package/dist/src/ui/tui/tui-pane-report.js +154 -0
  438. package/dist/src/ui/tui/tui-pane-session.js +120 -0
  439. package/dist/src/ui/tui/tui-pane-transcript.js +224 -0
  440. package/dist/src/ui/tui/tui-pane.js +125 -44
  441. package/dist/src/ui/tui/tui-panes.js +364 -0
  442. package/dist/src/ui/tui/tui.js +52 -34
  443. package/dist/src/verdict/fix-claim.js +135 -62
  444. package/dist/src/verdict/no-change.js +19 -0
  445. package/dist/src/verdict/refusal.js +80 -24
  446. package/dist/src/verdict/reply-contract.js +51 -28
  447. package/dist/src/version.js +2 -2
  448. package/package.json +5 -2
  449. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  450. package/dist/src/brief-edit.js +0 -42
  451. package/dist/src/cli/cli-run.js +0 -469
  452. package/dist/src/fleet/fleet-polls.js +0 -207
  453. package/dist/src/landing/backlog-conflicts.js +0 -116
  454. package/dist/src/landing/landing-check-failures.js +0 -141
  455. package/dist/src/landing/landing-drain.js +0 -204
  456. package/dist/src/landing/landing-git.js +0 -190
  457. package/dist/src/landing/landing-pipeline.js +0 -96
  458. package/dist/src/landing/landing-queue-render.js +0 -56
  459. package/dist/src/landing/landing-queue.js +0 -149
  460. package/dist/src/landing/landing-slot.js +0 -256
  461. package/dist/src/landing/landing-vetting.js +0 -196
  462. package/dist/src/loop/leftover.js +0 -129
  463. package/dist/src/loop/loop-pi.js +0 -370
  464. package/dist/src/loop/loop-state.js +0 -204
  465. package/dist/src/loop/loop-tick.js +0 -545
  466. package/dist/src/loop/loop.js +0 -315
  467. package/dist/src/loop/revision.js +0 -61
  468. package/dist/src/pi-extension/role-notes.js +0 -40
  469. package/dist/src/roles/loop-ids.js +0 -60
  470. package/dist/src/tick/stage-check.js +0 -302
  471. package/dist/src/tick/telemetry-digest.js +0 -24
  472. package/dist/src/tick/tick-detail-data.js +0 -107
  473. package/dist/src/tick/tick-detail.js +0 -150
  474. package/dist/src/tick/tick-finalize.js +0 -135
  475. package/dist/src/tick/tick-prompt.js +0 -201
  476. package/dist/src/tick/tick-stage.js +0 -223
  477. package/dist/src/tick/tick-timing.js +0 -195
  478. package/dist/src/tick/tick-usage.js +0 -142
  479. package/dist/src/tick/tick-verdict.js +0 -201
  480. package/dist/src/ui/fleet-alerts.js +0 -174
  481. package/dist/src/ui/gui/gui-client-fleet.js +0 -576
  482. package/dist/src/ui/gui/gui-client-sound.js +0 -52
@@ -1,8 +1,8 @@
1
- /** Building the claude command line for one tick (plans/agent-backends.md "The claude
1
+ /** Building the claude command line for one run (plans/agent-backends.md "The claude
2
2
  * invocation") — pure argv construction plus the prompt-on-stdin pairing, with no subprocess,
3
3
  * backend registration, or session ledger. Part 3c supplies the session decision (which
4
- * `--session-id` or `--resume` to pass) and the notes MCP config path; this module turns those
5
- * inputs into the invocation's argv.
4
+ * `--session-id` or `--resume` to pass); this module turns those inputs into the invocation's
5
+ * argv.
6
6
  *
7
7
  * The environment is built separately (claude-env.ts) so each concern has one module.
8
8
  *
@@ -15,13 +15,15 @@
15
15
  * fallback the design holds in reserve is not needed. */
16
16
  import fs from "node:fs";
17
17
  import path from "node:path";
18
+ import { appendPassthroughArgs } from "../agent-args.js";
19
+ import { writeGuardSettings } from "./claude-write-guard.js";
18
20
  /** Flags the harness passes itself. A passthrough arg equal to one of these is appended AFTER
19
21
  * the harness's own copy, and claude's argument parser is last-wins — so it would silently
20
22
  * override the isolation flags, the session choice, or the model the harness resolved. Each is
21
23
  * rejected by claudeArgs with the offending arg named. This is exactly the design's reserved
22
- * set (plans/agent-backends.md "Config"); config validation (config-validation.ts) mirrors this
23
- * set for `agents.claude.args`, so a value that passes validation is never accepted by
24
- * validation and then thrown here. */
24
+ * set (plans/agent-backends.md "Config"); config validation (config-validation-agents.ts)
25
+ * mirrors this set for `backends.claude.args`, so a value that passes validation is never
26
+ * accepted by validation and then thrown here. */
25
27
  export const HARNESS_CLAUDE_FLAGS = new Set([
26
28
  "-p",
27
29
  "--print",
@@ -37,6 +39,7 @@ export const HARNESS_CLAUDE_FLAGS = new Set([
37
39
  "--permission-mode",
38
40
  "--tools",
39
41
  "--setting-sources",
42
+ "--settings",
40
43
  "--strict-mcp-config",
41
44
  "--mcp-config",
42
45
  "--disable-slash-commands",
@@ -68,14 +71,18 @@ function shouldAppendAgents(cwd) {
68
71
  * each would otherwise silently change what the harness asked for. */
69
72
  export function claudeArgs(opts) {
70
73
  if ((opts.sessionId === undefined) === (opts.resumeSessionId === undefined)) {
71
- throw new Error("claudeArgs: pass exactly one of sessionId or resumeSessionId");
74
+ // Name which side fired so the invariant that would otherwise silently change the run's
75
+ // session is diagnosable at a glance: a conflicting pair (both set) differs from a missing
76
+ // choice (neither) and the two bugs have different fixes.
77
+ const state = opts.sessionId === undefined ? "neither is set" : "both are set";
78
+ throw new Error(`claudeArgs: pass exactly one of sessionId or resumeSessionId (${state})`);
72
79
  }
73
80
  const args = ["-p", "--output-format", "stream-json", "--verbose", "--model", opts.model];
74
81
  if (opts.thinking !== undefined) {
75
82
  // Own-property test, not a bare index: a level like `constructor` or `toString` would
76
83
  // otherwise inherit a function from Object.prototype and bypass the rejection below.
77
84
  if (!Object.hasOwn(EFFORT_BY_THINKING, opts.thinking)) {
78
- throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)}`);
85
+ throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)} (known: ${Object.keys(EFFORT_BY_THINKING).join(", ")})`);
79
86
  }
80
87
  args.push("--effort", EFFORT_BY_THINKING[opts.thinking]);
81
88
  }
@@ -83,22 +90,15 @@ export function claudeArgs(opts) {
83
90
  args.push("--session-id", opts.sessionId);
84
91
  else
85
92
  args.push("--resume", opts.resumeSessionId);
86
- args.push("--permission-mode", "bypassPermissions", "--tools", "Bash,Read,Edit,Write,Glob,Grep", "--setting-sources", "project", "--strict-mcp-config");
87
- if (opts.mcpConfig !== undefined)
88
- args.push("--mcp-config", opts.mcpConfig);
93
+ args.push("--permission-mode", "bypassPermissions", "--tools", "Bash,Read,Edit,Write,Glob,Grep", "--setting-sources", "project",
94
+ // The write guard's PreToolUse hook (claude-write-guard.ts).
95
+ "--settings", writeGuardSettings(opts.cwd), "--strict-mcp-config");
89
96
  args.push("--disable-slash-commands", "--exclude-dynamic-system-prompt-sections");
90
97
  if (shouldAppendAgents(opts.cwd))
91
98
  args.push("--append-system-prompt-file", "AGENTS.md");
92
99
  if (opts.maxBudgetUsd !== undefined && opts.maxBudgetUsd > 0) {
93
100
  args.push("--max-budget-usd", String(opts.maxBudgetUsd));
94
101
  }
95
- for (const arg of opts.args ?? []) {
96
- // Match the flag itself even when it carries its value as `--flag=value`.
97
- const flag = arg.split("=", 1)[0];
98
- if (HARNESS_CLAUDE_FLAGS.has(flag)) {
99
- throw new Error(`claudeArgs: passthrough arg ${JSON.stringify(arg)} repeats a harness flag`);
100
- }
101
- args.push(arg);
102
- }
102
+ appendPassthroughArgs(args, opts.args, HARNESS_CLAUDE_FLAGS, "claudeArgs");
103
103
  return { args, stdin: opts.prompt };
104
104
  }
@@ -16,34 +16,49 @@
16
16
  import fs from "node:fs";
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
- import { fileURLToPath } from "node:url";
20
19
  import { randomUUID } from "node:crypto";
21
- import { resolvedAgentConfig } from "../../config/config-views.js";
20
+ import { resolvedBackendConfig } from "../../config/config-views.js";
22
21
  import { MODEL_TIERS } from "../../config/config-schema.js";
23
22
  import { routeModel } from "../../config/model-selector.js";
24
- import { agentBaseEnv } from "../../process/run-marker.js";
25
- import { resolveBackendBin } from "../../pi/pi-bin.js";
23
+ import { backendBaseEnv, withNotesPath } from "../../process/run-marker.js";
24
+ import { resolveBinForBackend } from "../../pi/pi-bin.js";
25
+ import { expandHomeTilde } from "../../files/files.js";
26
26
  import { claudeArgs } from "./claude-args.js";
27
27
  import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
28
28
  import { claudeTranslator } from "./claude-stream.js";
29
29
  import { resumableSession } from "../session-ledger.js";
30
- /** claude's per-cwd project directory name under `~/.claude/projects/`: every character that
31
- * is not a letter or digit becomes `-`. Exported with `claudeTranscriptPath` so the PATH fake
32
- * and the tests create and look for exactly the file the resume check reads. */
30
+ /** claude's per-cwd project directory name under the claude config dir's `projects/`:
31
+ * every character that is not a letter or digit becomes `-`. Exported with
32
+ * `claudeTranscriptPath` so the PATH fake and the tests create and look for exactly the file
33
+ * the resume check reads. */
33
34
  export function claudeProjectSlug(cwd) {
34
35
  return cwd.replace(/[^A-Za-z0-9]/g, "-");
35
36
  }
37
+ /** claude's config directory: `$CLAUDE_CONFIG_DIR` when non-empty (a leading `~` expands
38
+ * against the home dir), else `~/.claude`. A configured team `configDir` (the expanded
39
+ * `backends.claude.configDir`) wins over any inherited env var — the team's own directory is
40
+ * never overridden by the operator's shell. This mirrors claude's own lookup and pi's
41
+ * `piAgentDir`; exported so doctor reads the same dir the adapter writes transcripts into. */
42
+ export function claudeConfigDir(env = process.env, homeDir = os.homedir(), configured) {
43
+ if (configured)
44
+ return expandHomeTilde(configured, homeDir);
45
+ const dir = env.CLAUDE_CONFIG_DIR;
46
+ if (!dir)
47
+ return path.join(homeDir, ".claude");
48
+ return expandHomeTilde(dir, homeDir);
49
+ }
36
50
  /** The transcript file claude writes for a session started in `cwd`: its resume check must find
37
51
  * a real file before passing `--resume`. claude's own layout is
38
- * `~/.claude/projects/<slug>/<sessionId>.jsonl`; exported so the fake and the resume logic
39
- * cannot drift. */
40
- export function claudeTranscriptPath(cwd, id) {
41
- return path.join(os.homedir(), ".claude", "projects", claudeProjectSlug(cwd), `${id}.jsonl`);
52
+ * `<config>/projects/<slug>/<sessionId>.jsonl`; exported so the fake and the resume logic
53
+ * cannot drift. `configDir` is the configured team dir, threaded through so the resume check
54
+ * looks where the run's own env will write. */
55
+ export function claudeTranscriptPath(cwd, id, env = process.env, homeDir = os.homedir(), configDir) {
56
+ return path.join(claudeConfigDir(env, homeDir, configDir), "projects", claudeProjectSlug(cwd), `${id}.jsonl`);
42
57
  }
43
58
  /** The CLI version every gated claude model needs (plans/agent-backends.md finding 9): on
44
- * 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and fleet runs
59
+ * 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and team runs
45
60
  * set DISABLE_AUTOUPDATER so they never update themselves. The gated models are the three
46
- * tier defaults, derived from AGENT_DEFAULT_MODELS through the same routing the config uses
61
+ * tier defaults, derived from BACKEND_DEFAULT_MODELS through the same routing the config uses
47
62
  * rather than listed again, so this table cannot drift from the models a seam resolves. */
48
63
  const CLAUDE_MIN_VERSION = "2.1.280";
49
64
  const CLAUDE_GATED_MODELS = new Set(MODEL_TIERS.map((tier) => routeModel(undefined, undefined, "claude", tier).model).filter((model) => model !== undefined));
@@ -65,37 +80,27 @@ function classifyClaudeExit(info) {
65
80
  }
66
81
  return {};
67
82
  }
68
- /** The inline `--mcp-config` JSON that runs the bundled role-notes server (part 5/7): the
69
- * same dist module pi's extension resolves to, spawned by the same node running this harness
70
- * so it needs no PATH lookup. */
71
- function roleNotesMcpConfig() {
72
- const server = fileURLToPath(new URL("../mcp/role-notes-server.js", import.meta.url));
73
- return JSON.stringify({ mcpServers: { tumwater: { command: process.execPath, args: [server] } } });
74
- }
75
83
  /** The ledger record this run may resume, or null: the newest record of `opts.kind` for
76
84
  * claude, at `opts.cwd` when one is given, whose transcript file exists. Shared by
77
85
  * `resolveClaudeSession` and the backend's `resumable` hook, so the resume guard and the
78
86
  * invocation can never drift on what counts as resumable. */
79
- function claudeResumableRecord(dir, opts) {
87
+ function claudeResumableRecord(dir, opts, configDir) {
80
88
  const record = resumableSession(dir, opts);
81
89
  if (record === null)
82
90
  return null;
83
91
  if (opts.cwd !== undefined && record.cwd !== opts.cwd)
84
92
  return null;
85
- if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id)))
93
+ if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id, process.env, os.homedir(), configDir)))
86
94
  return null;
87
95
  return record;
88
96
  }
89
97
  /** The session id this run uses: for a continue, the ledger's newest record of this kind when
90
98
  * it is claude's, this cwd's, and its transcript file exists; otherwise a fresh UUID. Returns
91
- * the id and the `--resume` id when resuming. */
92
- function resolveClaudeSession(run) {
99
+ * the id and the `--resume` id when resuming. `configDir` is the configured team dir, so the
100
+ * resume check looks where the run's own env will write transcripts. */
101
+ function resolveClaudeSession(run, configDir) {
93
102
  if (run.continueSession) {
94
- const record = claudeResumableRecord(run.sessionDir, {
95
- kind: run.kind,
96
- agent: "claude",
97
- cwd: run.cwd,
98
- });
103
+ const record = claudeResumableRecord(run.sessionDir, { kind: run.kind, agent: "claude", cwd: run.cwd }, configDir);
99
104
  if (record !== null)
100
105
  return { sessionId: record.id, resumeSessionId: record.id };
101
106
  }
@@ -109,7 +114,7 @@ export const claudeBackend = {
109
114
  binEnv: "TUMWATER_CLAUDE_BIN",
110
115
  minVersion: claudeMinVersion,
111
116
  resolveBin(config) {
112
- return resolveBackendBin(config, this);
117
+ return resolveBinForBackend(config, this);
113
118
  },
114
119
  invocation(run) {
115
120
  // Part 4b/7's config routing fills a claude tier default before the seam, so a nameless
@@ -117,35 +122,35 @@ export const claudeBackend = {
117
122
  if (run.config.model === undefined) {
118
123
  throw new Error("claude backend: no model resolved for this run");
119
124
  }
120
- const { sessionId, resumeSessionId } = resolveClaudeSession(run);
125
+ // The configured team config dir (plans/notes-command.md "Keeping funes out of team runs"):
126
+ // the run's env, its resume lookup, and the account seed all follow it; unset, nothing changes.
127
+ const claudeCfg = resolvedBackendConfig(run.config, "claude");
128
+ const configDir = claudeCfg.configDir;
129
+ const expandedConfigDir = configDir !== undefined ? claudeConfigDir(process.env, os.homedir(), configDir) : undefined;
130
+ const { sessionId, resumeSessionId } = resolveClaudeSession(run, configDir);
121
131
  const { args, stdin } = claudeArgs({
122
132
  cwd: run.cwd,
123
133
  model: run.config.model,
124
134
  thinking: run.config.thinking,
125
135
  ...(resumeSessionId !== undefined ? { resumeSessionId } : { sessionId }),
126
- // The operator's `agents.claude.args` passthrough, appended after the harness flags (the
127
- // builder rejects any entry that repeats a harness-managed flag). resolvedAgentConfig
136
+ // The operator's `backends.claude.args` passthrough, appended after the harness flags (the
137
+ // builder rejects any entry that repeats a harness-managed flag). resolvedBackendConfig
128
138
  // owns the view so a caller cannot mutate the loaded config through the returned array.
129
- args: resolvedAgentConfig(run.config, "claude").args,
139
+ args: claudeCfg.args,
130
140
  prompt: run.prompt,
131
141
  // The caller computes the remaining daily budget for an API-billed claude run and the
132
142
  // adapter forwards it; a subscription run or the director passes none (part 7a/7).
133
143
  ...(run.maxBudgetUsd !== undefined ? { maxBudgetUsd: run.maxBudgetUsd } : {}),
134
- // claude does not load the bundled pi extensions, so a run carrying a notebook gets the
135
- // role_notes MCP server instead. Passed on EVERY claude invocation that carries a
136
- // notebook — fresh or resumed, a transient retry, a SUMMARY follow-up — because claude
137
- // keeps no flags across --resume, so a resume without this would lose the tool.
138
- ...(run.notesPath !== undefined ? { mcpConfig: roleNotesMcpConfig() } : {}),
139
144
  });
140
- // agentBaseEnv applies the run marker and the macOS preload (shared with the pi adapter);
141
- // claudeEnv layers the backend's own scrub-and-set on top. The notebook path rides beside
142
- // the marker the same way pi's adapter carries it (pi-backend.ts withNotesPath), so the
143
- // spawned MCP server — which inherits this environment — reads the same notes file.
144
- const base = run.notesPath === undefined
145
- ? agentBaseEnv(run.runMarker)
146
- : { ...agentBaseEnv(run.runMarker), TUMWATER_NOTES_PATH: run.notesPath };
145
+ // backendBaseEnv applies the run marker and the macOS preload (shared with the pi adapter);
146
+ // claudeEnv layers the backend's own scrub-and-set on top. The notes path rides beside
147
+ // the marker the same way pi's adapter carries it (withNotesPath); from part 2/4 the run
148
+ // saves its note through the `tumwater-notes` command on PATH, so no MCP server reaches the
149
+ // run and `--strict-mcp-config` (always in claude-args.ts) keeps it that way.
150
+ const base = withNotesPath(backendBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
147
151
  const env = claudeEnv(base, {
148
152
  quietTimeoutSeconds: run.config.quietTimeoutSeconds,
153
+ ...(expandedConfigDir !== undefined ? { configDir: expandedConfigDir } : {}),
149
154
  });
150
155
  return { args, stdin, env, sessionId };
151
156
  },
@@ -155,8 +160,13 @@ export const claudeBackend = {
155
160
  // seed the account file, so both must classify it the same way.
156
161
  return claudeTranslator({ thirdPartyProvider: claudeThirdPartyProvider(process.env) });
157
162
  },
158
- resumable(dir, opts) {
159
- return claudeResumableRecord(dir, opts) !== null;
163
+ resumable(dir, opts, configDir) {
164
+ return claudeResumableRecord(dir, opts, configDir) !== null;
165
+ },
166
+ // Every Claude model reads images (Claude Code's Read tool returns them as image blocks), so
167
+ // a claude seam is always capable — the doctor probes nothing and never works around it.
168
+ imageInput() {
169
+ return true;
160
170
  },
161
171
  classifyExit: classifyClaudeExit,
162
172
  };
@@ -1,8 +1,7 @@
1
1
  /** Building the environment for one claude run (plans/agent-backends.md "The claude
2
2
  * invocation", "Findings" 2 and 4) — pure scrub-and-set over a base environment, with no
3
3
  * subprocess. The caller layers the run-marker, the macOS LaunchServices preload, and the
4
- * notebook on first (src/pi/pi-backend.ts's `withNotesPath` shows the pi adapter doing the
5
- * same), then hands the result here.
4
+ * notes on first (src/process/run-marker.ts's `withNotesPath`), then hands the result here.
6
5
  *
7
6
  * Inherited Claude Code session variables break auth: spawned from inside a Claude Code
8
7
  * session, a child that keeps `CLAUDECODE`, `CLAUDE_CODE_*`, `CLAUDE_AGENT_SDK_*` or the host's
@@ -13,7 +12,7 @@
13
12
  *
14
13
  * The set half disables auto-memory, backgrounded Bash tasks (finding 4: claude refuses a long
15
14
  * foreground command, backgrounds it, and abandons it when `-p` ends, which would report work
16
- * never verified), the auto-updater (a mid-fleet self-update must not swap the binary under a
15
+ * never verified), the auto-updater (a mid-team self-update must not swap the binary under a
17
16
  * run), telemetry, and error reporting; and raises the Bash timeouts so a slow suite runs in
18
17
  * the foreground. `BASH_MAX_TIMEOUT_MS` is derived from `quietTimeoutSeconds` so claude's own
19
18
  * Bash timeout fires before tumwater's quiet kill and the model sees a tool error rather than a
@@ -80,5 +79,10 @@ export function claudeEnv(base, opts) {
80
79
  env.DISABLE_AUTOUPDATER = "1";
81
80
  env.DISABLE_TELEMETRY = "1";
82
81
  env.DISABLE_ERROR_REPORTING = "1";
82
+ // A team config dir (plans/notes-command.md "Keeping funes out of team runs"): force claude's
83
+ // own config and transcripts under it, so a funes hook on ~/.claude/projects never indexes
84
+ // team runs. Unset leaves any inherited CLAUDE_CONFIG_DIR (or claude's default) untouched.
85
+ if (opts.configDir)
86
+ env.CLAUDE_CONFIG_DIR = opts.configDir;
83
87
  return env;
84
88
  }
@@ -14,7 +14,8 @@
14
14
  * on the seam's optional `warnings()` — src/agent/run-agent.ts forwards each as an
15
15
  * `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
16
16
  * run produced no authoritative `run_cost`. */
17
- import { isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
17
+ import { joinParts } from "../../text/text.js";
18
+ import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
18
19
  /** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
19
20
  * lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
20
21
  * lower-cased so a future tool still reads as a name rather than as `undefined`. */
@@ -37,20 +38,10 @@ function str(value) {
37
38
  }
38
39
  /** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
39
40
  * authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
40
- * on every event, so summing per event would double-count — the fold takes it once). */
41
+ * on every event, so summing per event would double-count — the fold takes it once). The
42
+ * wire-field reads stay here; the block's assembly is the shared canonicalUsageBlock. */
41
43
  function canonicalUsage(usage) {
42
- const input = nonNegativeNumber(usage?.input_tokens, 0);
43
- const cacheRead = nonNegativeNumber(usage?.cache_read_input_tokens, 0);
44
- const cacheWrite = nonNegativeNumber(usage?.cache_creation_input_tokens, 0);
45
- const output = nonNegativeNumber(usage?.output_tokens, 0);
46
- return {
47
- input,
48
- cacheRead,
49
- cacheWrite,
50
- output,
51
- totalTokens: input + cacheRead + cacheWrite + output,
52
- cost: { total: 0 },
53
- };
44
+ return canonicalUsageBlock(nonNegativeNumber(usage?.input_tokens, 0), nonNegativeNumber(usage?.cache_read_input_tokens, 0), nonNegativeNumber(usage?.cache_creation_input_tokens, 0), nonNegativeNumber(usage?.output_tokens, 0), 0);
54
45
  }
55
46
  /** One claude content block → one pi content block, or undefined for a block pi has no
56
47
  * vocabulary for (dropped, like any other unmapped native shape). `tool_use` becomes pi's
@@ -76,7 +67,7 @@ function canonicalBlock(block) {
76
67
  return {
77
68
  type: "toolCall",
78
69
  id: str(block.id) ?? "",
79
- name: Object.hasOwn(TOOL_NAMES, name) ? TOOL_NAMES[name] : name.toLowerCase(),
70
+ name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
80
71
  arguments: args,
81
72
  };
82
73
  }
@@ -96,7 +87,7 @@ function classifyResult(result) {
96
87
  return {};
97
88
  if (terminal === "error_max_budget_usd")
98
89
  return { kind: "budgetCapped" };
99
- const text = [str(result.result), str(result.error)].filter(Boolean).join(" ");
90
+ const text = joinParts([str(result.result), str(result.error)], " ");
100
91
  if (/error_max_budget_usd/i.test(text))
101
92
  return { kind: "budgetCapped" };
102
93
  const status = nonNegativeNumber(result.api_error_status, undefined);
@@ -203,7 +194,7 @@ export function claudeTranslator(opts = {}) {
203
194
  const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
204
195
  const status = nonNegativeNumber(event.error_status, undefined);
205
196
  const error = str(event.error) ?? "";
206
- const errorMessage = [status !== undefined ? `status ${status}` : "", error].filter(Boolean).join(": ");
197
+ const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
207
198
  return [
208
199
  JSON.stringify({
209
200
  type: "auto_retry_start",
@@ -304,7 +295,7 @@ export function claudeTranslator(opts = {}) {
304
295
  if (info.isUsingOverage === true)
305
296
  out.isUsingOverage = true;
306
297
  if (status === "rejected" && resetsAt !== undefined) {
307
- // resetsAt is a unix-seconds stamp; retryAfterSeconds is the wait the loop honors.
298
+ // resetsAt is a unix-seconds stamp; retryAfterSeconds is the wait the runner honors.
308
299
  out.retryAfterSeconds = Math.max(0, Math.round(resetsAt - Date.now() / 1000));
309
300
  }
310
301
  return [JSON.stringify(out), ...windows];
@@ -347,7 +338,8 @@ export function claudeTranslator(opts = {}) {
347
338
  return out;
348
339
  }
349
340
  case "tool_progress":
350
- // A 30 s heartbeat of a running tool call: known, and the open call already tracks liveness.
341
+ // A 30 s heartbeat of a running tool call: known, and the open call already tracks
342
+ // liveness.
351
343
  return [];
352
344
  default:
353
345
  record(type ?? "unknown");
@@ -0,0 +1,49 @@
1
+ /** claude's half of the write guard (src/agent/write-guard.ts holds the rule): a PreToolUse hook on
2
+ * the file-writing tools, passed to each run through `--settings`, refuses a write under the team
3
+ * root but outside the run's worktree.
4
+ *
5
+ * Hooks run under `--permission-mode bypassPermissions` too, and a hook's `deny` decision blocks
6
+ * the call and hands its reason to the model (checked live with claude 2.1.295 on 2026-10-23).
7
+ * `--setting-sources project` leaves the operator's own hooks out of a run, while `--settings`
8
+ * still loads this one. The hook gets the run's worktree as an argument rather than reading
9
+ * claude's `cwd`, which follows the Bash tool's `cd` into subdirectories. The entry point is
10
+ * write-guard-hook.ts beside this file. */
11
+ import { fileURLToPath } from "node:url";
12
+ import { shellQuote, strayWriteRefusal } from "../write-guard.js";
13
+ /** claude's file-writing tools a team run has: claude-args.ts's `--tools` allows only these two,
14
+ * and `--tools` is a reserved flag, so no passthrough can add another. */
15
+ const WRITE_TOOLS = ["Edit", "Write"];
16
+ /** Seconds claude waits for the hook before running the tool anyway. */
17
+ const HOOK_TIMEOUT_SECONDS = 30;
18
+ /** The `--settings` value for a run in `cwd`: one PreToolUse hook on the file-writing tools that
19
+ * runs the compiled write-guard-hook.js with this process's node, falling back to `node` on PATH
20
+ * when that binary has moved (as the notes shim does), and the run's worktree as its argument. The
21
+ * hook path resolves from this module, so a staged build (.tumwater/build/<sha>) runs its own
22
+ * copy. */
23
+ export function writeGuardSettings(cwd, nodeBin = process.execPath) {
24
+ const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
25
+ const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)} ${shellQuote(cwd)}`;
26
+ return JSON.stringify({
27
+ hooks: {
28
+ PreToolUse: [
29
+ { matcher: WRITE_TOOLS.join("|"), hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }] },
30
+ ],
31
+ },
32
+ });
33
+ }
34
+ /** The refusal for one PreToolUse payload (claude's hook stdin, parsed) from a run in `runDir`, or
35
+ * null to let the call run. A relative path resolves from the payload's `cwd`, where claude's
36
+ * tools would resolve it. */
37
+ export function claudeWriteRefusal(payload, runDir) {
38
+ if (typeof payload !== "object" || payload === null)
39
+ return null;
40
+ const { tool_name: tool, tool_input: input, cwd } = payload;
41
+ if (typeof tool !== "string" || !WRITE_TOOLS.includes(tool))
42
+ return null;
43
+ if (typeof input !== "object" || input === null)
44
+ return null;
45
+ const target = input.file_path;
46
+ if (typeof target !== "string" || target === "")
47
+ return null;
48
+ return strayWriteRefusal(target, runDir, typeof cwd === "string" && cwd !== "" ? cwd : runDir);
49
+ }
@@ -0,0 +1,13 @@
1
+ /** The PreToolUse hook claude runs before each file write in a team run (claude-write-guard.ts
2
+ * installs it through `--settings`): `node write-guard-hook.js <worktree>` with claude's hook
3
+ * payload on stdin. To block the write it prints a `deny` decision with the refusal as its reason:
4
+ * the model reads only the reason, where a refusal by exit 2 would also quote the whole hook
5
+ * command. It prints nothing to let the write run. Any failure of its own lets the write run, so a
6
+ * broken hook cannot stop every edit of a run; the stray-write warning in runner-run.ts still sees
7
+ * a write that slips through. The stdin read and the deny-decision printing are runGuardHook's
8
+ * (src/agent/write-guard.ts), shared with the codex PreToolUse hook; only the refusal computation
9
+ * differs, and it needs the run's checkout, which this process receives as its first argument. */
10
+ import { runGuardHook } from "../write-guard.js";
11
+ import { claudeWriteRefusal } from "./claude-write-guard.js";
12
+ const runDir = process.argv[2];
13
+ runGuardHook((payload) => (runDir ? claudeWriteRefusal(payload, runDir) : null));
@@ -0,0 +1,119 @@
1
+ /** The codex account state: the codex backend's subscription account (src/agent/
2
+ * subscription-account.ts) — which billing pays for codex runs, and the newest subscription
3
+ * usage-window reading each run reported (plans/codex-backend.md §7 "Spend, free-ness, window,
4
+ * images"). Persisted at `.tumwater/state/codex-account.json`, written only by the
5
+ * orchestrator process, so the sibling parts that throttle or display the subscription window
6
+ * read one answer instead of re-deriving it per surface.
7
+ *
8
+ * Everything here is a thin wrapper over the generic per-backend store
9
+ * (grounds/codex-backend.md "Groundwork: the subscription account is per backend", item 1),
10
+ * specialized to the backend id "codex", plus the codex-only seed (the `codex login status`
11
+ * probe that decides subscription vs API-key billing) that no other backend shares. A Codex
12
+ * run's billing and window readings reach the store through the generic observeAccount call
13
+ * the run finalizers make (run.agent === "codex"); the seed only fills the billing answer in
14
+ * before the first run so the budget gate and the API-key refusal can act on it. The file is a
15
+ * cache, never a source of truth: a run that reports billing overwrites it, and a missing or
16
+ * unreadable file reads as "no subscription". */
17
+ import { spawnSync } from "node:child_process";
18
+ import { resolvedBackendConfig } from "../../config/config-views.js";
19
+ import { observeAccount, readAccount, writeAccount, } from "../subscription-account.js";
20
+ import { codexBackend } from "./codex-backend.js";
21
+ import { codexEnv } from "./codex-env.js";
22
+ import { codexHome } from "./codex-catalog.js";
23
+ /** Read the persisted codex account state, or null when it is absent or unreadable. A file
24
+ * without a usable top-level `observedAt` reads as absent (the plan's torn-file rule). */
25
+ export function readCodexAccount(root, now = Date.now()) {
26
+ return readAccount(root, "codex", now);
27
+ }
28
+ /** Fold one run's billing and rate-window readings into the codex account file — the store's
29
+ * generic observeAccount for the "codex" backend, so a run's rollout feeding (plans/
30
+ * codex-backend.md "Spend, free-ness, window, images") lands under the codex id. */
31
+ export function observeCodexAccount(root, run, now = Date.now()) {
32
+ return observeAccount(root, "codex", run, now);
33
+ }
34
+ /** Seed the codex account file before the first run (plans/codex-backend.md §7): `codex login
35
+ * status` decides — a ChatGPT login is subscription billing, an API key is API billing.
36
+ * An existing non-api file is left alone, and a probe that cannot run or is not logged in (or
37
+ * prints something else) leaves the file absent (readCodexAccount then reads "no subscription")
38
+ * unless it must clear a sticky api refusal: an "api" answer is re-probed so the operator's
39
+ * remedy (`codex login --device-auth`) clears it next startup, and a re-probe that cannot
40
+ * decide keeps the api file so the refusal does not fail open.
41
+ * Plan and windows arrive with the first run's rollout, so the seed writes no window readings.
42
+ * Returns the account written, or null. A failed write also returns null and never throws, so
43
+ * the first codex run's observation fills the file in. */
44
+ export function seedCodexAccount(root, config, now = Date.now()) {
45
+ const existing = readCodexAccount(root, now);
46
+ // Leave an existing answer alone — a run's observation is fresher than this probe — except
47
+ // an "api" answer: the startup gate refuses on that, so it must never be sticky. Re-probe
48
+ // then, so an API-key login the operator has since fixed to a subscription (the refusal's
49
+ // named remedy, `codex login --device-auth`) clears the refusal on the next startup instead
50
+ // of a stale file refusing forever.
51
+ if (existing !== null && existing.billing !== "api")
52
+ return existing;
53
+ const billing = seedBilling(config);
54
+ if (billing === null) {
55
+ // A re-probe that cannot decide must not fail open: without an existing answer it seeds
56
+ // nothing, but a stale api answer keeps refusing — the persisted api file stays and the
57
+ // gate still reads it as a refusal rather than quietly passing (review objection 2).
58
+ return existing;
59
+ }
60
+ const account = {
61
+ billing,
62
+ observedAt: now,
63
+ windows: {},
64
+ };
65
+ try {
66
+ writeAccount(root, "codex", account);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ return account;
72
+ }
73
+ /** Parse `codex login status` stdout into the billing it names: a ChatGPT login is a
74
+ * subscription, an API-key login is API billing, and anything else (not logged in, an unknown
75
+ * string, an IP firewall prompt) is null — the seed then stays silent and the run's rollout
76
+ * decides. Shared by seedBilling below and (from part 9/9) doctor's codex auth check, so the
77
+ * two cannot disagree about what a login means. */
78
+ export function parseCodexLoginStatus(stdout) {
79
+ if (typeof stdout !== "string" || stdout === "")
80
+ return null;
81
+ if (stdout.includes("ChatGPT"))
82
+ return "subscription";
83
+ if (stdout.includes("API key"))
84
+ return "api";
85
+ return null;
86
+ }
87
+ /** The billing the login probe names, or null when it cannot be decided. */
88
+ function seedBilling(config) {
89
+ const status = probeLoginStatus(config);
90
+ if (status === null)
91
+ return null;
92
+ if (status === "api")
93
+ return "api";
94
+ if (status === "subscription")
95
+ return "subscription";
96
+ return null;
97
+ }
98
+ /** Run `codex login status` under the same scrubbed environment a real run gets (a team
99
+ * config dir's login is what it reports), bounded by a short deadline. Null when the probe
100
+ * could not run or its stdout was unreadable. */
101
+ function probeLoginStatus(config) {
102
+ const resolved = codexBackend.resolveBin(config);
103
+ const home = codexHome(resolvedBackendConfig(config, "codex"));
104
+ const env = codexEnv(process.env, home !== undefined ? { home } : {});
105
+ try {
106
+ const result = spawnSync(resolved.bin, ["login", "status"], {
107
+ env,
108
+ encoding: "utf8",
109
+ timeout: 10_000,
110
+ killSignal: "SIGKILL",
111
+ });
112
+ if (result.status !== 0)
113
+ return null;
114
+ return parseCodexLoginStatus(typeof result.stdout === "string" ? result.stdout : null);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }