tumwater 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Bundled pi extension: bounds oversized tool results so a single read or command cannot
3
- * flood the tick's context window (PLANS.md "Bound tool output head+tail with a tumwater
3
+ * flood the run's context window (PLANS.md "Bound tool output head+tail with a tumwater
4
4
  * pi extension"). Loaded on every pi run via `-e <this file>` from src/pi/pi-args.ts.
5
5
  *
6
6
  * All bounding logic lives in pure, filesystem-free exported functions — `boundText`,
@@ -12,14 +12,16 @@
12
12
  * harness root and writes the file.
13
13
  *
14
14
  * Limits are constants on purpose (opinionated defaults over configuration). The read
15
- * limit (~12k chars ≈ 300 lines) mirrors the prompt's CONTEXT_BUDGET_RULE; the bash limit
15
+ * limit (~12k chars ≈ 300 lines) backs the prompt's contextBudgetRule, which asks for ranged
16
+ * reads past ~150 lines, the share a 64k window's scaled limit holds; the bash limit
16
17
  * is looser because test logs and build output legitimately need more tail. Both were tuned on
17
18
  * the 128k local fallback, so a model that declares a smaller window gets them scaled down in
18
19
  * proportion (`outputLimits`): a 12k-char result is twice the share of a 64k window.
19
20
  */
20
21
  import { windowScale } from "./context-window.js";
21
22
  import { writeFullOutput } from "./full-output.js";
22
- /** Char budget for a `read` tool result (~300 lines, matching CONTEXT_BUDGET_RULE). */
23
+ /** Char budget for a `read` tool result (~300 lines; contextBudgetRule asks for ranges past
24
+ * ~150). */
23
25
  export const READ_LIMIT_CHARS = 12_000;
24
26
  /** Char budget for a `bash` tool result — looser, so test/build tails survive. */
25
27
  export const BASH_LIMIT_CHARS = 16_000;
@@ -12,8 +12,8 @@
12
12
  * the note is the stop-reading backstop for a run whose remaining results were not reclaimable. (On
13
13
  * a window under 128k context-shake crosses earlier, under pi's compaction line — see its
14
14
  * `shakeLimits` — while these notes keep their percentages.) The
15
- * failure mode the numbers exist for is the fallback's local model (Qwen3.8-27B): its longest ticks
16
- * peaked at 96–110k tokens (coverage, steward, telemetry, plan; 2026-09-29..10-01) while the
15
+ * failure mode the numbers exist for is the fallback's local model (Qwen3.8-27B): its longest runs
16
+ * peaked at 96–110k tokens (coverage, steward, selfcheck, plan; 2026-09-29..10-01) while the
17
17
  * prompt's reading budget could only say "your window is finite". On a large-window model the
18
18
  * thresholds are rarely reached, so the extension costs nothing there.
19
19
  *
@@ -11,7 +11,7 @@
11
11
  * old tool results with recoverable references and keep the recent window intact. pi 1.0.0 can
12
12
  * do the same from an extension — a `turn_end` handler appends persisted `context_edit` entries
13
13
  * (pi docs, extensions.md "Events and concurrency"; session-format.md "ContextEditEntry"). On a
14
- * large-window primary (1M) the 70% line sits far above the largest team tick, so the
14
+ * large-window primary (1M) the 70% line sits far above the largest team run, so the
15
15
  * extension plans nothing and costs nothing there.
16
16
  *
17
17
  * The selection logic is a pure, exported function (`shakePlan`) so every rule is unit-testable
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * The model's declared context window, as the bundled pi extensions read it: the one home of the
4
3
  * window arithmetic bounded-output, context-shake and strip-thinking share.
@@ -14,7 +14,7 @@
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
16
  import { toolOutputDir } from "../paths.js";
17
- import { walkUp, writeTextAtomic } from "../files/files.js";
17
+ import { isDirectory, walkUp, writeTextAtomic } from "../files/files.js";
18
18
  /** Walk up from `startDir` looking for a `.tumwater/` directory — the harness root. Worktrees
19
19
  * under it (`.tumwater/worktrees/<role>`, merger, gate alike) reach it three levels up
20
20
  * (`../../..`, the repo checkout that owns `.tumwater/`); a harness root carries it directly.
@@ -22,14 +22,7 @@ import { walkUp, writeTextAtomic } from "../files/files.js";
22
22
  * terminates without walking to the filesystem root; returns null when no ancestor within
23
23
  * that bound has one, so such a run writes nothing. Exported for tests. */
