tumwater 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/README.md +94 -25
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +34 -0
  4. package/dist/src/agent/claude/claude-account.js +243 -0
  5. package/dist/src/agent/claude/claude-args.js +104 -0
  6. package/dist/src/agent/claude/claude-backend.js +162 -0
  7. package/dist/src/agent/claude/claude-env.js +84 -0
  8. package/dist/src/agent/claude/claude-stream.js +384 -0
  9. package/dist/src/agent/mcp/role-notes-server.js +160 -0
  10. package/dist/src/agent/role-notes-write.js +65 -0
  11. package/dist/src/agent/run-agent.js +439 -0
  12. package/dist/src/agent/session-ledger.js +169 -0
  13. package/dist/src/backlog/backlog-eligibility.js +195 -0
  14. package/dist/src/{backlog.js → backlog/backlog-md.js} +86 -82
  15. package/dist/src/backlog/backlog-render.js +28 -0
  16. package/dist/src/backlog/backlog-structure.js +355 -0
  17. package/dist/src/backlog/backlog-write.js +653 -0
  18. package/dist/src/backlog/backlog.js +88 -0
  19. package/dist/src/baseline/main-baseline.js +357 -0
  20. package/dist/src/{main-red.js → baseline/main-red.js} +64 -50
  21. package/dist/src/{readme.js → brief.js} +17 -7
  22. package/dist/src/{budget.js → budget/budget.js} +62 -28
  23. package/dist/src/{fallback-breaker.js → budget/fallback-breaker.js} +60 -10
  24. package/dist/src/build/build-check-counts.js +58 -0
  25. package/dist/src/{build-check-detect.js → build/build-check-detect.js} +34 -47
  26. package/dist/src/{build-check-events.js → build/build-check-events.js} +69 -10
  27. package/dist/src/{build-check-report.js → build/build-check-report.js} +9 -9
  28. package/dist/src/{build-check-scoped.js → build/build-check-scoped.js} +40 -40
  29. package/dist/src/{build-check.js → build/build-check.js} +11 -8
  30. package/dist/src/{build-info.js → build/build-info.js} +12 -6
  31. package/dist/src/{build-stage.js → build/build-stage.js} +105 -30
  32. package/dist/src/{dep-install.js → build/dep-install.js} +14 -7
  33. package/dist/src/{host-sleep.js → build/host-sleep.js} +6 -4
  34. package/dist/src/{change-data.js → change/change-data.js} +21 -18
  35. package/dist/src/{ui/change-preview.js → change/change-render.js} +14 -7
  36. package/dist/src/{cli-args.js → cli/cli-args.js} +113 -37
  37. package/dist/src/{cli-command-args.js → cli/cli-command-args.js} +167 -85
  38. package/dist/src/cli/cli-flag-specs.js +321 -0
  39. package/dist/src/cli/cli-marker-commands.js +44 -0
  40. package/dist/src/cli/cli-output.js +63 -0
  41. package/dist/src/cli/cli-query-commands.js +249 -0
  42. package/dist/src/cli/cli-repo-gate.js +15 -0
  43. package/dist/src/cli/cli-run.js +376 -0
  44. package/dist/src/cli/config-commands.js +131 -0
  45. package/dist/src/cli/help.js +348 -0
  46. package/dist/src/{ui → cli}/log-commands.js +23 -23
  47. package/dist/src/cli/notify-command.js +68 -0
  48. package/dist/src/{question-commands.js → cli/question-commands.js} +87 -50
  49. package/dist/src/cli.js +143 -268
  50. package/dist/src/collections.js +77 -0
  51. package/dist/src/{check-permit.js → concurrency/check-permit.js} +14 -15
  52. package/dist/src/concurrency/lock.js +236 -0
  53. package/dist/src/{semaphore.js → concurrency/semaphore.js} +44 -18
  54. package/dist/src/config/config-editable-keys.js +26 -0
  55. package/dist/src/{config-example.js → config/config-example.js} +33 -17
  56. package/dist/src/config/config-field-checks.js +216 -0
  57. package/dist/src/{config-live.js → config/config-live.js} +50 -17
  58. package/dist/src/{config-schema.js → config/config-schema.js} +47 -4
  59. package/dist/src/config/config-validation.js +468 -0
  60. package/dist/src/config/config-views.js +528 -0
  61. package/dist/src/config/config-write.js +641 -0
  62. package/dist/src/{config.js → config/config.js} +123 -31
  63. package/dist/src/config/model-selector.js +74 -0
  64. package/dist/src/{doctor-backlog.js → doctor/doctor-backlog.js} +35 -38
  65. package/dist/src/doctor/doctor-checks.js +457 -0
  66. package/dist/src/doctor/doctor-holds.js +122 -0
  67. package/dist/src/doctor/doctor-launch-services.js +22 -0
  68. package/dist/src/doctor/doctor-model-checks.js +396 -0
  69. package/dist/src/{doctor-orphans.js → doctor/doctor-orphans.js} +12 -20
  70. package/dist/src/doctor/doctor-render.js +13 -0
  71. package/dist/src/{doctor.js → doctor/doctor.js} +31 -10
  72. package/dist/src/doctor/outcome.js +16 -0
  73. package/dist/src/errno.js +1 -1
  74. package/dist/src/events/event-format.js +446 -0
  75. package/dist/src/{event-read.js → events/event-read.js} +59 -25
  76. package/dist/src/{event-window.js → events/event-window.js} +106 -18
  77. package/dist/src/{events.js → events/events.js} +67 -34
  78. package/dist/src/{notify.js → events/notify.js} +26 -14
  79. package/dist/src/{failure-cluster.js → failure/failure-cluster.js} +52 -19
  80. package/dist/src/failure/failure-data.js +356 -0
  81. package/dist/src/{failure-report.js → failure/failure-render.js} +110 -66
  82. package/dist/src/{failure-state-change.js → failure/failure-state-change.js} +65 -26
  83. package/dist/src/failure/time-spend.js +240 -0
  84. package/dist/src/{file-queue.js → files/file-queue.js} +1 -1
  85. package/dist/src/files/files.js +376 -0
  86. package/dist/src/{json-files.js → files/json-files.js} +18 -15
  87. package/dist/src/files/json-object.js +94 -0
  88. package/dist/src/{stat-cache.js → files/stat-cache.js} +16 -3
  89. package/dist/src/{tail.js → files/tail.js} +14 -13
  90. package/dist/src/{error-storm.js → fleet/error-storm.js} +9 -10
  91. package/dist/src/{failure-spread.js → fleet/failure-spread.js} +7 -8
  92. package/dist/src/{fleet-hold.js → fleet/fleet-hold.js} +77 -21
  93. package/dist/src/fleet/fleet-polls.js +207 -0
  94. package/dist/src/{fleet-state.js → fleet/fleet-state.js} +98 -56
  95. package/dist/src/fleet/orchestrator-info.js +150 -0
  96. package/dist/src/fleet/reclaim.js +299 -0
  97. package/dist/src/gates/bootstrap-gates.js +101 -0
  98. package/dist/src/gates/budget-gates.js +218 -0
  99. package/dist/src/gates/disk-gate.js +78 -0
  100. package/dist/src/gates/gate-polls.js +283 -0
  101. package/dist/src/gates/maintenance-quota.js +198 -0
  102. package/dist/src/{pause-gates.js → gates/pause-gates.js} +10 -10
  103. package/dist/src/gates/readiness.js +11 -0
  104. package/dist/src/gates/role-cap-gates.js +135 -0
  105. package/dist/src/gates/startup-gate.js +124 -0
  106. package/dist/src/{streak-gate.js → gates/streak-gate.js} +31 -33
  107. package/dist/src/{commit-message.js → git/commit-message.js} +49 -14
  108. package/dist/src/{git-diff.js → git/git-diff.js} +81 -46
  109. package/dist/src/git/git-run.js +237 -0
  110. package/dist/src/git/git.js +488 -0
  111. package/dist/src/git/slots-state.js +109 -0
  112. package/dist/src/git/worktree-pool.js +399 -0
  113. package/dist/src/git/worktree-use.js +173 -0
  114. package/dist/src/git/worktree.js +263 -0
  115. package/dist/src/git/xcrun-git.js +24 -0
  116. package/dist/src/{ui → gui}/gui-args.js +68 -18
  117. package/dist/src/{ui/gui.js → gui/gui-command.js} +5 -8
  118. package/dist/src/gui/gui-endpoint-commands.js +671 -0
  119. package/dist/src/gui/gui-endpoints.js +322 -0
  120. package/dist/src/gui/gui-server.js +270 -0
  121. package/dist/src/{ui → gui}/http-body.js +35 -16
  122. package/dist/src/{history-data.js → history/history-data.js} +161 -68
  123. package/dist/src/{ui → history}/history.js +59 -60
  124. package/dist/src/inbox/inbox-attachments.js +286 -0
  125. package/dist/src/inbox/inbox-cancel.js +118 -0
  126. package/dist/src/inbox/inbox-edit.js +144 -0
  127. package/dist/src/{inbox-submit.js → inbox/inbox-submit.js} +46 -22
  128. package/dist/src/inbox/inbox.js +371 -0
  129. package/dist/src/inbox/pending-prompt.js +148 -0
  130. package/dist/src/inbox/prompt-commands.js +222 -0
  131. package/dist/src/inbox/prompt-not-before.js +61 -0
  132. package/dist/src/{init-templates.js → init/init-templates.js} +78 -4
  133. package/dist/src/{init.js → init/init.js} +89 -92
  134. package/dist/src/landing/backlog-conflicts.js +116 -0
  135. package/dist/src/{landing-batch.js → landing/landing-batch.js} +112 -73
  136. package/dist/src/{landing-check-failures.js → landing/landing-check-failures.js} +66 -40
  137. package/dist/src/{landing-core.js → landing/landing-core.js} +109 -23
  138. package/dist/src/landing/landing-diff.js +57 -0
  139. package/dist/src/{landing-drain.js → landing/landing-drain.js} +66 -17
  140. package/dist/src/{landing-git.js → landing/landing-git.js} +78 -18
  141. package/dist/src/landing/landing-merge.js +308 -0
  142. package/dist/src/{landing-pipeline.js → landing/landing-pipeline.js} +33 -9
  143. package/dist/src/landing/landing-questions.js +16 -0
  144. package/dist/src/landing/landing-queue-render.js +31 -0
  145. package/dist/src/{landing-queue.js → landing/landing-queue.js} +60 -20
  146. package/dist/src/{landing-slot.js → landing/landing-slot.js} +80 -40
  147. package/dist/src/{landing-stack.js → landing/landing-stack.js} +35 -21
  148. package/dist/src/{landing-vetting.js → landing/landing-vetting.js} +27 -22
  149. package/dist/src/{leftover.js → loop/leftover.js} +53 -20
  150. package/dist/src/loop/loop-fallback.js +66 -0
  151. package/dist/src/loop/loop-pi.js +370 -0
  152. package/dist/src/loop/loop-recovery.js +90 -0
  153. package/dist/src/loop/loop-state.js +195 -0
  154. package/dist/src/loop/loop-tick.js +539 -0
  155. package/dist/src/loop/loop.js +311 -0
  156. package/dist/src/loop/model-fallback.js +75 -0
  157. package/dist/src/loop/revision.js +61 -0
  158. package/dist/src/{operator-commands.js → operator/operator-commands.js} +140 -43
  159. package/dist/src/{operator-intent.js → operator/operator-intent.js} +133 -42
  160. package/dist/src/operator/operator-requests.js +340 -0
  161. package/dist/src/operator/retire.js +145 -0
  162. package/dist/src/orchestrator/orchestrator-launch.js +159 -0
  163. package/dist/src/orchestrator/orchestrator-scheduling.js +304 -0
  164. package/dist/src/{orchestrator.js → orchestrator/orchestrator.js} +196 -185
  165. package/dist/src/{retention.js → orchestrator/retention.js} +15 -6
  166. package/dist/src/paths.js +135 -22
  167. package/dist/src/{command-shape.js → pi/command-shape.js} +15 -3
  168. package/dist/src/{pi-args.js → pi/pi-args.js} +8 -6
  169. package/dist/src/pi/pi-backend.js +90 -0
  170. package/dist/src/pi/pi-bin.js +138 -0
  171. package/dist/src/{pi-event-line.js → pi/pi-event-line.js} +26 -25
  172. package/dist/src/pi/pi-models.js +197 -0
  173. package/dist/src/pi/pi-run-result.js +7 -0
  174. package/dist/src/{pi-stream.js → pi/pi-stream.js} +252 -48
  175. package/dist/src/pi/pi-watchdogs.js +193 -0
  176. package/dist/src/pi/pi.js +9 -0
  177. package/dist/src/pi-extension/bounded-output.js +5 -51
  178. package/dist/src/pi-extension/context-budget.js +44 -37
  179. package/dist/src/pi-extension/context-shake.js +241 -0
  180. package/dist/src/pi-extension/context-usage.js +20 -0
  181. package/dist/src/pi-extension/full-output.js +62 -0
  182. package/dist/src/pi-extension/role-notes.js +40 -0
  183. package/dist/src/pi-extension/tool-result-content.js +15 -0
  184. package/dist/src/{launch-services.js → process/launch-services.js} +16 -30
  185. package/dist/src/{process-group.js → process/process-group.js} +79 -51
  186. package/dist/src/{process-table.js → process/process-table.js} +23 -13
  187. package/dist/src/{process.js → process/process.js} +66 -22
  188. package/dist/src/{run-marker.js → process/run-marker.js} +22 -9
  189. package/dist/src/process/supervisor.js +205 -0
  190. package/dist/src/prompt/dialect.js +35 -0
  191. package/dist/src/prompt/gate-prompts.js +409 -0
  192. package/dist/src/prompt/principles.js +14 -0
  193. package/dist/src/{prompt-followup.js → prompt/prompt-followup.js} +31 -9
  194. package/dist/src/{prompt.js → prompt/prompt.js} +111 -51
  195. package/dist/src/{redeploy-policy.js → redeploy/redeploy-policy.js} +15 -11
  196. package/dist/src/{redeploy-probes.js → redeploy/redeploy-probes.js} +5 -2
  197. package/dist/src/{redeploy.js → redeploy/redeploy.js} +24 -18
  198. package/dist/src/{redeployer.js → redeploy/redeployer.js} +46 -18
  199. package/dist/src/{self-reload.js → redeploy/self-reload.js} +29 -3
  200. package/dist/src/report/report-data.js +425 -0
  201. package/dist/src/{ui → report}/report-render.js +62 -35
  202. package/dist/src/{ui → report}/report.js +32 -19
  203. package/dist/src/request-timeouts.js +6 -0
  204. package/dist/src/review/known-flakes.js +45 -0
  205. package/dist/src/{review-followup.js → review/review-followup.js} +40 -44
  206. package/dist/src/{review-precheck.js → review/review-precheck.js} +38 -20
  207. package/dist/src/review/review-verdict.js +177 -0
  208. package/dist/src/{review.js → review/review.js} +193 -71
  209. package/dist/src/{suite-rerun.js → review/suite-rerun.js} +6 -5
  210. package/dist/src/roles/loop-ids.js +60 -0
  211. package/dist/src/{role-catalog.js → roles/role-catalog.js} +175 -29
  212. package/dist/src/{role-guidance.js → roles/role-guidance.js} +23 -10
  213. package/dist/src/{ui/role-report.js → roles/role-render.js} +49 -13
  214. package/dist/src/{role-view.js → roles/role-view.js} +44 -17
  215. package/dist/src/{roles.js → roles/roles.js} +49 -22
  216. package/dist/src/{backoff.js → scheduling/backoff.js} +13 -3
  217. package/dist/src/scheduling/claims.js +149 -0
  218. package/dist/src/{once-round.js → scheduling/once-round.js} +3 -6
  219. package/dist/src/{quiet-hours.js → scheduling/quiet-hours.js} +30 -11
  220. package/dist/src/{scheduling.js → scheduling/scheduling.js} +33 -43
  221. package/dist/src/{work-landed-cache.js → scheduling/work-landed-cache.js} +4 -4
  222. package/dist/src/status/status-data.js +272 -0
  223. package/dist/src/status/status-polls.js +202 -0
  224. package/dist/src/{datetime.js → text/datetime.js} +70 -16
  225. package/dist/src/text/format.js +58 -0
  226. package/dist/src/text/markdown.js +30 -0
  227. package/dist/src/text/phrases.js +228 -0
  228. package/dist/src/text/suggest.js +66 -0
  229. package/dist/src/{text-width.js → text/text-width.js} +1 -1
  230. package/dist/src/text/text.js +199 -0
  231. package/dist/src/{qa-coverage.js → tick/qa-coverage.js} +14 -6
  232. package/dist/src/tick/stage-check.js +298 -0
  233. package/dist/src/{telemetry-digest.js → tick/telemetry-digest.js} +6 -6
  234. package/dist/src/{tick-apply.js → tick/tick-apply.js} +69 -15
  235. package/dist/src/{tick-detail-data.js → tick/tick-detail-data.js} +9 -8
  236. package/dist/src/tick/tick-detail.js +122 -0
  237. package/dist/src/{tick-finalize.js → tick/tick-finalize.js} +59 -17
  238. package/dist/src/tick/tick-prompt.js +201 -0
  239. package/dist/src/tick/tick-resume.js +115 -0
  240. package/dist/src/tick/tick-stage.js +193 -0
  241. package/dist/src/{tick-timing.js → tick/tick-timing.js} +67 -63
  242. package/dist/src/tick/tick-usage.js +142 -0
  243. package/dist/src/tick/tick-verdict.js +200 -0
  244. package/dist/src/ui/badges.js +155 -24
  245. package/dist/src/ui/fleet-alerts.js +35 -16
  246. package/dist/src/ui/{gui-client-boot.js → gui/gui-client-boot.js} +101 -7
  247. package/dist/src/ui/{gui-client-composer.js → gui/gui-client-composer.js} +51 -14
  248. package/dist/src/ui/gui/gui-client-drawer.js +392 -0
  249. package/dist/src/ui/gui/gui-client-fleet.js +568 -0
  250. package/dist/src/ui/{gui-client-history.js → gui/gui-client-history.js} +84 -37
  251. package/dist/src/ui/{gui-client-loops.js → gui/gui-client-loops.js} +98 -12
  252. package/dist/src/ui/{gui-client-model.js → gui/gui-client-model.js} +48 -21
  253. package/dist/src/ui/{gui-client-operator.js → gui/gui-client-operator.js} +95 -25
  254. package/dist/src/ui/gui/gui-client-pending.js +109 -0
  255. package/dist/src/ui/gui/gui-client-report.js +420 -0
  256. package/dist/src/ui/gui/gui-client-settings.js +122 -0
  257. package/dist/src/ui/{gui-client.js → gui/gui-client.js} +116 -19
  258. package/dist/src/ui/{gui-page.js → gui/gui-page.js} +15 -7
  259. package/dist/src/ui/{gui-styles.js → gui/gui-styles.js} +11 -5
  260. package/dist/src/{progress-data.js → ui/progress-data.js} +92 -60
  261. package/dist/src/ui/status-model.js +140 -149
  262. package/dist/src/ui/status-payload.js +88 -31
  263. package/dist/src/ui/status-render.js +58 -20
  264. package/dist/src/ui/tick-progress-model.js +117 -0
  265. package/dist/src/ui/tone.js +28 -9
  266. package/dist/src/ui/transcript-tail.js +38 -14
  267. package/dist/src/ui/transcript.js +49 -40
  268. package/dist/src/ui/{tui-app.js → tui/tui-app.js} +2 -4
  269. package/dist/src/ui/{tui-backlog.js → tui/tui-backlog.js} +11 -8
  270. package/dist/src/ui/tui/tui-frame.js +162 -0
  271. package/dist/src/ui/tui/tui-history.js +29 -0
  272. package/dist/src/ui/{tui-input.js → tui/tui-input.js} +108 -90
  273. package/dist/src/ui/tui/tui-keys.js +1783 -0
  274. package/dist/src/ui/tui/tui-pane.js +175 -0
  275. package/dist/src/ui/tui/tui-prompt-history.js +80 -0
  276. package/dist/src/ui/{tui.js → tui/tui.js} +53 -73
  277. package/dist/src/{fix-claim.js → verdict/fix-claim.js} +62 -47
  278. package/dist/src/{refusal.js → verdict/refusal.js} +11 -5
  279. package/dist/src/{reply-contract.js → verdict/reply-contract.js} +54 -42
  280. package/dist/src/version.js +3 -3
  281. package/package.json +3 -1
  282. package/dist/src/backlog-structure.js +0 -166
  283. package/dist/src/budget-gates.js +0 -128
  284. package/dist/src/build-check-counts.js +0 -36
  285. package/dist/src/cli-flag-specs.js +0 -185
  286. package/dist/src/cli-output.js +0 -44
  287. package/dist/src/cli-run.js +0 -243
  288. package/dist/src/config-commands.js +0 -58
  289. package/dist/src/config-field-checks.js +0 -132
  290. package/dist/src/config-validation.js +0 -307
  291. package/dist/src/config-views.js +0 -106
  292. package/dist/src/config-write.js +0 -217
  293. package/dist/src/doctor-checks.js +0 -301
  294. package/dist/src/event-format.js +0 -261
  295. package/dist/src/failure-data.js +0 -216
  296. package/dist/src/files.js +0 -168
  297. package/dist/src/fleet-polls.js +0 -113
  298. package/dist/src/gate-polls.js +0 -150
  299. package/dist/src/gate-prompts.js +0 -210
  300. package/dist/src/git.js +0 -333
  301. package/dist/src/help.js +0 -185
  302. package/dist/src/inbox-attachments.js +0 -114
  303. package/dist/src/inbox.js +0 -392
  304. package/dist/src/json-object.js +0 -31
  305. package/dist/src/landing-merge.js +0 -285
  306. package/dist/src/lock.js +0 -165
  307. package/dist/src/loop-pi.js +0 -218
  308. package/dist/src/loop-state.js +0 -44
  309. package/dist/src/loop.js +0 -541
  310. package/dist/src/main-baseline.js +0 -195
  311. package/dist/src/operator-requests.js +0 -114
  312. package/dist/src/orchestrator-launch.js +0 -119
  313. package/dist/src/pending-prompt.js +0 -96
  314. package/dist/src/phrases.js +0 -116
  315. package/dist/src/pi-models.js +0 -114
  316. package/dist/src/pi.js +0 -333
  317. package/dist/src/prompt-commands.js +0 -188
  318. package/dist/src/rank.js +0 -18
  319. package/dist/src/readiness.js +0 -78
  320. package/dist/src/report-data.js +0 -186
  321. package/dist/src/review-verdict.js +0 -80
  322. package/dist/src/role-cap-gates.js +0 -76
  323. package/dist/src/startup-gate.js +0 -88
  324. package/dist/src/status-data.js +0 -133
  325. package/dist/src/status-polls.js +0 -133
  326. package/dist/src/supervisor.js +0 -127
  327. package/dist/src/text.js +0 -183
  328. package/dist/src/tick-prompt.js +0 -120
  329. package/dist/src/tick-resume.js +0 -54
  330. package/dist/src/tick-stage.js +0 -95
  331. package/dist/src/tick-usage.js +0 -67
  332. package/dist/src/tick-verdict.js +0 -114
  333. package/dist/src/time-spend.js +0 -176
  334. package/dist/src/ui/backlog-report.js +0 -36
  335. package/dist/src/ui/doctor-report.js +0 -9
  336. package/dist/src/ui/gui-client-drawer.js +0 -219
  337. package/dist/src/ui/gui-client-fleet.js +0 -308
  338. package/dist/src/ui/gui-client-report.js +0 -276
  339. package/dist/src/ui/gui-client-settings.js +0 -70
  340. package/dist/src/ui/gui-endpoint-commands.js +0 -296
  341. package/dist/src/ui/gui-endpoints.js +0 -142
  342. package/dist/src/ui/gui-server.js +0 -198
  343. package/dist/src/ui/tick-detail.js +0 -87
  344. package/dist/src/ui/tui-frame.js +0 -111
  345. package/dist/src/ui/tui-keys.js +0 -439
  346. package/dist/src/worktree.js +0 -154
  347. /package/dist/src/{exemptions.js → review/exemptions.js} +0 -0
  348. /package/dist/src/{tick-outcome.js → tick/tick-outcome.js} +0 -0
  349. /package/dist/src/ui/{gui-client-markdown.js → gui/gui-client-markdown.js} +0 -0
  350. /package/dist/src/ui/{gui-client-sound.js → gui/gui-client-sound.js} +0 -0
  351. /package/dist/src/ui/{gui-icons.js → gui/gui-icons.js} +0 -0
  352. /package/dist/src/ui/{tui-keymap.js → tui/tui-keymap.js} +0 -0
  353. /package/dist/src/{no-change.js → verdict/no-change.js} +0 -0
