tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # tumwater
2
2
 
3
- tumwater is an opinionated autonomous development harness built on
4
- [pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a fleet of
5
- role-driven loops (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
3
+ tumwater is an opinionated autonomous development team, built on
4
+ [pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a team of
5
+ roles (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
6
6
  one small, reviewed commit at a time. It came from wanting to write only the brief and let a team
7
- of always-on specialists do the rest: each loop owns one concern, lands one change per tick,
7
+ of always-on specialists do the rest: each role owns one concern, merges one change per run,
8
8
  sleeps when it has nothing to do, and wakes when main moves. All project state lives in the
9
- local git repo, and no remote is ever touched.
9
+ local git repo, and no remote is ever touched. The words used here (team, role, run, merge) are
10
+ defined in [docs/glossary.md](docs/glossary.md).
10
11
 
11
- ![The tumwater web dashboard running tumwater's own fleet: a sidebar with the project, its fleet status, the Fleet, History, Usage, Failures, and Settings views, today's spend against the cap, and the pause control; an alert that the running build is behind main; the director prompt box; today's progress (loops in flight, commits landed, ticks, open backlog); and the loops grouped by what they are doing, each with its status, current work or last result, spend, and controls](docs/gui.png)
12
+ ![The tumwater web dashboard running tumwater's own team: a sidebar with the project, its team status, and the Team, History, Usage, Failures, and Settings views, with today's spend against the cap and the pause control; the director prompt box; and today's progress (roles running, commits merged, runs, open backlog) above the roles grouped by what they are doing, each with its status, current work or last result, spend, and controls](docs/gui.png)
12
13
 
13
14
  ## Install
14
15
 
@@ -23,13 +24,16 @@ instead: `npm install && npm run build && npm link`.
23
24
 
24
25
  With `claude` installed and logged in, `npm run smoke:claude` is an opt-in live check of the
25
26
  claude backend: it drives the real CLI through a throwaway repo (a few cents of API-equivalent
26
- spend on a subscription) and is not part of `npm test`, which stays green without claude.
27
+ spend on a subscription) and is not part of `npm test`, which stays green without claude. `npm run
28
+ smoke:opencode` is the same live check for the opencode backend, driving the real `opencode` CLI
29
+ against its free DeepSeek tier; it needs an OpenCode V2 install (`brew install opencode`) and is
30
+ also outside `npm test`.
27
31
 
28
32
  ## Status
29
33
 
30
34
  <!-- tumwater:status:start -->
31
- **v0.1.1**: working harness. The director and 14 of the 15 roles are enabled by default; `telemetry`,
32
- which files harness bugs from tumwater's own event log, is opt-in (`roles.telemetry.enabled`).
35
+ **v0.1.4**: working tumwater. The director and 14 of the 15 roles are enabled by default; `selfcheck`,
36
+ which files tumwater bugs from tumwater's own event log, is opt-in (`roles.selfcheck.enabled`).
33
37
 
34
38
  Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
35
39
  [QUESTIONS.md](QUESTIONS.md) (open questions).
@@ -38,112 +42,74 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
38
42
  ## Usage
39
43
 
40
44
  ```bash
41
- cd your-project # a new or existing directory
45
+ cd your-project # a new or existing directory
42
46
  tumwater init "Build a tiny markdown-to-html converter CLI in Python."
43
- # add --template <id> to seed from a bundled starting point
44
- # (blank, python-cli, node-cli, static-site); --list-templates
45
- # prints the catalog
46
- # add --file <path> to read the brief from a file
47
- # add --adopt to adopt an existing repo as-is
48
- # a fresh/empty project is seeded with
49
- # "bootstrap": {"untilPlansDone": 5}, holding the maintenance
50
- # loops until 5 plans are done; remove "bootstrap" from
51
- # tumwater.json to end that early
52
- tumwater run # start the loops (Ctrl+C to stop)
53
- tumwater run --gui # ... and serve the browser dashboard at http://127.0.0.1:7180 from the same process
54
- tumwater run --for 2h # run for a bounded window (capped at 90d), then drain and exit like Ctrl+C would
55
- tumwater run --once # one full round of ticks, drains its landings, and exits (for cron/CI);
56
- # add --role <id> to scope the round to one loop
47
+ # --template, --file, and --adopt pick a starting point (see docs/cli.md)
48
+ tumwater start # start the team (Ctrl+C to stop)
49
+ tumwater start --gui # ... and serve the browser dashboard at http://127.0.0.1:7180
50
+ tumwater start --tui # ... and attach the terminal TUI in the same window
51
+ tumwater start --for 2h # run for a bounded window, then finish current work, summarize, and exit
52
+ tumwater start --once # one round of runs for cron/CI; exits 3 if a run failed
57
53
  ```
58
54
 
59
- Then, from another terminal:
60
-
61
- | To | Run |
62
- | --- | --- |
63
- | Watch the fleet | `tumwater tui`, or the browser dashboard at http://127.0.0.1:7180 — `tumwater gui` on its own, or `tumwater run --gui` to boot the fleet and the dashboard together |
64
- | Watch per-tick history | `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>] [--result <changes|none|problems>]`, or `tumwater history --json` for the rows as JSON; `tumwater tick <role> <n>` for one tick's full event trail (a summary header — when it ran, how long, result, usage — followed by the tick's events, oldest first; `--last` shows the newest completed tick's trail instead of numbering one; `--json` prints the payload as JSON, `null` when the log holds no such tick) |
65
- | Check state | `tumwater status`, `tumwater logs -f`, `tumwater logs --since <duration>`, `tumwater logs --grep <text>`, `tumwater logs --json` (the event feed as NDJSON, for scripts), `tumwater logs --role <id>`, `tumwater queue [--json]` (the durable land queue in execution order — one line per waiting landing with its position, loop, tick, pinned sha, age, and summary; `--json` for scripts), `tumwater backlog` (planned features, open bugs, open questions as Markdown, each held entry's heading marked `[refused]`, `[needs review]`, `[needs replan]`, or `[blocked: requires …]`), `tumwater backlog --json` (the backlog as JSON, for scripts, with a `holds.plans`/`holds.bugs` array parallel to `plans`/`bugs`), `tumwater role <id>` (one loop's standing prompt — find text, `instructions` override, resolved model and interval, enabled/paused state, its notebook — plus its next tick's assembled prompt, which shows the oldest queued prompt without consuming it (one is dequeued per tick; `--json` for scripts); `--clear-notes` clears that loop's notebook and `--notes-file <path>` (`-` for stdin) replaces it (4 KB cap); the dashboard's loop drawer edits or clears the notebook the same way, and the TUI's notebook view (Ctrl+N) clears it with Ctrl+X pressed twice) |
66
- | See a loop's pending change | `tumwater diff --role <id>` — that loop's branch's unlanded commits (with the patch) and its worktree's uncommitted edits (staged and unstaged); without `--role`, one line per loop holding pending work; `--json` prints the payload as data |
67
- | Steer the project | `tumwater prompt "prefer no third-party deps"` queues a request for the director; add `--role <id>` to aim it at one loop's next tick, `--file <path>` (`-` for stdin) to read the text from a file, or `--at <duration>` to defer it until the duration passes (45s, 90m, 1h30m, 1d; capped at 90d), `tumwater prompt --list` shows the queued prompts numbered and grouped by loop, with how long each has waited (`--json` for scripts), and `tumwater prompt --cancel <n>` removes the Nth entry as `--list` shows them, and `tumwater prompt --edit <n> "new text" [--attach <path>]` rewrites the Nth entry in place (or `--edit <n> --file <path>` takes the replacement from a file), keeping its position and wait time, and appends any `--attach` images after the ones it already carries (both take `--role <id>` when several loops share that number), and `--attach <path>` (repeatable, up to 4 images — png, jpg, jpeg, gif, webp, bmp, each at most 5 MiB) attaches an image the receiving loop reads at its next tick, whether it is queued or edited; `tumwater bug "<symptom>"` files a bug into BUGS.md's Open section and wakes the bugfix loop, `tumwater plan "<title>" [body...]` — or `--file <path>` (`-` for stdin) to file one from a markdown file, its first line the title and the rest the body, kept line for line — files a plan request into PLANS.md's Planned section and wakes the feature loop — both stamped as operator-reported (the other half of `tumwater backlog`), `--list` shows a command's own entries numbered with their eligibility marks (`--json` for scripts), and `--cancel <n>` retracts one or `--edit <n> "<text>"` rewrites one in place (or `--edit <n> --file <path>` takes the replacement from a file, keeping its lines), neither with a wake; the dashboard's plan/bug drawer cancels an entry the same way, after a confirming second click, and the TUI's backlog pane does too, with Ctrl+X pressed twice on the selected entry (Ctrl+E edits its title; Ctrl+N files a new plan or bug by title and wakes its loop), the drawer edits an entry's title and body in place, and the Plans/Bugs tabs' New form files one with an optional markdown body |
68
- | Answer open questions | `tumwater questions` lists QUESTIONS.md's open questions numbered (as the loops wrote them); `tumwater questions answer <n> "decision text"` moves the Nth entry to ## Answered with your dated answer (`--file <path>`, `-` for stdin, records a multiline decision with its lines kept; loops read the file back on their next tick); `--json` prints the list as data; the dashboard's question drawer records an answer with its lines kept, as `--file` does, and the TUI's backlog pane answers the selected question with Ctrl+A |
69
- | Control the loops | `tumwater pause [--for <duration>]` / `resume [--role <id>]` (fleet or one loop; `--for 2h` auto-resumes, capped at 90d; add `--reason <text>` on a fleet pause to state why — it shows on `status`, the TUI, and the dashboard; the TUI's Ctrl+F takes a duration and reason too, and its Ctrl+P on a loop's transcript takes a duration, and its Ctrl+W takes a duration or `cancel`), `tumwater wake` (skip backoff; `wake --in 45m` schedules the wake for 45 minutes from now — the marker is written immediately but consumed no earlier than the deadline, like `pause --for`'s auto-resume, and each scheduled wake stands on its own, so later wakes do not cancel it, while `tumwater wake --cancel [--role <id>]` drops the pending scheduled wakes (or that loop's share of them), `tumwater wake --list [--role <id>] [--json]` lists them, and the dashboard's Wake later menu and loop drawer cancel pending wakes the same way; the dashboard's loop drawer schedules one loop's wake the same way ("Wake later…"); `tumwater status` and the dashboard's loop rows show the pending wake until it fires), `tumwater reclaim [--dry-run]` (drop gitignored build outputs from every harness worktree; `--dry-run` lists the candidates and cleans nothing), `tumwater retire --role <id>` (remove a disabled loop's worktree, branch, and per-role state — use after setting `enabled: false` for that role; `--force` overrides the safety rails), `tumwater abort --role <id>`, `tumwater stop` (drain and exit, like Ctrl+C), `tumwater reset-counters [--role <id>]` (zero the per-loop counters the dashboards show, starting a fresh observation window — scheduling is untouched; the dashboard's Loops card and loop drawer reach the same reset) |
70
- | Audit | `tumwater doctor` (pre-flight, naming a standing fleet or per-role pause or quiet-hours window holding now, today's spend against the daily cap, and the maintenance allowance's rolling-24 h use; `--json` prints the report as JSON, for scripts; the dashboard's Settings view runs the same checks in its Health check card), `tumwater report` (usage and cost, with landed commits by role and the work/maintenance split; totals include landing runs — reviewer + conflict resolution), `tumwater report --role <id>` (scopes the day or `--since` report to one loop's ticks, tokens, cost, and commits; landing runs and backlog tallies are fleet-wide and omitted; with `--failures`, scopes the failure digest to that loop too), `tumwater report --since <duration>` (totals over a trailing window, capped at 7d; with `--failures`, the failure digest over that window instead), `tumwater report --json` (the `--days`/`--since`/`--failures` reports as JSON, for scripts), `tumwater report --failures` (the failure digest: per-role outcomes, each role's time and spend by outcome, and the top five loss causes ranked by agent-hours, with a marker naming how many were cut; `--since <duration>` windows the digest over a trailing span, capped at 7d, compared against the equal-length window before it) |
71
-
72
- `tumwater help` lists every command and flag; `tumwater help <command>` shows one command's
73
- usage. `gui --all-interfaces` exposes the dashboard, and with it the director prompt, to your
74
- whole network, so pair it with `--token <secret>`.
75
-
76
- Settings live in `tumwater.json`: enabled roles, model (either one selector or a map of tiers
77
- `small`/`default`/`strong` — omitted tiers inherit `default`; per-tier `fallback` overrides may
78
- name a model or `"pause"`, and a role's `model` may name a tier; see
79
- [plans/model-tiers.md](plans/model-tiers.md)),
80
- intervals, per-role `instances` (`feature` and `bugfix` may each run 1–8 parallel runners, every one
81
- with its own branch and state; default 1), the daily spend cap
82
- (`maxDailyCostUsd`, with optional per-role caps `maxDailyCostUsdPerRole` — a loop over its own
83
- cap starts no new ticks until the next local day or a live edit), a
84
- `maintenancePerWorkLanding` ratio (default 2; a rolling 24 h allowance of that many
85
- code-maintenance landings per feature/bugfix/director landing, plus a floor of 12 — when the
86
- window reaches it the scheduler holds the enabled maintenance loops until it rolls under, and a
87
- fresh `tumwater wake <role>` or a queued prompt for one admits a single tick anyway), a nightly
88
- `quietHours` window (e.g. `"23:00-07:00"` local time) during
89
- which role loops start no new ticks (the director is exempt), with optional per-role windows
90
- `quietHoursPerRole` — a loop inside its own window starts no new ticks, whether or not the
91
- fleet-wide window covers now — a `diskHoldGB` free-space floor (default 10; when the volume
92
- holding the worktrees drops below it, no new work starts until free space recovers 5 GB above it;
93
- 0 disables), a `diskReclaimGB` pressure-reclaim threshold (default 40; below it idle worktrees
94
- drop their gitignored build outputs before the hold engages; 0 disables), and a
95
- `worktreeIdleReclaimHours` window (default 24; an hourly pass drops a worktree's gitignored build
96
- outputs once it has sat unused that long, whatever the free space; 0 disables), a
97
- `worktreeSlots` count of pooled checkouts shared by role ticks and landing vets (default
98
- `maxConcurrent` + 1; the director's worktree sits outside the pool) — user-defined
99
- `customLoops`, and an
100
- optional `notify` shell command run when the fleet needs a human (a budget pause, a budget warning
101
- at 80% of the cap while the gate is still open, an error-streak
102
- breaker trip, a failed landing, a blocked restart, a posted question — the command gets
103
- `TUMWATER_EVENT_TYPE`, `TUMWATER_EVENT_LOOP`, and `TUMWATER_EVENT_MESSAGE` in its environment;
104
- verify it with `tumwater notify --test`).
105
- Edits apply live while the fleet runs.
106
- From the terminal, `tumwater config` prints the effective config as JSON, `tumwater config get
107
- <key>` reads one resolved value, `tumwater config set <key> <value>` writes one top-level
108
- key, and `tumwater config unset <key>` removes one — the inverse of `set`, so a value reverts
109
- to its default or disappears from its map or section; dotted keys
110
- (`maxDailyCostUsdPerRole.feature 1.5`, `roles.qa.model x`) merge one entry into the existing
111
- map or role entry, while bare keys replace the whole value. `roles` always merges the built-in
112
- catalog per entry at load, so a bare `config set roles` replaces the file's section but cannot
113
- disable a role by omission — disable one with `tumwater config set roles.<id>.enabled false`.
114
-
115
- **Backends:** any OpenAI-compatible model pi can reach works; set `model` in `tumwater.json` to
116
- one `provider/id[:thinking]` selector (plus `fallback` to a free selector), or a map of tiers
117
- `small`/`default`/`strong`. A role whose primary keeps failing with provider-class errors
118
- (three consecutive 429s or backend failures) runs its next ticks on its tier's resolved
119
- `fallback` pair and probes the primary once the 5-minute cooldown elapses, returning to it
120
- when a probe answers; `tumwater status`, the TUI, the dashboard, and `tumwater role <id>` name
121
- the off-model episode. Set `agent` to run bare selectors and the no-model default on another
122
- backend — `claude` runs on Claude Code, with per-tier defaults (`claude-sonnet-5-5` and
123
- friends) and per-backend `agents.claude.bin`/`agents.claude.args` overrides; a selector whose
124
- provider names a backend id (`claude/claude-opus-5-5`) always routes there. A claude run's
125
- cost comes from the CLI's own price table. A run paid by a claude.ai subscription is exempt
126
- from the daily dollar cap: its API-equivalent cost is tracked as notional spend and shown on
127
- its own line in `tumwater report`, never in the cap; a run billed to an API key — or one that
128
- saw `isUsingOverage` — counts against the cap like a priced pi model. A subscription claude
129
- model with no overage also counts as free for the budget badge and fallback decisions, so at
130
- the cap it stays put while priced pi seams move to their fallback. A rejected claude.ai
131
- subscription usage window holds subscription claude seams until it resets (a first
132
- `allowed_warning` is logged too); `tumwater status`, the TUI, the dashboard, and
133
- `tumwater role <id>` show the window in place of the daily spend on a subscription-only fleet
134
- (or beside it on a mixed one, where the daily spend covers priced seams only), and a run cut off
135
- by a rejected window keeps its session and worktree edits and resumes once the window resets.
136
- See
137
- [docs/backends.md](docs/backends.md) for requirements and a worked setup.
138
-
139
- For how the loops, review gate, scheduling, and self-redeploy work, see
55
+ A fresh or empty project is seeded with `"bootstrap": {"untilPlansDone": 5}`, which holds the
56
+ maintenance roles until 5 plans are done; remove it from `tumwater.json` to end that early.
57
+
58
+ From another terminal, `tumwater tui` or the browser dashboard watches the team, `tumwater status`
59
+ and `tumwater logs -f` check it, `tumwater brief --file <path>` replaces the brief,
60
+ `tumwater prompt "<text>"` sends a message to the director's inbox, `tumwater plan` and
61
+ `tumwater bug` file backlog entries, `tumwater revert <sha>` undoes one merged change on main,
62
+ and `tumwater pause`, `resume`, `wake`, and `stop` control the roles. `tumwater doctor` audits the team and `tumwater report` shows usage and cost. `gui
63
+ --all-interfaces` exposes the dashboard, and with it the director prompt, to your whole network,
64
+ so pair it with `--token <secret>`.
65
+
66
+ `tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
67
+ model cannot read images, the command refuses the attachment up front (naming the fix) unless
68
+ the role reroutes a blind read to a vision model, in which case it says where the message will
69
+ run; `tumwater doctor` probes each claimed image-input model once (cached 7 days, `--reprobe` to
70
+ force) and the dashboards and `tumwater role <id>` say which roles read images and where a blind
71
+ run reroutes.
72
+
73
+ Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
74
+ same list, and `tumwater completion bash|zsh|fish` prints shell completion. Settings live in
75
+ `tumwater.json` and edits apply live while the team runs; see [docs/config.md](docs/config.md).
76
+
77
+ **Backends:** any OpenAI-compatible model pi can reach works, chosen by `model` in `tumwater.json`
78
+ with a `fallback` that a failing role switches to. `backend: "claude"` runs bare model ids on Claude
79
+ Code instead, and a model id that starts with `opencode/` runs on the OpenCode CLI — its free
80
+ DeepSeek tier needs no login or API key. Requirements, the fallback, free-ness, and usage-window
81
+ behavior, and how subscription runs are billed (as estimated spend, outside the daily cap) are in
82
+ [docs/backends.md](docs/backends.md).
83
+
84
+ **Mixing providers:** a bare model id that starts with `claude-` runs on the claude CLI even
85
+ under the default `backend: "pi"`, so one config can mix a Claude strong tier with pi providers:
86
+
87
+ ```json
88
+ {
89
+ "model": {
90
+ "default": "huggingface/deepseek-ai/DeepSeek-V4.1-Flash:deepinfra",
91
+ "strong": "claude-opus-5-5"
92
+ },
93
+ "fallback": "omlx/Qwen3.8-27B-MLX-oQ4e-mtp"
94
+ }
95
+ ```
96
+
97
+ The default tier runs on pi through Hugging Face, the strong model jobs (the director, the plan
98
+ role, the reviewer, and the conflict resolver) run on the claude CLI's included usage, and the fallback runs on pi through
99
+ oMLX — all concurrently, under each provider's cap (set it with `maxConcurrentPerProvider`;
100
+ `tumwater status` shows how many runs each key has going out of its cap). A model id that starts
101
+ with `opencode/` routes to the OpenCode CLI under any backend, so its free tier can sit beside pi
102
+ and claude in one config. Prefix `anthropic/` (`anthropic/claude-…`)
103
+ to keep a Claude model on pi instead (billed per token). See [plans/agent-backends.md §3](plans/agent-backends.md).
104
+
105
+ For how the roles, review gate, scheduling, and updates work, see
140
106
  [docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
141
107
  code against human-written open source, see [docs/code-metrics.md](docs/code-metrics.md).
142
108
 
143
- ## Appendix: initial prompt
109
+ ## Appendix: the brief
144
110
 
145
- The brief this repository was started from, kept for history. The harness still reads it from
146
- between the markers below on every tick.
111
+ The brief this repository was started from, kept for history. tumwater still reads the brief from
112
+ between the markers below on every run.
147
113
 
148
114
  <!-- tumwater:prompt:start -->
149
115
  Idea: agentic harness
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "90a6c2c27eb822740efee37d84a0518b76b4d7bb",
3
- "builtAt": 1791821277046,
2
+ "sha": "cff6961c92b598be9b21cc49a6faa66a16f11a8c",
3
+ "builtAt": 1792409331797,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -0,0 +1,24 @@
1
+ // vocabulary: plain
2
+ /** Appending a backend's configured passthrough args to an already-built argv. Shared by the
3
+ * claude and opencode argv builders (claude-args.ts and opencode-args.ts), whose passthrough
4
+ * handling is otherwise identical, so the two backends reject a reserved flag by the same
5
+ * rule and the same message shape. A passthrough arg equal to a flag the harness itself sets
6
+ * is appended AFTER the harness's own copy, and both backends' parsers are last-wins, so it
7
+ * would silently override the model, the session choice, or the isolation flags — each such
8
+ * arg is rejected here with the offending arg named. `moduleName` names the blocking backend
9
+ * in the error (e.g. `claudeArgs` or `opencodeArgs`); `reserved` is that backend's
10
+ * HARNESS_*_FLAGS set. */
11
+ export function appendPassthroughArgs(args, passthrough, reserved, moduleName) {
12
+ for (const arg of passthrough ?? []) {
13
+ // Match the flag itself even when it carries its value as `--flag=value`.
14
+ const flag = arg.split("=", 1)[0];
15
+ if (reserved.has(flag)) {
16
+ // Name the reserved flag (the arg may carry it as `--flag=value`, so `flag` here is the
17
+ // normalized token) and the fix, so the operator knows exactly which passthrough to drop
18
+ // rather than which of several harness-set flags the arg collided with.
19
+ throw new Error(`${moduleName}: passthrough arg ${JSON.stringify(arg)} repeats a flag tumwater sets (` +
20
+ `${flag} is reserved) — drop it from your passthrough args`);
21
+ }
22
+ args.push(arg);
23
+ }
24
+ }
@@ -26,9 +26,9 @@ export function agentBackend(id) {
26
26
  * "Sessions"), so a claude author or gate run resumes through its ledger id instead of being
27
27
  * read as having no session. Falls back to the backend-neutral session-file check when the
28
28
  * backend is unregistered or supplies no `resumable` hook. */
29
- export function agentSessionResumable(dir, opts) {
29
+ export function agentSessionResumable(dir, opts, configDir) {
30
30
  const backend = agentBackend(opts.agent);
31
31
  if (backend?.resumable)
32
- return backend.resumable(dir, opts);
32
+ return backend.resumable(dir, opts, configDir);
33
33
  return hasResumableSession(dir);
34
34
  }
@@ -0,0 +1,60 @@
1
+ // vocabulary: plain
2
+ /**
3
+ * The per-backend capabilities that the config, prompt, watchdog, progress and doctor layers
4
+ * read (plans/opencode-backend.md "Groundwork: the claude-only branches become backend
5
+ * traits"). Pure — imports nothing from the runtime registry (agent-backend.ts) — so those
6
+ * layers can read a backend's traits without pulling the registry in. That is the same reason
7
+ * `AGENT_DEFAULT_MODELS` lives in src/config/model-selector.ts.
8
+ *
9
+ * An id with no entry reads as pi, because src/agent/run-agent.ts routes an unregistered id to
10
+ * the pi backend.
11
+ */
12
+ import { HARNESS_CLAUDE_FLAGS } from "./claude/claude-args.js";
13
+ import { HARNESS_OPENCODE_FLAGS } from "./opencode/opencode-args.js";
14
+ import { HARNESS_PI_FLAGS } from "../config/config-schema.js";
15
+ /** Whether the backend streams tool output. pi streams content-bearing tool updates, so its
16
+ * calls are classified per command; claude and opencode send none, so silence carries no hang
17
+ * signal and every call is judged by the buffered threshold (plans/agent-backends.md
18
+ * "Watchdogs and progress"). Only claude and opencode are false; an unknown id reads as pi. */
19
+ export function streamsToolOutput(agent) {
20
+ // Only claude and opencode stream no tool output; every other id (including an unregistered
21
+ // one) reads as pi, which does stream.
22
+ return agent !== "claude" && agent !== "opencode";
23
+ }
24
+ /** Whether the backend's model jobs resolve through pi's models.json. True for pi only (and an
25
+ * unknown id, which reads as pi); a claude or opencode job reads no pi pricing and needs no
26
+ * definitions file. */
27
+ export function usesPiModels(agent) {
28
+ // Only claude and opencode read no pi models.json; every other id (including an unregistered
29
+ // one) reads as pi, which does.
30
+ return agent !== "claude" && agent !== "opencode";
31
+ }
32
+ /** The reason string config validation prints for a reserved `backends.<id>.args` flag, or
33
+ * undefined when the flag is not reserved for that backend. Wraps claude's HARNESS_CLAUDE_FLAGS
34
+ * and pi's HARNESS_PI_FLAGS, keeping each backend's reason wording. The flag is the bare name —
35
+ * checkReservedArgs splits `--mode=x` on `=` before calling. */
36
+ export function reservedPassthroughFlag(agent, flag) {
37
+ switch (agent) {
38
+ case "pi":
39
+ return HARNESS_PI_FLAGS.get(flag);
40
+ case "claude":
41
+ return HARNESS_CLAUDE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
42
+ case "opencode":
43
+ return HARNESS_OPENCODE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
44
+ default:
45
+ // An unknown backend has no known harness argv to clash with, so nothing is reserved.
46
+ return undefined;
47
+ }
48
+ }
49
+ /** A seam's model as one readable token: `<provider>/<model>` for a pi seam (and any unknown
50
+ * id, which reads as pi), the bare model for claude — or "claude backend" when there is none —
51
+ * and `opencode/<model>` for opencode, whose ids all read under the opencode namespace. The
52
+ * one home of the label, shared by the doctor report and the image-input surface strings so
53
+ * they cannot drift. */
54
+ export function modelLabel(agent, provider, model) {
55
+ if (agent === "claude")
56
+ return model ?? "claude backend";
57
+ if (agent === "opencode")
58
+ return `opencode/${model ?? "?"}`;
59
+ return `${provider ?? "?"}/${model ?? "?"}`;
60
+ }
@@ -9,13 +9,15 @@
9
9
  * milliseconds (the run parser reports unix seconds; observeClaudeAccount converts once here),
10
10
  * so consumers compare it to `Date.now()` directly. */
11
11
  import { spawnSync } from "node:child_process";
12
+ import os from "node:os";
13
+ import { resolvedAgentConfig } from "../../config/config-views.js";
12
14
  import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
13
15
  import { finiteNumber, isJsonObject, parseJsonObject } from "../../files/json-object.js";
14
16
  import { claudeAccountPath } from "../../paths.js";
15
- import { claudeBackend } from "./claude-backend.js";
17
+ import { claudeBackend, claudeConfigDir } from "./claude-backend.js";
16
18
  import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
17
19
  /** True when a failed run ended on a subscription usage window the account reported as
18
- * `rejected` without overage — the signal the fleet hold throttles on (plans/agent-backends.md
20
+ * `rejected` without overage — the signal the team hold throttles on (plans/agent-backends.md
19
21
  * §8). Such a run is a transient of the world, not a failure of the session: its session and
20
22
  * worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
21
23
  * partial work or reset. API-billed claude and overage are budget/fallback matters, not a
@@ -25,6 +27,39 @@ export function subscriptionWindowRejected(run) {
25
27
  run.billing === "subscription" &&
26
28
  (run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
27
29
  }
30
+ /** The subscription account's used-up usage window, or null: a window reading `rejected` with no
31
+ * overage, or (with `includeOverage`, the default) a window whose current reading shows
32
+ * `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
33
+ * last. With a `reserve` (config `claudeWindowReserve`), a window whose utilization has reached
34
+ * its type's fraction counts too, and comes back with `reserve: true`; a rejected or overage
35
+ * window outranks a reserve one, so the flag is set only when no window is used up for real.
36
+ * The budget gate's window move (pollBudgetGate) reads the default; the team hold
37
+ * (claudeWindowObservation) passes `includeOverage: false`, because overage never holds a seam.
38
+ * Only subscription billing produces one; a missing account reads null. */
39
+ export function claudeWindowUsedUp(account, now, includeOverage = true, reserve) {
40
+ if (account?.billing !== "subscription")
41
+ return null;
42
+ let furthest = null;
43
+ let reserved = null;
44
+ for (const [type, w] of Object.entries(account.windows)) {
45
+ if (typeof w.resetsAt !== "number" || w.resetsAt <= now)
46
+ continue;
47
+ const used = w.isUsingOverage === true ? includeOverage : w.status === "rejected";
48
+ if (used) {
49
+ if (furthest === null || w.resetsAt > furthest.resetsAt)
50
+ furthest = { type, resetsAt: w.resetsAt };
51
+ continue;
52
+ }
53
+ if (windowReserved(w, reserve?.[type]) && (reserved === null || w.resetsAt > reserved.resetsAt))
54
+ reserved = { type, resetsAt: w.resetsAt, reserve: true };
55
+ }
56
+ return furthest ?? reserved;
57
+ }
58
+ /** Has this window's utilization reached its reserve fraction? False without a fraction or a
59
+ * reading. */
60
+ export function windowReserved(w, fraction) {
61
+ return fraction !== undefined && typeof w.utilization === "number" && w.utilization >= fraction;
62
+ }
28
63
  /** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
29
64
  * every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
30
65
  const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
@@ -40,6 +75,14 @@ export function claudeWindowLabel(type) {
40
75
  const word = m[1] ?? "";
41
76
  return `${words[word] ?? word}${unit}`;
42
77
  }
78
+ /** The fraction of a subscription usage window consumed (0–1) as a whole-number percent: a
79
+ * `0.64` utilization reads `64`. The one home of the fraction→percent conversion, shared by
80
+ * the status badge and the doctor's hold reason so the two render a window's fill
81
+ * identically (the browser dashboard's own window line is embedded client-side and keeps its
82
+ * inline copy). */
83
+ export function claudeUtilizationPct(utilization) {
84
+ return Math.round(utilization * 100);
85
+ }
43
86
  /** The account's windows as a list, most constrained first: by status severity, then the
44
87
  * reported utilization (a missing reading sorts below any number), then the earlier reset, then
45
88
  * the type name — so a tie always orders the same way. Empty when the account reported none. */
@@ -67,7 +110,7 @@ export function readClaudeAccount(root, now = Date.now()) {
67
110
  const doc = readJsonFile(claudeAccountPath(root));
68
111
  if (doc === null)
69
112
  return null;
70
- const billing = doc.billing === "subscription" || doc.billing === "api" ? doc.billing : undefined;
113
+ const billing = doc.billing === "subscription" || doc.billing === "api" ? (doc.billing) : undefined;
71
114
  const observedAt = finiteNumber(doc.observedAt, Number.NaN);
72
115
  if (billing === undefined || !Number.isFinite(observedAt))
73
116
  return null;
@@ -88,7 +131,7 @@ export function readClaudeAccount(root, now = Date.now()) {
88
131
  }
89
132
  /** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
90
133
  * missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
91
- * status to `reset`. */
134
+ * status to `reset` and drops the overage flags. */
92
135
  function readWindow(value, fileObservedAt, now) {
93
136
  if (!isJsonObject(value) || typeof value.status !== "string")
94
137
  return undefined;
@@ -99,12 +142,14 @@ function readWindow(value, fileObservedAt, now) {
99
142
  // non-finite fraction would render as `Infinity%` in the badges and poison the
100
143
  // claudeWindowEntries comparator (Infinity - Infinity is NaN).
101
144
  const utilization = finiteNumber(value.utilization, undefined);
145
+ const reset = resets && resetsAt <= now;
102
146
  const window = {
103
- status: resets && resetsAt <= now ? "reset" : value.status,
147
+ status: reset ? "reset" : value.status,
104
148
  ...(resets ? { resetsAt } : {}),
105
149
  ...(utilization !== undefined ? { utilization } : {}),
106
- ...(typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
107
- ...(value.isUsingOverage === true ? { isUsingOverage: true } : {}),
150
+ // a reset window is no longer in overage: dropping both keeps every reader in agreement
151
+ ...(!reset && typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
152
+ ...(!reset && value.isUsingOverage === true ? { isUsingOverage: true } : {}),
108
153
  observedAt,
109
154
  };
110
155
  return window;
@@ -225,7 +270,15 @@ export function claudeAuthUsesSubscription(auth) {
225
270
  * short deadline. Null when the probe could not run or its reply was unreadable. */
226
271
  function probeAuthStatus(config) {
227
272
  const resolved = claudeBackend.resolveBin(config);
228
- const env = claudeEnv(process.env, { quietTimeoutSeconds: 0 });
273
+ const configDir = resolvedAgentConfig(config, "claude").configDir;
274
+ const env = claudeEnv(process.env, {
275
+ quietTimeoutSeconds: 0,
276
+ // The seed probes auth under the same env a real run gets, so a team config dir's login
277
+ // is what it reports (plans/notes-command.md "Keeping funes out of team runs").
278
+ ...(configDir !== undefined
279
+ ? { configDir: claudeConfigDir(process.env, os.homedir(), configDir) }
280
+ : {}),
281
+ });
229
282
  try {
230
283
  const result = spawnSync(resolved.bin, ["auth", "status"], {
231
284
  env,
@@ -1,8 +1,9 @@
1
+ // vocabulary: plain
1
2
  /** Building the claude command line for one tick (plans/agent-backends.md "The claude
2
3
  * invocation") — pure argv construction plus the prompt-on-stdin pairing, with no subprocess,
3
4
  * 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.
5
+ * `--session-id` or `--resume` to pass); this module turns those inputs into the invocation's
6
+ * argv.
6
7
  *
7
8
  * The environment is built separately (claude-env.ts) so each concern has one module.
8
9
  *
@@ -15,12 +16,13 @@
15
16
  * fallback the design holds in reserve is not needed. */
16
17
  import fs from "node:fs";
17
18
  import path from "node:path";
19
+ import { appendPassthroughArgs } from "../agent-args.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
24
  * 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
25
+ * set for `backends.claude.args`, so a value that passes validation is never accepted by
24
26
  * validation and then thrown here. */
25
27
  export const HARNESS_CLAUDE_FLAGS = new Set([
26
28
  "-p",
@@ -68,14 +70,18 @@ function shouldAppendAgents(cwd) {
68
70
  * each would otherwise silently change what the harness asked for. */
69
71
  export function claudeArgs(opts) {
70
72
  if ((opts.sessionId === undefined) === (opts.resumeSessionId === undefined)) {
71
- throw new Error("claudeArgs: pass exactly one of sessionId or resumeSessionId");
73
+ // Name which side fired so the invariant that would otherwise silently change the run's
74
+ // session is diagnosable at a glance: a conflicting pair (both set) differs from a missing
75
+ // choice (neither) and the two bugs have different fixes.
76
+ const state = opts.sessionId === undefined ? "neither is set" : "both are set";
77
+ throw new Error(`claudeArgs: pass exactly one of sessionId or resumeSessionId (${state})`);
72
78
  }
73
79
  const args = ["-p", "--output-format", "stream-json", "--verbose", "--model", opts.model];
74
80
  if (opts.thinking !== undefined) {
75
81
  // Own-property test, not a bare index: a level like `constructor` or `toString` would
76
82
  // otherwise inherit a function from Object.prototype and bypass the rejection below.
77
83
  if (!Object.hasOwn(EFFORT_BY_THINKING, opts.thinking)) {
78
- throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)}`);
84
+ throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)} (known: ${Object.keys(EFFORT_BY_THINKING).join(", ")})`);
79
85
  }
80
86
  args.push("--effort", EFFORT_BY_THINKING[opts.thinking]);
81
87
  }
@@ -84,21 +90,12 @@ export function claudeArgs(opts) {
84
90
  else
85
91
  args.push("--resume", opts.resumeSessionId);
86
92
  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);
89
93
  args.push("--disable-slash-commands", "--exclude-dynamic-system-prompt-sections");
90
94
  if (shouldAppendAgents(opts.cwd))
91
95
  args.push("--append-system-prompt-file", "AGENTS.md");
92
96
  if (opts.maxBudgetUsd !== undefined && opts.maxBudgetUsd > 0) {
93
97
  args.push("--max-budget-usd", String(opts.maxBudgetUsd));
94
98
  }
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
- }
99
+ appendPassthroughArgs(args, opts.args, HARNESS_CLAUDE_FLAGS, "claudeArgs");
103
100
  return { args, stdin: opts.prompt };
104
101
  }