24
24
  export function findTumwaterRoot(startDir = process.cwd()) {
25
- return walkUp(startDir, 63, (dir) => {
26
- try {
27
- return fs.statSync(path.join(dir, ".tumwater")).isDirectory() ? dir : null;
28
- }
29
- catch {
30
- return null; // Not here — keep walking up.
31
- }
32
- });
25
+ return walkUp(startDir, 63, (dir) => (isDirectory(path.join(dir, ".tumwater")) ? dir : null));
33
26
  }
34
27
  /** Persist a full tool output under the harness's gitignored `.tumwater/` area and return
35
28
  * its path, or null when there is no harness root (or the write fails). Files are named
@@ -41,7 +34,7 @@ export function findTumwaterRoot(startDir = process.cwd()) {
41
34
  * orchestrator's retention passes prune this directory with the team's
42
35
  * sessionRetentionDays window (paths.ts's toolOutputDir, the single home of this path that
43
36
  * this module shares instead of re-joining it), so a pointer to a full output stays readable
44
- * while its tick is recent and never accumulates forever. */
37
+ * while its run is recent and never accumulates forever. */
45
38
  export function writeFullOutput(text, toolCallId, startDir = process.cwd()) {
46
39
  const root = findTumwaterRoot(startDir);
47
40
  if (!root)
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * Bundled pi extension: on a model with a small context window, send none of the model's earlier
4
3
  * thinking back to it. Loaded on every pi run via `-e <this file>` from src/pi/pi-args.ts.
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * Bundled pi extension: send pi's system prompt without its `docs` section, on every model.
4
3
  * Loaded on every pi run via `-e <this file>` from src/pi/pi-args.ts.
@@ -6,15 +5,15 @@
6
5
  * Why: pi 1.2.0's default system prompt carries a `<docs>` section — the install paths of pi's own
7
6
  * README, docs and examples, to read "only when the user asks about pi itself". A tumwater run
8
7
  * never does, and the section is ~1,400 characters (~350 tokens) that every request re-sends,
9
- * alongside tumwater's own run prompt, which is the compact one on every model too (prompt.ts
10
- * CompactPrompt). Until 2026-10-18 both applied only at a declared window of at most 128k; they
8
+ * alongside tumwater's own run prompt, which is compact on every model too (prompt.ts). Until
9
+ * 2026-10-18 both applied only at a declared window of at most 128k; they
11
10
  * now apply everywhere to cut token spend (a 2026-10-18 audit of the team's 1M-window runs). pi
12
11
  * offers no option to leave the section out short of replacing the whole prompt, so the
13
12
  * `before_agent_start` handler returns the rendered prompt with that one section cut — the
14
13
  * preamble, tool list, tool rules and working directory are pi's own text.
15
14
  *
16
15
  * The cut is a pure exported function so it is unit-testable without pi; the default export is
17
- * the thin adapter over pi's `before_agent_start` event and `ctx.model`.
16
+ * the thin adapter over pi's `before_agent_start` event.
18
17
  */
19
18
  /** pi's docs section as rendered, with the blank lines that separate it from the section before
20
19
  * it. pi wraps each named section in a tag of the same name. */
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Bundled pi extension: refuse an `edit` or `write` that would land under the team root but
3
+ * outside the run's own worktree, and name the path inside the worktree to use instead. Loaded on
4
+ * every pi run via `-e <this file>` from src/pi/pi-args.ts.
5
+ *
6
+ * The rule lives in src/agent/write-guard.ts, shared with claude's hook. This file is the thin
7
+ * adapter over pi's `tool_call` event, which blocks a call when a handler returns
8
+ * `{ block: true, reason }` (pi docs, extensions.md "Events and concurrency"); the model gets the
9
+ * reason as the tool's error. A nested call from a codemode script goes through the same event.
10
+ * pi blocks the tool when a handler throws, so an unexpected failure here lets the write through
11
+ * rather than stopping every edit of the run: runner-run.ts's stray-write warning still sees it.
12
+ */
13
+ import { strayWriteRefusal } from "../agent/write-guard.js";
14
+ /** The file-writing tools pi ships, by the name a `tool_call` event carries. Both take `path`. */
15
+ const WRITE_TOOLS = new Set(["edit", "write"]);
16
+ /** The refusal for one `tool_call` event, or undefined to let it run. Exported for tests. */
17
+ export function guardToolCall(event, cwd) {
18
+ if (typeof event.toolName !== "string" || !WRITE_TOOLS.has(event.toolName))
19
+ return undefined;
20
+ const input = event.input;
21
+ if (typeof input !== "object" || input === null)
22
+ return undefined;
23
+ const target = input.path;
24
+ if (typeof target !== "string" || target === "")
25
+ return undefined;
26
+ try {
27
+ const reason = strayWriteRefusal(target, cwd);
28
+ return reason === null ? undefined : { block: true, reason };
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
34
+ /** The pi extension entry point. */
35
+ export default function writeGuard(pi) {
36
+ pi.on("tool_call", (event, ctx) => guardToolCall(event, typeof ctx?.cwd === "string" ? ctx.cwd : process.cwd()));
37
+ }
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /** launchservicesd's Mach-port budget on macOS: the running team's warning, driven by a watch
3
2
  * stepped from the orchestrator's poll. The daemon keeps a port for every process that ever
4
3
  * checked in with LaunchServices and never releases it, and the kernel kills it near
@@ -47,7 +46,7 @@ export function nextLaunchServicesWarning(lastWarned, ports) {
47
46
  * is already leaking says so right away — and logs a harness warning per
48
47
  * nextLaunchServicesWarning. The sample runs in the background: a poll never waits on `top`.
49
48
  * In memory only: a restarted team warns again on its first sample if the count is still high,
50
- * the right default for a condition only a reboot clears. cli/cli-run.ts builds one for a daemon
49
+ * the right default for a condition only a reboot clears. cli/cli-start.ts builds one for a daemon
51
50
  * `tumwater run` only — a `--once` round and in-process tests run without it. */
52
51
  export class LaunchServicesWatch {
53
52
  root;
@@ -0,0 +1,16 @@
1
+ /** Choosing which node binary a supervised child or self-reexec spawns, shared by the
2
+ * supervisor (spawnSupervised, src/process/supervisor.ts) and the dashboard reload
3
+ * (reexecSelf, src/redeploy/self-reload.ts). Both embed `process.execPath`, which is the
4
+ * running process's resolved, versioned node path — a Homebrew upgrade under the running team
5
+ * deletes the old keg while the team stays up, so `execPath` names a file that no longer
6
+ * exists (BUGS.md 2026-10-21). The fix is the same trick build-stage's `compileStaged` uses
7
+ * for tsc (BUGS-FIXED.md, the 2026-09-29 `tsc exited ENOENT` cluster): PATH resolves a live
8
+ * node at spawn time, so fall back to the bare `node` when the recorded binary is gone.
9
+ * Nothing spawns until a child is created, so this cheap existence check is the whole fix. */
10
+ import fs from "node:fs";
11
+ /** The node binary to pass `spawn` for a harness child: the running process's own `execPath`
12
+ * while it still exists, else the bare `node` resolved through PATH. An explicit path makes
13
+ * the deleted-keg branch testable without touching this process. */
14
+ export function liveNodeBin(execPath = process.execPath) {
15
+ return fs.existsSync(execPath) ? execPath : "node";
16
+ }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { releaseChildHandles, signalTree, withoutLaunchServicesCheckIn } from "./process.js";
2
+ import { captureCappedOutput, releaseChildHandles, signalTree, withoutLaunchServicesCheckIn } from "./process.js";
3
3
  import { errCode } from "../errno.js";
4
4
  /** The detached process-group runner: runScriptGroup starts a command in its own process
5
5
  * group — exactly like pi — and settles exactly once, bounding a timed-out tree at its
@@ -122,22 +122,12 @@ export function runScriptGroup(cmd, args, opts) {
122
122
  env: withoutLaunchServicesCheckIn(process.env),
123
123
  });
124
124
  let settled = false;
125
- let stdout = "";
126
- let stderr = "";
127
- let stdoutSize = 0;
128
- let stderrSize = 0;
129
- child.stdout?.on("data", (chunk) => {
130
- if (stdoutSize < opts.maxBuffer) {
131
- stdoutSize += chunk.length;
132
- stdout += chunk;
133
- }
134
- });
135
- child.stderr?.on("data", (chunk) => {
136
- if (stderrSize < opts.maxBuffer) {
137
- stderrSize += chunk.length;
138
- stderr += chunk;
139
- }
140
- });
125
+ // Captured output is capped at maxBuffer per stream (further chunks are dropped —
126
+ // classification reads the tail, so a chatty script can neither wedge the check nor balloon
127
+ // memory). The overflow is deliberately ignored here: truncated output still classifies.
128
+ // `captured` is read at settle time (its fields accumulate as data arrives), never
129
+ // destructured up front.
130
+ const captured = captureCappedOutput(child, opts.maxBuffer);
141
131
  const deadline = armGroupDeadline(child, {
142
132
  startedAt: spawnedAt,
143
133
  timeoutMs: opts.timeoutMs,
@@ -158,7 +148,7 @@ export function runScriptGroup(cmd, args, opts) {
158
148
  const run = { spawnedAt, settledAt: Date.now(), timeoutMs: opts.timeoutMs };
159
149
  if (timedOut)
160
150
  run.deadlineLateMs = deadline.lateMs();
161
- resolve({ ...result, timedOut, stdout, stderr, run });
151
+ resolve({ ...result, timedOut, stdout: captured.stdout, stderr: captured.stderr, run });
162
152
  }
163
153
  child.on("error", (err) => {
164
154
  // Before the deadline this is the spawn failing (npm missing from PATH). After it, it can
@@ -24,7 +24,7 @@ export function execFileAsync(file, args, options = {}) {
24
24
  * added after the abort event never fires, so `addEventListener` alone would leave the caller
25
25
  * waiting on a shutdown that has come and gone. A missing signal (no shutdown wiring)
26
26
  * registers nothing. Shared by spawnSupervised (supervisor.ts), runPi's child spawn
27
- * (src/agent/run-agent.ts), sleepInterruptible (tick-timing.ts), and abortOnShutdown
27
+ * (src/agent/run-agent.ts), sleepInterruptible (run-timing.ts), and abortOnShutdown
28
28
  * (merge-pipeline.ts).
29
29
  *
30
30
  * Returns a disposer that detaches `fn`. A `{ once: true }` listener detaches itself when the
@@ -69,7 +69,7 @@ export function pidAlive(pid) {
69
69
  * escalating to SIGKILL after 10 s if any of it is still alive. The child is spawned detached
70
70
  * (its own process group leader — runPi does this for pi), so a negative PID reaches every
71
71
  * tool-call grandchild — which a single-PID kill leaves orphaned to launchd — while the
72
- * harness's own group is never in the blast radius. Shared by the tick-timeout,
72
+ * harness's own group is never in the blast radius. Shared by the run-timeout,
73
73
  * quiet-watchdog, and harness-shutdown kills, and by the sweep every run gets once pi exits
74
74
  * (runPi's 'exit' handler), when the group holds only what pi's tool calls backgrounded.
75
75
  *
@@ -121,6 +121,45 @@ export function releaseChildHandles(child) {
121
121
  child.stderr?.destroy();
122
122
  child.unref();
123
123
  }
124
+ /** Collect a detached child's stdout/stderr, keeping each stream while it stays within
125
+ * `maxBuffer` BYTES and reporting the stream that passes the ceiling via `onOverflow` (called at
126
+ * most once per stream, on the first overflowing chunk). The shared acquisition the three
127
+ * bounded detached-child probes (execGitBounded, runScriptGroup, boundedProbe) all hand-declared
128
+ * before: git fails loudly and kills the child on overflow, the script runner keeps a truncated
129
+ * tail for classification, the doctor probe aborts and reads "could not check" — the policy stays
130
+ * in the caller, the byte counting here. The chunk arrives as
131
+ * a Buffer from a raw stdio pipe, or as a String once the caller setEncoding'd the stream; both
132
+ * are counted by BYTE length (Buffer.byteLength / Buffer.length), exactly what execFile's
133
+ * maxBuffer ceiling limits. Call BEFORE the child can emit: receivers are attached here. */
134
+ export function captureCappedOutput(child, maxBuffer, onOverflow) {
135
+ const captured = { stdout: "", stderr: "", stdoutOverflow: false, stderrOverflow: false };
136
+ const capture = (stream) => {
137
+ let bytes = 0;
138
+ child[stream]?.on("data", (chunk) => {
139
+ bytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
140
+ if (bytes <= maxBuffer) {
141
+ captured[stream] += chunk;
142
+ return;
143
+ }
144
+ // Past the ceiling: drop this and every later chunk, and call onOverflow once per stream
145
+ // (further overflowing chunks are ignored).
146
+ if (stream === "stdout") {
147
+ if (captured.stdoutOverflow)
148
+ return;
149
+ captured.stdoutOverflow = true;
150
+ }
151
+ else {
152
+ if (captured.stderrOverflow)
153
+ return;
154
+ captured.stderrOverflow = true;
155
+ }
156
+ onOverflow?.(stream);
157
+ });
158
+ };
159
+ capture("stdout");
160
+ capture("stderr");
161
+ return captured;
162
+ }
124
163
  // ── Child environments ───────────────────────────────────────────────────────────────────
125
164
  /** The child-environment policy every spawn that hands a child the harness's environment shares:
126
165
  * start from `process.env` and apply `overrides` on top. The explicit spawn sites
@@ -10,6 +10,7 @@
10
10
  import crypto from "node:crypto";
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
+ import { plural } from "../text/phrases.js";
13
14
  import { execFileAsync, withoutLaunchServicesCheckIn } from "./process.js";
14
15
  /** Every lookup normally finishes in well under a second; a wedged one must not hang
15
16
  * its caller — the same ceiling process-table.ts's probe lookups carry. */
@@ -45,7 +46,7 @@ export function runMarkerEnv(base, marker) {
45
46
  * given, is prepended to PATH — run-agent passes the notes shim dir (notesShimDir) so every
46
47
  * run finds the `tumwater-notes` command first (plans/notes-command.md "Reaching it from a
47
48
  * run"). */
48
- export function agentBaseEnv(marker, base = process.env, binDir) {
49
+ export function backendBaseEnv(marker, base = process.env, binDir) {
49
50
  const env = runMarkerEnv(withoutLaunchServicesCheckIn(base), marker);
50
51
  if (binDir) {
51
52
  const prev = env.PATH;
@@ -53,20 +54,28 @@ export function agentBaseEnv(marker, base = process.env, binDir) {
53
54
  }
54
55
  return env;
55
56
  }
56
- /** The environment variable that names a run's notebook file, exported to authoring runs as
57
+ /** The environment variable that names a run's notes file, exported to authoring runs as
57
58
  * the path the bundled `tumwater-notes` command reads and writes (notes-cli.ts). */
58
59
  const NOTES_PATH_VAR = "TUMWATER_NOTES_PATH";
59
- /** `base` plus this run's notebook as NOTES_PATH_VAR when `notesPath` is set — the agent
60
+ /** `base` plus this run's notes as NOTES_PATH_VAR when `notesPath` is set — the agent
60
61
  * adapter's run spec carries one only for authoring runs, so every other run leaves the
61
62
  * variable untouched (and an inherited value survives). Kept out of runMarkerEnv so the two
62
- * environment concerns — cross-group attribution and the notebook — stay separate; shared here
63
- * beside agentBaseEnv because all three adapters (pi, claude, opencode) build their run
63
+ * environment concerns — cross-group attribution and the notes — stay separate; shared here
64
+ * beside backendBaseEnv because all three adapters (pi, claude, opencode) build their run
64
65
  * environment from it. */
65
66
  export function withNotesPath(base, notesPath) {
66
67
  if (!notesPath)
67
68
  return base;
68
69
  return { ...base, [NOTES_PATH_VAR]: notesPath };
69
70
  }
71
+ /** The full environment every agent backend's run starts from: the composition every adapter
72
+ * (pi, claude, codex, opencode) once spelled `withNotesPath(backendBaseEnv(run.runMarker,
73
+ * undefined, run.binDir), run.notesPath)`; the adapter layers its own settings on top, so
74
+ * the shared base lives here rather than being re-composed in each. Structurally typed on
75
+ * the three `AgentRunSpec` fields it reads so run-marker (process/) need not import agent/. */
76
+ export function backendRunEnv(run) {
77
+ return withNotesPath(backendBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
78
+ }
70
79
  /** True when a `TUMWATER_RUN=` value names `marker` as one of its comma-separated runs. */
71
80
  function markerValueCarries(value, marker) {
72
81
  return value.split(",").includes(marker);
@@ -171,13 +180,24 @@ async function findMarkedPids(marker) {
171
180
  }
172
181
  return pidsMarkedInPs(await psEnvironTable(), marker);
173
182
  }
183
+ /** The warning a run logs when its end-of-run sweep had to signal `count` processes it left
184
+ * running, or undefined when there is nothing to report. A run tumwater killed (abort, deadline,
185
+ * quiet watchdog, blind image read) is excluded: its process group got SIGTERM moments before
186
+ * the sweep scans, so members still exiting would be counted and every killed run would raise
187
+ * a false leak. */
188
+ export function leftoversSweptMessage(kind, count, killed) {
189
+ if (count === 0 || killed)
190
+ return undefined;
191
+ const verb = count === 1 ? "was" : "were";
192
+ return `run sweep: ${plural(count, "process", "processes")} the ${kind} run started ${verb} still running after pi exited; sent SIGTERM`;
193
+ }
174
194
  /** SIGTERM every same-user process whose environment names `marker`, escalating to SIGKILL
175
195
  * after 10 s for any that survive — the run's cross-group leftovers: what a tool call
176
196
  * backgrounded into its own detached group, which the process-group sweep cannot reach
177
197
  * (BUGS.md 2026-09-30). Marker membership is per-run unique, so the blast radius is exactly
178
198
  * this run's tree even while sibling runs are live. Never throws: an unreadable table (no
179
199
  * ps, a wedged /proc) degrades to no sweep — the group sweep still ran — never a failed
180
- * tick. Fire-and-forget by design: the run resolves on pi's output, not on the sweep. */
200
+ * run. Fire-and-forget by design: the run resolves on pi's output, not on the sweep. */
181
201
  export async function sweepRunMarker(marker) {
182
202
  let victims;
183
203
  try {
@@ -1,10 +1,10 @@
1
- // vocabulary: plain
2
1
  import { spawn } from "node:child_process";
3
2
  import { TUMWATER_SOURCE } from "../events/events.js";
4
3
  import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
5
4
  import { childEnv, runOnAbort } from "./process.js";
6
- /** The respawn loop behind `tumwater run`. The command runs as two processes: this supervisor (the
7
- * one the operator started, holding the terminal) and a child that actually runs the orchestrator.
5
+ import { liveNodeBin } from "./node-bin.js";
6
+ /** The respawn loop behind `tumwater start`. The command runs as two processes: this supervisor
7
+ * (the one the operator started, holding the terminal) and a child that runs the orchestrator.
8
8
  * When the child rebuilt dist/ onto a newer main (src/redeploy/redeployer.ts) it exits
9
9
  * RESTART_EXIT_CODE and the supervisor respawns it — the same script path, now holding the new
10
10
  * code — so a self-hosting team picks up its own changes without anyone at the keyboard. Any
@@ -12,7 +12,7 @@ import { childEnv, runOnAbort } from "./process.js";
12
12
  * is down with nobody at the keyboard either, so that exit is recorded first (onTeamDown). Ctrl+C
13
13
  * reaches both processes from the terminal (same foreground group), so the supervisor forwards only
14
14
  * SIGTERM, which a plain `kill` delivers to it alone. */
15
- /** Marks the child: `tumwater run` with this set runs the orchestrator instead of supervising. */
15
+ /** Marks the child: `tumwater start` with this set runs the orchestrator instead of supervising. */
16
16
  export const SUPERVISED_ENV = "TUMWATER_SUPERVISED";
17
17
  /** Names the supervisor's pid in a supervised child's environment, so the child's parent-death
18
18
  * watch compares against the pid the spawner handed down instead of its own first read. A
@@ -23,9 +23,9 @@ export const SUPERVISOR_PID_ENV = "TUMWATER_SUPERVISOR_PID";
23
23
  /** Marks a `run --for` window's start (epoch ms), set once by the supervisor and handed to every
24
24
  * orchestrator generation, so a self-redeploy continues the window instead of opening a new one. */
25
25
  export const RUN_WINDOW_START_ENV = "TUMWATER_RUN_WINDOW_START";
26
- /** The exit code of a `run --once` round that finished and found failed ticks (cli-run.ts's
27
- * onceFailures): an asked-for ending, so the supervisor passes it through without paging the
28
- * team-down hook. */
26
+ /** The exit code of a `run --once` round that finished and found failed runs
27
+ * (onceFailures in src/cli/cli-start-summary.ts): an asked-for ending, so the supervisor
28
+ * passes it through without paging the team-down hook. */
29
29
  export const ONCE_FAILED_EXIT_CODE = 3;
30
30
  /** How often the supervised generation checks that its supervisor still lives (ms). */
31
31
  const PARENT_POLL_MS = 5_000;
@@ -41,7 +41,7 @@ function intervalPoll(intervalMs) {
41
41
  /** Parent-death watch for the supervised generation. The supervisor forwards only SIGTERM —
42
42
  * SIGKILL (the OOM killer, `kill -9`) cannot be trapped or forwarded, and an uncaught supervisor
43
43
  * crash forwards nothing either — so a supervisor that dies outright leaves the generation
44
- * reparented (on macOS to launchd) and ticking the team unattended, with nothing left to sweep
44
+ * reparented (on macOS to launchd) and running the team unattended, with nothing left to sweep
45
45
  * it. The watch polls the parent pid on an interval: once it changes, the supervisor is gone
46
46
  * without having stopped the team, and `onGone` runs the same graceful stop a forwarded SIGTERM
47
47
  * would have run — a killed supervisor takes its team down with it. The timer is unref'd so it
@@ -96,7 +96,7 @@ function exitCodeOf(exit) {
96
96
  }
97
97
  /** Run child generations until one exits with something other than a restart request, and
98
98
  * return the code the supervisor should exit with. Pure apart from the injected effects. */
99
- export async function superviseRun(opts, signal) {
99
+ export async function superviseStart(opts, signal) {
100
100
  const now = opts.now ?? Date.now;
101
101
  let generation = 1;
102
102
  const recentRespawns = [];
@@ -114,7 +114,7 @@ export async function superviseRun(opts, signal) {
114
114
  if (exit.code !== RESTART_EXIT_CODE || opts.stopping()) {
115
115
  // A clean exit (a child stopped by its own SIGTERM logs orchestrator_stop) and anything
116
116
  // while the operator is stopping are asked-for endings, and so is a once round that
117
- // finished with failed ticks; everything else is the team dying.
117
+ // finished with failed runs; everything else is the team dying.
118
118
  const code = exitCodeOf(exit);
119
119
  if (code !== 0 && code !== ONCE_FAILED_EXIT_CODE && !opts.stopping())
120
120
  await teamDown({ generation, exit, crashLoop: false });
@@ -163,7 +163,7 @@ export function teamDownEvent(down, diagnosis) {
163
163
  }
164
164
  /** The production child spawner: the same node binary and cli.js script as this process with
165
165
  * `run` as the command — plus any flags the operator's original invocation carried (e.g.
166
- * `tumwater run --branch x`: the child IS the orchestrator generation, so it must target the
166
+ * `tumwater start --branch x`: the child IS the orchestrator generation, so it must target the
167
167
  * same branch) — stdio shared (the child's event stream is what the operator sees), and
168
168
  * SUPERVISED_ENV set so it runs the orchestrator instead of supervising again, plus
169
169
  * SUPERVISOR_PID_ENV naming this supervisor so the child's own parent-death watch fires even if
@@ -171,7 +171,7 @@ export function teamDownEvent(down, diagnosis) {
171
171
  * RUN_WINDOW_START_ENV for a `--for` window. `stdio` defaults to `"inherit"` (the child's
172
172
  * event stream is what a plain `run` operator sees); `run --tui` passes silent stdio instead,
173
173
  * because the observer TUI owns the terminal and the child must not paint over its render. */
174
- export function spawnRunChild(signal, extraArgs = [], env = {}, stdio = "inherit") {
174
+ export function spawnStartChild(signal, extraArgs = [], env = {}, stdio = "inherit") {
175
175
  // Spawn `run`, never `start`: the supervisor runs the build it started on, but each child
176
176
  // loads dist/src/cli.js, which a redeploy replaces with main's newest build. Every build from
177
177
  // the `start` rename on accepts `run`, and older builds accept only `run`, so `run` works
@@ -183,21 +183,22 @@ export function spawnRunChild(signal, extraArgs = [], env = {}, stdio = "inherit
183
183
  ...env,
184
184
  }, stdio);
185
185
  }
186
- /** The production dashboard spawner for `run --gui`: a `tumwater gui` child on its defaults,
187
- * stdio shared so a terminal Ctrl+C reaches it exactly as it reaches the orchestrator child —
188
- * unless `--tui` is hosted, when the caller passes silent stdio so the dashboard's
189
- * `tumwater gui at http://…` banner cannot paint over the observer TUI's full-screen render.
186
+ /** The production dashboard spawner for `run --gui`: a `tumwater gui` child (on its defaults, or
187
+ * on `--port <n>` when `extraArgs` carries the port), stdio shared so a terminal Ctrl+C reaches
188
+ * it exactly as it reaches the orchestrator child — unless `--tui` is hosted, when the caller
189
+ * passes silent stdio so the dashboard's `tumwater gui at http://…` banner cannot paint over the
190
+ * observer TUI's full-screen render.
190
191
  * `env` carries the caller's mark — in production DASHBOARD_CHILD_ENV naming this supervisor's
191
192
  * pid — so the child's own reload watch (src/redeploy/self-reload.ts) both exits
192
193
  * RESTART_EXIT_CODE for a fresh generation on a redeploy and exits on its own if this supervisor
193
194
  * dies outright (SIGKILL, a crash), freeing the dashboard port. Deliberately not SUPERVISED_ENV:
194
- * the child must run `gui`, not supervise a team. Resolves like spawnRunChild — a failed spawn
195
+ * the child must run `gui`, not supervise a team. Resolves like spawnStartChild — a failed spawn
195
196
  * is `{ code: 1, signal: null }` — so an EMFILE/ENOMEM here cannot throw an unhandled 'error'. */
196
- export function spawnGuiChild(signal, env = {}, stdio = "inherit") {
197
- return spawnSupervised(["gui"], signal, env, stdio);
197
+ export function spawnGuiChild(signal, env = {}, stdio = "inherit", extraArgs = []) {
198
+ return spawnSupervised(["gui", ...extraArgs], signal, env, stdio);
198
199
  }
199
200
  /** Spawn one supervised child (`commandArgs` after the cli.js script path) and resolve on its
200
- * exit, with the abort signal wiring and spawn-failure handling shared by spawnRunChild and
201
+ * exit, with the abort signal wiring and spawn-failure handling shared by spawnStartChild and
201
202
  * spawnGuiChild: the signal's abort sends SIGTERM, and a spawn that never starts resolves as a
202
203
  * failed exit instead of emitting an unhandled 'error'. */
203
204
  function spawnSupervised(commandArgs, signal, env, stdio = "inherit") {
@@ -207,7 +208,7 @@ function spawnSupervised(commandArgs, signal, env, stdio = "inherit") {
207
208
  resolve({ code: 1, signal: null });
208
209
  return;
209
210
  }
210
- const child = spawn(process.execPath, [script, ...commandArgs], {
211
+ const child = spawn(liveNodeBin(), [script, ...commandArgs], {
211
212
  stdio,
212
213
  env: childEnv(env),
213
214
  });
@@ -1,8 +1,7 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * The only backend-specific prompt text (plans/agent-backends.md "What the pi extensions did,
4
3
  * per backend"). Everything else in the prompts already speaks in shell commands and "tool
5
- * calls"; what differs between pi and claude is the name of the notebook tool and how each
4
+ * calls"; what differs between pi and claude is the name of the notes tool and how each
6
5
  * backend's oversized tool output reaches the model, so those live behind a `PromptDialect`
7
6
  * that prompt.ts and prompt-followup.ts take as an optional argument.
8
7
  *
@@ -41,6 +40,21 @@ export const OPENCODE_DIALECT = {
41
40
  "Run every shell command in the foreground: never set `background`, because a background command is killed when your run ends.",
42
41
  ],
43
42
  };
43
+ /** codex's dialect (plans/codex-backend.md §6): it has no read tool (so the reading-budget
44
+ * rule names only `sed -n`/`head`/`tail`), its oversized tool output comes truncated around a
45
+ * `…N tokens truncated…` marker (finding 18), and two backend rules tell the model how a
46
+ * longer command works: codex returns a session id to poll with `write_stdin`, and ends the
47
+ * command when the turn ends (finding 19); and all file edits go through `apply_patch` (finding
48
+ * 6), because the write guard checks only that tool (plans/codex-write-guard.md). */
49
+ export const CODEX_DIALECT = {
50
+ readRange: "`sed -n 'A,Bp'` (or `head` / `tail`)",
51
+ oversizedOutput: `Oversized command output comes back as its head and tail around a \`…N tokens truncated…\`
52
+ marker — narrow the command (grep, \`sed -n\`, \`head\`/\`tail\`) instead of re-running it whole.`,
53
+ backendRules: [
54
+ "A command that runs longer than 30 seconds returns a session id while it keeps running. Keep calling `write_stdin` with that session id until it reports an exit code, and never end your turn while a command is still running: Codex kills it when you finish, and its result is lost.",
55
+ "Edit and create files with the `apply_patch` tool, not shell redirection (`>`, `>>`, `tee`) or `sed -i`.",
56
+ ],
57
+ };
44
58
  /** The dialect table keyed by backend id: every backend but claude speaks pi's dialect (and
45
59
  * an unknown id falls back to pi because run-agent.ts routes an unregistered id to the pi
46
60
  * backend). Adding a backend means one row here, not another branch. */
@@ -48,6 +62,7 @@ const DIALECTS = {
48
62
  pi: PI_DIALECT,
49
63
  claude: CLAUDE_DIALECT,
50
64
  opencode: OPENCODE_DIALECT,
65
+ codex: CODEX_DIALECT,
51
66
  };
52
67
  /** The dialect for an agent id, read from the per-backend table. */
53
68
  export function dialectFor(agent) {