package/README.md CHANGED
@@ -10,10 +10,26 @@ local git repo, and no remote is ever touched.
10
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
12
 
13
+ ## Install
14
+
15
+ Requires Node 20.3 or later on macOS or Linux.
16
+
17
+ ```bash
18
+ npm install -g tumwater
19
+ ```
20
+
21
+ Or run any command without installing: `npx tumwater`. To run from a checkout of this repo
22
+ instead: `npm install && npm run build && npm link`.
23
+
24
+ With `claude` installed and logged in, `npm run smoke:claude` is an opt-in live check of the
25
+ 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
+
13
28
  ## Status
14
29
 
15
30
  <!-- tumwater:status:start -->
16
- **v0.1**: working harness. All 13 roles and the director are enabled by default.
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`).
17
33
 
18
34
  Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
19
35
  [QUESTIONS.md](QUESTIONS.md) (open questions).
@@ -22,8 +38,6 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
22
38
  ## Usage
23
39
 
24
40
  ```bash
25
- npm install -g tumwater # or run any command with npx tumwater
26
- # from a checkout: npm install && npm run build && npm link
27
41
  cd your-project # a new or existing directory
28
42
  tumwater init "Build a tiny markdown-to-html converter CLI in Python."
29
43
  # add --template <id> to seed from a bundled starting point
@@ -31,41 +45,96 @@ tumwater init "Build a tiny markdown-to-html converter CLI in Python."
31
45
  # prints the catalog
32
46
  # add --file <path> to read the brief from a file
33
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
34
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
35
57
  ```
36
58
 
37
59
  Then, from another terminal:
38
60
 
39
61
  | To | Run |
40
62
  | --- | --- |
41
- | Watch the fleet | `tumwater tui`, or `tumwater gui` for the browser dashboard at http://127.0.0.1:7180 |
42
- | Watch per-tick history | `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>]`, 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; `--json` prints the payload as JSON, `null` when the log holds no such tick) |
43
- | 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 backlog` (planned features, open bugs, open questions as Markdown), `tumwater backlog --json` (the backlog as JSON, for scripts), `tumwater role <id>` (one loop's standing prompt — find text, `instructions` override, resolved model and interval, enabled/paused state — plus its next tick's assembled prompt, which shows the oldest queued prompt without consuming it (one is dequeued per tick; `--json` for scripts)) |
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) |
44
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 |
45
- | 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, `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 (add `--role <id>` when several loops share that number) |
46
- | 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 (loops read the file back on their next tick); `--json` prints the list as data |
47
- | 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), `tumwater wake` (skip backoff), `tumwater abort --role <id>`, `tumwater stop` (drain and exit, like Ctrl+C) |
48
- | Audit | `tumwater doctor` (pre-flight; `--json` prints the report as JSON, for scripts), `tumwater report` (usage and cost; totals include landing runs — reviewer + conflict resolution), `tumwater report --since <duration>` (totals over a trailing window, capped at 7d), `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) |
49
-
50
- `tumwater help` lists every command and flag; `tumwater help <command>` shows one command's usage. `gui --all-interfaces` exposes the dashboard, and
51
- with it the director prompt, to your whole network, so pair it with `--token <secret>`.
52
-
53
- Settings live in `tumwater.json`: enabled roles, model, intervals, the daily spend cap
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
54
82
  (`maxDailyCostUsd`, with optional per-role caps `maxDailyCostUsdPerRole` — a loop over its own
55
- cap starts no new ticks until the next local day or a live edit), a nightly `quietHours` window
56
- (e.g. `"23:00-07:00"` local time) during
57
- which role loops start no new ticks (the director is exempt), user-defined `customLoops`, and an
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
58
100
  optional `notify` shell command run when the fleet needs a human (a budget pause, a budget warning
59
101
  at 80% of the cap while the gate is still open, an error-streak
60
- breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER_EVENT_TYPE`,
61
- `TUMWATER_EVENT_LOOP`, and `TUMWATER_EVENT_MESSAGE` in its environment).
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`).
62
105
  Edits apply live while the fleet runs.
63
106
  From the terminal, `tumwater config` prints the effective config as JSON, `tumwater config get
64
- <key>` reads one resolved value, and `tumwater config set <key> <value>` writes one top-level
65
- key.
66
-
67
- **Backends:** any OpenAI-compatible model pi can reach works; set `provider` and `model` in
68
- `tumwater.json`. See [docs/backends.md](docs/backends.md) for requirements and a worked setup.
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.
69
138
 
70
139
  For how the loops, review gate, scheduling, and self-redeploy work, see
71
140
  [docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "4c69994273d9f4d707b0c5a6e7d00f50a3b265e6",
3
- "builtAt": 1791110618512,
2
+ "sha": "90a6c2c27eb822740efee37d84a0518b76b4d7bb",
3
+ "builtAt": 1791821277046,
4
4
  "root": "/home/runner/work/tumwater/tumwater"
5
5
  }
@@ -0,0 +1,34 @@
1
+ /** The backend seam (plans/agent-backends.md "The seam"): what one non-interactive agent
2
+ * run needs from its backend — how to build the command, how to translate its native stdout
3
+ * into tumwater's canonical event stream, and how to classify its exit. src/agent/run-agent.ts
4
+ * owns everything backend-neutral (spawn, marker, watchdogs, abort, group kill and marker
5
+ * sweep, stderr capture); a backend registered here supplies the rest. src/agent/run-agent.ts
6
+ * registers the pi and claude adapters here.
7
+ *
8
+ * The registry is keyed by id and fed by explicit registration (src/agent/run-agent.ts
9
+ * registers both adapters at load), so a test can register a fake backend without importing a
10
+ * real one. */
11
+ import { hasResumableSession } from "./session-ledger.js";
12
+ /** The backend id used when a config names none: pi, the harness's default backend. */
13
+ export const DEFAULT_AGENT = "pi";
14
+ const registry = new Map();
15
+ /** Register one backend under its id, replacing any previous entry with the same id. */
16
+ export function registerAgentBackend(backend) {
17
+ registry.set(backend.id, backend);
18
+ }
19
+ /** The backend registered under `id`, or undefined when none is — the caller decides whether
20
+ * an unknown id is a hard error. */
21
+ export function agentBackend(id) {
22
+ return registry.get(id);
23
+ }
24
+ /** Whether the backend for `opts.agent` can resume a session in `dir` for a run of
25
+ * `opts.kind`: the one resume guard every resume point uses (plans/agent-backends.md
26
+ * "Sessions"), so a claude author or gate run resumes through its ledger id instead of being
27
+ * read as having no session. Falls back to the backend-neutral session-file check when the
28
+ * backend is unregistered or supplies no `resumable` hook. */
29
+ export function agentSessionResumable(dir, opts) {
30
+ const backend = agentBackend(opts.agent);
31
+ if (backend?.resumable)
32
+ return backend.resumable(dir, opts);
33
+ return hasResumableSession(dir);
34
+ }
@@ -0,0 +1,243 @@
1
+ /** The claude account state: which billing pays for claude runs, and the newest subscription
2
+ * usage-window reading each run reported (plans/agent-backends.md §7 "Billing" and §8 "The
3
+ * subscription usage window"). Persisted at `.tumwater/state/claude-account.json`, written only
4
+ * by the orchestrator process, so the sibling parts that throttle or display the subscription
5
+ * window (6b/7–6e/7) read one answer instead of re-deriving it per surface.
6
+ *
7
+ * The file is a cache, never a source of truth: a run that reports billing overwrites it, and a
8
+ * missing or unreadable file reads as "no subscription". `resetsAt` is stored as epoch
9
+ * milliseconds (the run parser reports unix seconds; observeClaudeAccount converts once here),
10
+ * so consumers compare it to `Date.now()` directly. */
11
+ import { spawnSync } from "node:child_process";
12
+ import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
13
+ import { finiteNumber, isJsonObject, parseJsonObject } from "../../files/json-object.js";
14
+ import { claudeAccountPath } from "../../paths.js";
15
+ import { claudeBackend } from "./claude-backend.js";
16
+ import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
17
+ /** 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
19
+ * §8). Such a run is a transient of the world, not a failure of the session: its session and
20
+ * worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
21
+ * partial work or reset. API-billed claude and overage are budget/fallback matters, not a
22
+ * window hold, so they read false; a pi 429 reports no window and also reads false. */
23
+ export function subscriptionWindowRejected(run) {
24
+ return (run.transientRateLimit &&
25
+ run.billing === "subscription" &&
26
+ (run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
27
+ }
28
+ /** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
29
+ * every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
30
+ const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
31
+ /** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
32
+ * type falls back to its words with underscores as spaces. The one home of the window spelling
33
+ * the status badge and the role view share. */
34
+ export function claudeWindowLabel(type) {
35
+ const m = /^(\w+)_(hour|day|week)$/.exec(type);
36
+ if (!m)
37
+ return type.replace(/_/g, " ");
38
+ const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
39
+ const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
40
+ const word = m[1] ?? "";
41
+ return `${words[word] ?? word}${unit}`;
42
+ }
43
+ /** The account's windows as a list, most constrained first: by status severity, then the
44
+ * reported utilization (a missing reading sorts below any number), then the earlier reset, then
45
+ * the type name — so a tie always orders the same way. Empty when the account reported none. */
46
+ export function claudeWindowEntries(account) {
47
+ return Object.entries(account.windows)
48
+ .map(([type, w]) => ({ type, ...w }))
49
+ .sort((a, b) => {
50
+ const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
51
+ if (rank !== 0)
52
+ return rank;
53
+ const util = (b.utilization ?? -1) - (a.utilization ?? -1);
54
+ if (util !== 0)
55
+ return util;
56
+ const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
57
+ if (reset !== 0)
58
+ return reset;
59
+ return a.type.localeCompare(b.type);
60
+ });
61
+ }
62
+ /** Read the persisted account state, or null when it is absent or unreadable. A file without a
63
+ * usable top-level `observedAt` reads as absent (the plan's torn-file rule), and a window whose
64
+ * `resetsAt` is at or before `now` reads back with `status: "reset"` — the reader, not every
65
+ * consumer, decides that the window no longer limits. */
66
+ export function readClaudeAccount(root, now = Date.now()) {
67
+ const doc = readJsonFile(claudeAccountPath(root));
68
+ if (doc === null)
69
+ return null;
70
+ const billing = doc.billing === "subscription" || doc.billing === "api" ? doc.billing : undefined;
71
+ const observedAt = finiteNumber(doc.observedAt, Number.NaN);
72
+ if (billing === undefined || !Number.isFinite(observedAt))
73
+ return null;
74
+ const windows = {};
75
+ if (isJsonObject(doc.windows)) {
76
+ for (const [type, value] of Object.entries(doc.windows)) {
77
+ const window = readWindow(value, observedAt, now);
78
+ if (window !== undefined)
79
+ setWindow(windows, type, window);
80
+ }
81
+ }
82
+ return {
83
+ billing,
84
+ ...(typeof doc.subscriptionType === "string" ? { subscriptionType: doc.subscriptionType } : {}),
85
+ observedAt,
86
+ windows,
87
+ };
88
+ }
89
+ /** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
90
+ * missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
91
+ * status to `reset`. */
92
+ function readWindow(value, fileObservedAt, now) {
93
+ if (!isJsonObject(value) || typeof value.status !== "string")
94
+ return undefined;
95
+ const observedAt = finiteNumber(value.observedAt, fileObservedAt);
96
+ const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
97
+ const resets = Number.isFinite(resetsAt);
98
+ // utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
99
+ // non-finite fraction would render as `Infinity%` in the badges and poison the
100
+ // claudeWindowEntries comparator (Infinity - Infinity is NaN).
101
+ const utilization = finiteNumber(value.utilization, undefined);
102
+ const window = {
103
+ status: resets && resetsAt <= now ? "reset" : value.status,
104
+ ...(resets ? { resetsAt } : {}),
105
+ ...(utilization !== undefined ? { utilization } : {}),
106
+ ...(typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
107
+ ...(value.isUsingOverage === true ? { isUsingOverage: true } : {}),
108
+ observedAt,
109
+ };
110
+ return window;
111
+ }
112
+ /** Store one window under its `rateLimitType`. A plain `windows[type] = window` loses a type of
113
+ * `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's prototype
114
+ * instead of adding an own entry, so the window vanishes from every `Object.entries`/`values`
115
+ * reader (the display, the hold, and accountUsesOverage). Defining the own property keeps even
116
+ * that key — the same inherited-key guard claude-stream's TOOL_NAMES lookup uses. */
117
+ function setWindow(windows, type, window) {
118
+ Object.defineProperty(windows, type, { value: window, enumerable: true, writable: true, configurable: true });
119
+ }
120
+ /** Overwrite the account file with `account`, creating the state directory. The orchestrator's
121
+ * one writer; a failure to write is the caller's to handle (best-effort at the tick boundary).
122
+ * Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write leaves the
123
+ * previous file whole, not a torn one the reader heals to "no subscription" — which would
124
+ * silently drop the billing answer and the window hold until the next run re-observed them. */
125
+ export function writeClaudeAccount(root, account) {
126
+ writeJsonAtomic(claudeAccountPath(root), account, true);
127
+ }
128
+ /** Fold one run's billing and rate-window readings into the account file: the run's `billing`
129
+ * overwrites the stored answer, and each reported window replaces the stored reading for its
130
+ * `type`, stamped with `observedAt`. A run that reported neither billing nor windows leaves the
131
+ * file untouched (and returns whatever was already there). */
132
+ export function observeClaudeAccount(root, run, now = Date.now()) {
133
+ const existing = readClaudeAccount(root, now);
134
+ const billing = run.billing ?? existing?.billing;
135
+ const reported = run.rateWindows ?? [];
136
+ if (billing === undefined || (run.billing === undefined && reported.length === 0))
137
+ return existing;
138
+ const windows = { ...(existing?.windows ?? {}) };
139
+ for (const w of reported)
140
+ setWindow(windows, w.type, windowFromRun(w, now));
141
+ const account = {
142
+ billing,
143
+ ...(existing?.subscriptionType !== undefined ? { subscriptionType: existing.subscriptionType } : {}),
144
+ observedAt: now,
145
+ windows,
146
+ };
147
+ writeClaudeAccount(root, account);
148
+ return account;
149
+ }
150
+ /** One run's rate-window reading in the file's shape: unix-seconds `resetsAt` becomes epoch ms,
151
+ * and absent optional fields stay absent. */
152
+ function windowFromRun(w, now) {
153
+ // The write choke point heals utilization like readWindow does, so a non-finite value
154
+ // handed straight to observeClaudeAccount can never re-enter the file through a save.
155
+ const utilization = finiteNumber(w.utilization, undefined);
156
+ return {
157
+ status: w.status,
158
+ ...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt * 1000 } : {}),
159
+ ...(utilization !== undefined ? { utilization } : {}),
160
+ ...(typeof w.overageStatus === "string" ? { overageStatus: w.overageStatus } : {}),
161
+ ...(w.isUsingOverage === true ? { isUsingOverage: true } : {}),
162
+ observedAt: now,
163
+ };
164
+ }
165
+ /** Seed the account file before the first run (plans/agent-backends.md §7): `ANTHROPIC_API_KEY`
166
+ * set means claude prefers the key, so billing is API; otherwise `claude auth status` decides —
167
+ * a claude.ai OAuth login is a subscription, any other login is API. An existing file is left
168
+ * alone, and a probe that cannot run or is not logged in leaves the file absent
169
+ * (readClaudeAccount then reads "no subscription"). Returns the account written, or null. */
170
+ export function seedClaudeAccount(root, config, now = Date.now()) {
171
+ const existing = readClaudeAccount(root, now);
172
+ if (existing !== null)
173
+ return existing;
174
+ const billing = seedBilling(config);
175
+ if (billing === null)
176
+ return null;
177
+ const account = {
178
+ billing: billing.billing,
179
+ ...(billing.subscriptionType !== undefined ? { subscriptionType: billing.subscriptionType } : {}),
180
+ observedAt: now,
181
+ windows: {},
182
+ };
183
+ writeClaudeAccount(root, account);
184
+ return account;
185
+ }
186
+ /** The billing the environment implies, or null when it cannot be decided: an API key wins, a
187
+ * third-party provider env (`CLAUDE_CODE_USE_BEDROCK`/`_VERTEX`/`_FOUNDRY`) is API billing, and
188
+ * otherwise `claude auth status` names the login. */
189
+ function seedBilling(config) {
190
+ const apiKey = process.env.ANTHROPIC_API_KEY;
191
+ if (typeof apiKey === "string" && apiKey !== "")
192
+ return { billing: "api" };
193
+ if (claudeThirdPartyProvider(process.env))
194
+ return { billing: "api" };
195
+ const auth = probeAuthStatus(config);
196
+ if (!auth?.loggedIn)
197
+ return null;
198
+ const subscription = claudeAuthUsesSubscription(auth);
199
+ return subscription
200
+ ? { billing: "subscription", ...(auth.subscriptionType !== undefined ? { subscriptionType: auth.subscriptionType } : {}) }
201
+ : { billing: "api" };
202
+ }
203
+ /** Parse `claude auth status` stdout, returning null when it is not a JSON object or does not
204
+ * report a boolean `loggedIn` — the one home of the read, shared by this module's
205
+ * probeAuthStatus and doctor's claudeAuthProbe (doctor-model-checks.ts). An optional string
206
+ * field reads as absent when the reply names something else. */
207
+ export function parseClaudeAuthStatus(stdout) {
208
+ const doc = parseJsonObject(stdout);
209
+ if (doc === null || typeof doc.loggedIn !== "boolean")
210
+ return null;
211
+ return {
212
+ loggedIn: doc.loggedIn,
213
+ subscriptionType: typeof doc.subscriptionType === "string" ? doc.subscriptionType : undefined,
214
+ authMethod: typeof doc.authMethod === "string" ? doc.authMethod : undefined,
215
+ };
216
+ }
217
+ /** True when an auth-status reply names claude.ai subscription billing — a subscription type
218
+ * (its presence alone signals one) or the explicit claude.ai auth method — rather than API
219
+ * billing. Shared by seedBilling below and doctor's checkClaudeAuth (doctor-model-checks.ts),
220
+ * which must not disagree about which seams are subscription-billed. */
221
+ export function claudeAuthUsesSubscription(auth) {
222
+ return auth.subscriptionType !== undefined || auth.authMethod === "claude.ai";
223
+ }
224
+ /** Run `claude auth status` under the same scrubbed environment a real run gets, bounded by a
225
+ * short deadline. Null when the probe could not run or its reply was unreadable. */
226
+ function probeAuthStatus(config) {
227
+ const resolved = claudeBackend.resolveBin(config);
228
+ const env = claudeEnv(process.env, { quietTimeoutSeconds: 0 });
229
+ try {
230
+ const result = spawnSync(resolved.bin, ["auth", "status"], {
231
+ env,
232
+ encoding: "utf8",
233
+ timeout: 10_000,
234
+ killSignal: "SIGKILL",
235
+ });
236
+ if (result.status !== 0 || typeof result.stdout !== "string")
237
+ return null;
238
+ return parseClaudeAuthStatus(result.stdout);
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
@@ -0,0 +1,104 @@
1
+ /** Building the claude command line for one tick (plans/agent-backends.md "The claude
2
+ * invocation") — pure argv construction plus the prompt-on-stdin pairing, with no subprocess,
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.
6
+ *
7
+ * The environment is built separately (claude-env.ts) so each concern has one module.
8
+ *
9
+ * `NODE_OPTIONS` preload decision (2026-10-09): claude 2.1.295 on macOS is a single-file
10
+ * Mach-O native executable (`file` reports `Mach-O 64-bit executable arm64`) and ignores
11
+ * `NODE_OPTIONS` — it exits 0 and prints its version both with tumwater's `--import=data:`
12
+ * preload and with a deliberately broken `--require` path. So the preload stays on claude's
13
+ * environment: claude itself is unaffected, while the Bash tool's `npm` grandchildren inherit
14
+ * it and skip their LaunchServices check-in (src/process/process.ts). The `--settings` Bash-env
15
+ * fallback the design holds in reserve is not needed. */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ /** Flags the harness passes itself. A passthrough arg equal to one of these is appended AFTER
19
+ * the harness's own copy, and claude's argument parser is last-wins — so it would silently
20
+ * override the isolation flags, the session choice, or the model the harness resolved. Each is
21
+ * 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. */
25
+ export const HARNESS_CLAUDE_FLAGS = new Set([
26
+ "-p",
27
+ "--print",
28
+ "--output-format",
29
+ "--verbose",
30
+ "--model",
31
+ "--effort",
32
+ "--session-id",
33
+ "--resume",
34
+ "-r",
35
+ "--continue",
36
+ "-c",
37
+ "--permission-mode",
38
+ "--tools",
39
+ "--setting-sources",
40
+ "--strict-mcp-config",
41
+ "--mcp-config",
42
+ "--disable-slash-commands",
43
+ "--exclude-dynamic-system-prompt-sections",
44
+ "--append-system-prompt-file",
45
+ "--max-budget-usd",
46
+ ]);
47
+ /** claude's effort levels are pi's thinking levels with `off`/`minimal` clamped to `low`:
48
+ * claude has no lower setting, and doctor notes the clamp (plans/agent-backends.md "Config").
49
+ * `off` and `minimal` are the only remapped entries; the rest are 1:1. */
50
+ const EFFORT_BY_THINKING = {
51
+ off: "low",
52
+ minimal: "low",
53
+ low: "low",
54
+ medium: "medium",
55
+ high: "high",
56
+ xhigh: "xhigh",
57
+ max: "max",
58
+ };
59
+ /** True when the worktree has an `AGENTS.md` and no `CLAUDE.md`: claude does not read
60
+ * `AGENTS.md` on its own, so the adapter appends it — but only when the repo has no
61
+ * `CLAUDE.md`, which `--setting-sources project` already loads (finding 1). */
62
+ function shouldAppendAgents(cwd) {
63
+ return fs.existsSync(path.join(cwd, "AGENTS.md")) && !fs.existsSync(path.join(cwd, "CLAUDE.md"));
64
+ }
65
+ /** Build one run's claude argv and stdin. The prompt is never an argv element (claude reads
66
+ * `-p` input from stdin). Throws on an unusable invocation — no session choice, both session
67
+ * choices, an unknown thinking level, or a passthrough arg that repeats a harness flag — since
68
+ * each would otherwise silently change what the harness asked for. */
69
+ export function claudeArgs(opts) {
70
+ if ((opts.sessionId === undefined) === (opts.resumeSessionId === undefined)) {
71
+ throw new Error("claudeArgs: pass exactly one of sessionId or resumeSessionId");
72
+ }
73
+ const args = ["-p", "--output-format", "stream-json", "--verbose", "--model", opts.model];
74
+ if (opts.thinking !== undefined) {
75
+ // Own-property test, not a bare index: a level like `constructor` or `toString` would
76
+ // otherwise inherit a function from Object.prototype and bypass the rejection below.
77
+ if (!Object.hasOwn(EFFORT_BY_THINKING, opts.thinking)) {
78
+ throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)}`);
79
+ }
80
+ args.push("--effort", EFFORT_BY_THINKING[opts.thinking]);
81
+ }
82
+ if (opts.sessionId !== undefined)
83
+ args.push("--session-id", opts.sessionId);
84
+ else
85
+ 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);
89
+ args.push("--disable-slash-commands", "--exclude-dynamic-system-prompt-sections");
90
+ if (shouldAppendAgents(opts.cwd))
91
+ args.push("--append-system-prompt-file", "AGENTS.md");
92
+ if (opts.maxBudgetUsd !== undefined && opts.maxBudgetUsd > 0) {
93
+ args.push("--max-budget-usd", String(opts.maxBudgetUsd));
94
+ }
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
+ }
103
+ return { args, stdin: opts.prompt };
104
+ }