tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) 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 +42 -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 +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +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 +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -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 +1 -2
  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 +118 -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 +150 -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 +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,11 +1,11 @@
1
1
  import { eventsSince } from "../events/event-window.js";
2
2
  import { isNotifyFailure } from "../events/notify.js";
3
- import { humanSeconds } from "../text/datetime.js";
3
+ import { DAY_MS, humanAgo } from "../text/datetime.js";
4
4
  import { plural } from "../text/phrases.js";
5
5
  import { errorMessage, squash } from "../text/text.js";
6
6
  import { okOutcome, warnOutcome } from "./outcome.js";
7
7
  /** How far back the `notify` check looks for failed pages. */
8
- const NOTIFY_CHECK_WINDOW_MS = 24 * 3600_000;
8
+ const NOTIFY_CHECK_WINDOW_MS = DAY_MS;
9
9
  /** The `notify` check: whether a notify command is configured, and a warning when it failed to
10
10
  * page within the last day (a nonzero exit, a kill, or a failed start — the warnings
11
11
  * spawnNotifyCommand logs). Warns but never fails, like the hold rows: a dead webhook must not
@@ -18,8 +18,7 @@ export function checkNotify(root, config, now = Date.now()) {
18
18
  if (failures.length === 0)
19
19
  return okOutcome("configured — no failed pages in the last 24h");
20
20
  const latest = failures[failures.length - 1];
21
- const age = humanSeconds(Math.max(0, Math.round((now - latest.ts) / 1000)));
22
- return warnOutcome(`${plural(failures.length, "failed page")} in the last 24h; latest ${age} ago: ${squash(String(latest.message), 200)}`);
21
+ return warnOutcome(`${plural(failures.length, "failed page")} in the last 24h; latest ${humanAgo(latest.ts, now)}: ${squash(String(latest.message), 200)}`);
23
22
  }
24
23
  catch (err) {
25
24
  return warnOutcome(`could not read the event log for failed pages: ${errorMessage(err)}`);
@@ -0,0 +1,162 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { childEnv } from "../process/process.js";
4
+ import { boundedProbe } from "./probe.js";
5
+ import { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
6
+ import { opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
7
+ import { opencodeDbPath } from "../paths.js";
8
+ import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes } from "../files/files.js";
9
+ import { formatBytes, tokenWindowLabel } from "../text/format.js";
10
+ import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
11
+ import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, loadConfigForCheck, opencodeSeamsInUse, seamedModels, } from "./doctor-model-checks.js";
12
+ /** The doctor's opencode credential and leak readiness check, split out of doctor-model-checks.ts
13
+ * (whose shared seam/model framework and the pi, tier-models and image-input checks stay there):
14
+ * checkOpencode and its helpers (opencodeProviderOf, opencodeAuthLists, jsonKeyPresent,
15
+ * opencodeConfigLeaks). This is the doctor's only check that spawns the `opencode` CLI and reads
16
+ * its catalog and the operator's global opencode config — a distinct concern from the
17
+ * filesystem/git checks beside it, kept together so a change to how doctor reads opencode does
18
+ * not touch the other model checks. The shared report contract (CheckOutcome) and the
19
+ * seam/model framework (loadConfigForCheck, opencodeSeamsInUse, seamPairs, the AUTH_CHECK_*
20
+ * budgets) come from doctor-model-checks.ts and outcome.ts, as the other doctor-* modules do. */
21
+ /** The provider of a non-gateway opencode model id (`opencode/<provider>/<model>`), or
22
+ * undefined for a gateway id (a bare `opencode/<model>` or `<model>`, served by OpenCode's
23
+ * own gateway with no credential). A model id with more than the `opencode/` namespace prefix
24
+ * is provider-qualified and needs a credential OpenCode lists. */
25
+ function opencodeProviderOf(model) {
26
+ const parts = model.split("/");
27
+ if (parts[0] === "opencode" && parts.length >= 3)
28
+ return parts[1];
29
+ if (parts[0] !== "opencode" && parts.length >= 2)
30
+ return parts[0];
31
+ return undefined;
32
+ }
33
+ /** Whether `opencode auth list --format json` (bounded) lists `provider` exactly. A probe that
34
+ * cannot run, exits nonzero, or omits the provider reads as NOT listed — the safe direction,
35
+ * since doctor is warning the operator to add the credential, never confirming one it did not
36
+ * see. The list is matched EXACTLY, not as a raw substring: when the output parses as an array
37
+ * (of names or of per-credential objects), a credential listed as `anthropic` must never vouch
38
+ * for `anthropic-foo` twice over; when it cannot be parsed as JSON (a shape the CLI may change),
39
+ * the text is matched word-bounded so a truncated or prose line still reads. */
40
+ async function opencodeAuthLists(config, provider, pathEnv, timeoutMs) {
41
+ const resolved = opencodeBackend.resolveBin(config);
42
+ const result = await boundedProbe(resolved.bin, ["auth", "list", "--format", "json"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
43
+ if (result?.code !== 0)
44
+ return false;
45
+ const text = result.stdout;
46
+ try {
47
+ const parsed = JSON.parse(text);
48
+ if (Array.isArray(parsed)) {
49
+ return parsed.some((item) => {
50
+ if (typeof item === "string")
51
+ return item === provider;
52
+ return (item !== null &&
53
+ typeof item === "object" &&
54
+ Object.values(item).some((v) => v === provider));
55
+ });
56
+ }
57
+ }
58
+ catch {
59
+ // Not an array-shaped JSON payload — fall through to the word-bounded text match.
60
+ }
61
+ const esc = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
62
+ return new RegExp(`(^|[^A-Za-z0-9_-])${esc}([^A-Za-z0-9_-]|$)`).test(text);
63
+ }
64
+ /** Whether a JSON-ish key (optionally quoted, followed by a colon) appears in a config file's
65
+ * text — a loose match that tolerates JSONC comments without parsing them. */
66
+ function jsonKeyPresent(text, key) {
67
+ return new RegExp(`["']?${key}["']?\\s*:`).test(text);
68
+ }
69
+ /** The operator's global opencode config files and AGENTS.md (plans/opencode-backend.md §7
70
+ * "Leaks", part 7/7): the operator's own config and instructions still load into team runs,
71
+ * so each MCP server (disabled for team runs) or plugin/agent/instructions entry or the
72
+ * AGENTS.md file itself produces one warning naming the file and what leaks in: an MCP server
73
+ * is switched off by the harness, while a plugin/agent/instructions entry or AGENTS.md is NOT
74
+ * switched off and still loads. `configHome` is the config root to scan — the XDG config home
75
+ * (when set) or `~/.config`, the SAME folder opencode's own operatorMcpNames reads, so the leak
76
+ * scan matches the config a run actually loads. Config files
77
+ * are JSON/C, so the keys are matched loosely (a word followed by a colon) rather than fully
78
+ * parsed — enough to name what still loads before a run inherits it. */
79
+ function opencodeConfigLeaks(configHome) {
80
+ const dir = path.join(configHome, "opencode");
81
+ const warns = [];
82
+ for (const file of ["config.json", "opencode.json", "opencode.jsonc"]) {
83
+ const text = readTextOrNull(path.join(dir, file));
84
+ if (text === null)
85
+ continue;
86
+ const parts = [];
87
+ let mcp = false;
88
+ if (jsonKeyPresent(text, "mcp") && jsonKeyPresent(text, "servers")) {
89
+ parts.push("an MCP server");
90
+ mcp = true;
91
+ }
92
+ for (const key of ["plugin", "agent", "instructions"]) {
93
+ if (jsonKeyPresent(text, key))
94
+ parts.push(`a ${key} entry`);
95
+ }
96
+ if (parts.length === 0)
97
+ continue;
98
+ const joined = parts.join(" and ");
99
+ // Only the MCP server is disabled for team runs; everything else still loads.
100
+ const verdict = mcp && parts.length === 1 ? " — disabled for team runs" : " — still loads into team runs";
101
+ warns.push(`${path.join(dir, file)} carries ${joined}${verdict}`);
102
+ }
103
+ const agentsMd = path.join(dir, "AGENTS.md");
104
+ if (statOrNull(agentsMd)?.isFile() === true)
105
+ warns.push(`${agentsMd} still loads into team runs`);
106
+ return warns;
107
+ }
108
+ /** OpenCode model, credential and leak readiness (plans/opencode-backend.md §7, part 7/7):
109
+ * every model routed to opencode must be in the catalog cache — an unknown model fails, a
110
+ * cached one reads "free" or its per-M prices and context window, and a thinking level with no
111
+ * catalog effort is noted as dropped; a non-gateway provider (`opencode/<provider>/…`) must
112
+ * have a credential `opencode auth list` lists, else warns; and the operator's global opencode
113
+ * config and AGENTS.md must not leak an MCP server or a plugin/agent/instructions entry into
114
+ * team runs, else warns naming the file and key. Reports the team's opencode database and spill
115
+ * sizes as information. Stands down (ok) when no seam routes to opencode, so a pi-only team
116
+ * spawns nothing and reads nothing. `pathEnv` and `home` are injectable so tests need no real
117
+ * PATH or HOME. The version half of the opencode pre-flight is the sibling checkOpencodeVersion
118
+ * in doctor-checks.ts. */
119
+ export async function checkOpencode(root, pathEnv = envPath(), home = os.homedir(), xdgConfigHome = process.env.XDG_CONFIG_HOME ?? "", timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
120
+ const config = loadConfigForCheck(root);
121
+ if ("level" in config)
122
+ return config;
123
+ if (!opencodeSeamsInUse(config))
124
+ return okOutcome("no model jobs run on opencode");
125
+ const models = seamedModels(config, "opencode");
126
+ const cache = readOpencodeCache(root);
127
+ const fails = [];
128
+ const warns = [];
129
+ const notes = [];
130
+ for (const pair of models) {
131
+ const key = opencodeModelKey(pair.model);
132
+ const entry = cache.models[key];
133
+ if (entry === undefined) {
134
+ fails.push(`${pair.model} is not in the opencode catalog cache`);
135
+ continue;
136
+ }
137
+ const price = entry.free ? "free" : `$${entry.inputPerM ?? 0}/$${entry.outputPerM ?? 0} per M`;
138
+ const window = entry.context !== undefined ? `, ${tokenWindowLabel(entry.context)}` : "";
139
+ notes.push(`${pair.model} — ${price}${window}`);
140
+ // A model with no effort list at all supports no thinking level, so a requested level is
141
+ // dropped just the same as one the list omits.
142
+ if (pair.thinking !== undefined &&
143
+ pair.thinking !== "" &&
144
+ !entry.efforts?.includes(pair.thinking)) {
145
+ warns.push(`${pair.model} has no ${pair.thinking} effort — that thinking level is dropped`);
146
+ }
147
+ const provider = opencodeProviderOf(pair.model);
148
+ if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
149
+ warns.push(`${pair.model} is a non-gateway provider opencode does not list a credential for`);
150
+ }
151
+ }
152
+ warns.push(...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")));
153
+ const db = statOrNull(opencodeDbPath(root));
154
+ const spillDir = opencodeSpillDir(root);
155
+ const spill = spillDir !== "" && isDirectory(spillDir) ? treeBytes(spillDir) : null;
156
+ const sizeNotes = `db ${formatBytes(db?.size ?? 0)}${spill !== null ? `, spill ${formatBytes(spill)}` : ", no spill dir"}`;
157
+ const found = [...fails, ...warns];
158
+ const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
159
+ if (fails.length > 0)
160
+ return failOutcome(base);
161
+ return (warns.length > 0 ? warnOutcome : okOutcome)(base);
162
+ }
@@ -1,9 +1,10 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { worktreesDir } from "../paths.js";
5
4
  import { pidAlive } from "../process/process.js";
5
+ import { GROUP_POLL_MS, KILL_GRACE_MS } from "../process/process-group.js";
6
6
  import { systemProcessProbe } from "../process/process-table.js";
7
+ import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
7
8
  import { errorMessage, truncate } from "../text/text.js";
8
9
  import { plural } from "../text/phrases.js";
9
10
  import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
@@ -71,10 +72,10 @@ function relativeArgvIsOurs(command, cwd, root, dirs) {
71
72
  }
72
73
  return false;
73
74
  }
74
- /** How many processes descend from `pid`, given the table's parent → children map — an
75
+ /** The processes that descend from `pid`, given the table's parent → children map — an
75
76
  * orphaned test runner's workers keep the runner as their parent, so they are the orphan's
76
77
  * tree, not orphans of their own, and killing the runner alone leaves them running. */
77
- function descendantCount(children, pid) {
78
+ function descendantPids(children, pid) {
78
79
  const seen = new Set();
79
80
  const stack = [...(children.get(pid) ?? [])];
80
81
  for (let p = stack.pop(); p !== undefined; p = stack.pop()) {
@@ -83,52 +84,43 @@ function descendantCount(children, pid) {
83
84
  seen.add(p);
84
85
  stack.push(...(children.get(p) ?? []));
85
86
  }
86
- return seen.size;
87
+ return [...seen];
87
88
  }
88
- /** Orphaned worktree processes — the detector half of the grandchild-leak fix (BUGS.md
89
- * 2026-09-21): a process reparented to PID 1 that belongs to one of this repo's worktrees.
90
- * Leaks of this kind (a killed tick's tool-call grandchildren, a timed-out build check's test
91
- * tree, a pi run's backgrounded server, a stray orchestrator an orphaned suite started) hold
92
- * no lock, write no event and touch no state file, so nothing else in the harness can see
93
- * them. A process is this repo's when its argv names a path under `.tumwater/worktrees/`
94
- * (absolute; or relative, confirmed by cwd where it can be — relativeArgvIsOurs) or when its
95
- * cwd lies under it: a leaked `node dist/test/test-runner.js` names no worktree at all.
96
- *
97
- * PPID 1 is what keeps the live team out: a running orchestrator's pi runs and build checks
98
- * have the orchestrator as parent, and the orchestrator and its supervisor run from the repo
99
- * root, never from a worktree, even when a detached `nohup` launch leaves them parentless.
100
- * (macOS always reparents an orphan to launchd, PID 1; on Linux a process under a child
101
- * subreaper — `systemd --user` in a desktop session — is reparented to that instead, and is
102
- * not seen here.)
103
- *
104
- * A second kind of evidence closes the gap the 2026-09-30 sweep fix left (BUGS.md, part
105
- * 2/2): every process a run starts carries `TUMWATER_RUN=<harness pid>-<nonce>` in its
106
- * environment (appended to any inherited mark), wherever its cwd is — exactly where the
107
- * prompts send agents (scratch dirs under the system temp). A parentless process whose
108
- * every mark names a harness pid that is no longer alive is this team's orphan: nothing
109
- * will reap it, because the run sweep that would have dies with its harness. A mark naming
110
- * ANY live harness is not reported — that harness's own sweep kills everything carrying the
111
- * mark when its run folds. A pid that died and was recycled by an unrelated process reads
112
- * as live (pidAlive cannot tell), so such a straggler stays unseen here — the same limit the
113
- * merge lock's stale-holder check carries. An unparseable mark (a torn or foreign value
114
- * without a `<pid>-` prefix) judges nothing; on macOS ps -E hides platform binaries'
115
- * environments, so a marked sleep/sh leak stays unseen there, as the sweep's own scan does.
116
- *
117
- * Cheap by construction: one `ps`, then one cwd lookup covering only the parentless
118
- * processes argv did not already settle (and only those this user can inspect — lsof and
119
- * /proc cannot read anyone else's, root reads all). An unreadable table degrades to a warn,
120
- * never a crashed doctor; unreadable cwds degrade to the argv match and say so. Any orphan is
121
- * a fail — the exit code is the point: a scripted doctor must notice. `probe` is injectable so
122
- * tests run against a fake table. */
123
- export async function checkOrphans(root, probe = systemProcessProbe) {
89
+ /** A mark's harness is dead (true), alive (false), or the mark names no harness (null — a torn
90
+ * or foreign value judges nothing rather than counting as dead). */
91
+ function harnessGone(marker) {
92
+ const m = /^(\d+)-/.exec(marker);
93
+ return m ? !pidAlive(Number(m[1])) : null;
94
+ }
95
+ /** A parent pid → child pids map of the table. */
96
+ function childrenOf(rows) {
97
+ const children = new Map();
98
+ for (const r of rows) {
99
+ const siblings = children.get(r.ppid);
100
+ if (siblings)
101
+ siblings.push(r.pid);
102
+ else
103
+ children.set(r.ppid, [r.pid]);
104
+ }
105
+ return children;
106
+ }
107
+ /** An orphan's command with the repo root cut out, longest spelling first: the resolved one can
108
+ * contain the given one (/private/var/… holds /var/…) — then trimmed to ORPHAN_COMMAND_MAX. */
109
+ function shownCommand(command, root) {
110
+ const cut = [...rootSpellings(root)].sort((a, b) => b.length - a.length);
111
+ return truncate(cut.reduce((c, rt) => c.split(`${rt}/`).join(""), command), ORPHAN_COMMAND_MAX);
112
+ }
113
+ /** The detection half of checkOrphans, shared with killOrphans so the check and the kill cannot
114
+ * disagree about the set. */
115
+ async function findOrphans(root, probe) {
116
+ const roots = rootSpellings(root);
124
117
  let rows;
125
118
  try {
126
119
  rows = await probe.list();
127
120
  }
128
121
  catch (err) {
129
- return warnOutcome(`could not scan the process table — ${errorMessage(err)}`);
122
+ return { error: errorMessage(err) };
130
123
  }
131
- const roots = rootSpellings(root);
132
124
  const dirs = roots.map(worktreesDir);
133
125
  // Never doctor itself, should it be run parentless from a worktree's build.
134
126
  const parentless = rows.filter((r) => r.ppid === 1 && r.pid !== process.pid);
@@ -156,12 +148,6 @@ export async function checkOrphans(root, probe = systemProcessProbe) {
156
148
  // Best-effort per the probe contract: argv and cwd still carry the check alone.
157
149
  }
158
150
  }
159
- /** A mark's harness is dead (true), alive (false), or the mark names no harness (null —
160
- * a torn or foreign value judges nothing rather than counting as dead). */
161
- const harnessGone = (marker) => {
162
- const m = /^(\d+)-/.exec(marker);
163
- return m ? !pidAlive(Number(m[1])) : null;
164
- };
165
151
  const byMark = new Set();
166
152
  for (const r of parentless) {
167
153
  if (byArgv.has(r.pid))
@@ -176,35 +162,168 @@ export async function checkOrphans(root, probe = systemProcessProbe) {
176
162
  const cwd = cwds.get(r.pid);
177
163
  return (cwd !== undefined && isUnder(cwd, dirs)) || relativeArgvIsOurs(r.command, cwd, root, dirs);
178
164
  });
165
+ return { rows, orphans, marks, cwdProblem };
166
+ }
167
+ /** Orphaned worktree processes — the detector half of the grandchild-leak fix (BUGS.md
168
+ * 2026-09-21): a process reparented to PID 1 that belongs to one of this repo's worktrees.
169
+ * Leaks of this kind (a killed run's tool-call grandchildren, a timed-out build check's test
170
+ * tree, a pi run's backgrounded server, a stray orchestrator an orphaned suite started) hold
171
+ * no lock, write no event and touch no state file, so nothing else in the harness can see
172
+ * them. A process is this repo's when its argv names a path under `.tumwater/worktrees/`
173
+ * (absolute; or relative, confirmed by cwd where it can be — relativeArgvIsOurs) or when its
174
+ * cwd lies under it: a leaked `node dist/test/test-runner.js` names no worktree at all.
175
+ *
176
+ * PPID 1 is what keeps the live team out: a running orchestrator's pi runs and build checks
177
+ * have the orchestrator as parent, and the orchestrator and its supervisor run from the repo
178
+ * root, never from a worktree, even when a detached `nohup` launch leaves them parentless.
179
+ * (macOS always reparents an orphan to launchd, PID 1; on Linux a process under a child
180
+ * subreaper — `systemd --user` in a desktop session — is reparented to that instead, and is
181
+ * not seen here.)
182
+ *
183
+ * A second kind of evidence closes the gap the 2026-09-30 sweep fix left (BUGS.md, part
184
+ * 2/2): every process a run starts carries `TUMWATER_RUN=<harness pid>-<nonce>` in its
185
+ * environment (appended to any inherited mark), wherever its cwd is — exactly where the
186
+ * prompts send agents (scratch dirs under the system temp). A parentless process whose
187
+ * every mark names a harness pid that is no longer alive is this team's orphan: nothing
188
+ * will reap it, because the run sweep that would have dies with its harness. A mark naming
189
+ * ANY live harness is not reported — that harness's own sweep kills everything carrying the
190
+ * mark when its run folds. A pid that died and was recycled by an unrelated process reads
191
+ * as live (pidAlive cannot tell), so such a straggler stays unseen here — the same limit the
192
+ * merge lock's stale-holder check carries. An unparseable mark (a torn or foreign value
193
+ * without a `<pid>-` prefix) judges nothing; on macOS ps -E hides platform binaries'
194
+ * environments, so a marked sleep/sh leak stays unseen there, as the sweep's own scan does.
195
+ *
196
+ * Cheap by construction: one `ps`, then one cwd lookup covering only the parentless
197
+ * processes argv did not already settle (and only those this user can inspect — lsof and
198
+ * /proc cannot read anyone else's, root reads all). An unreadable table degrades to a warn,
199
+ * never a crashed doctor; unreadable cwds degrade to the argv match and say so. Any orphan is
200
+ * a fail — the exit code is the point: a scripted doctor must notice. `probe` is injectable so
201
+ * tests run against a fake table. */
202
+ export async function checkOrphans(root, probe = systemProcessProbe) {
203
+ const scan = await findOrphans(root, probe);
204
+ if ("error" in scan)
205
+ return warnOutcome(`could not scan the process table — ${scan.error}`);
206
+ const { rows, orphans, marks, cwdProblem } = scan;
179
207
  if (orphans.length === 0) {
180
208
  if (cwdProblem !== null)
181
209
  return warnOutcome(`none named in argv, but process cwds are unreadable (${cwdProblem}) — an orphan started inside a worktree, like a leaked test runner, would be missed`);
182
210
  return okOutcome("none — no process reparented to PID 1 runs from .tumwater/worktrees/ or carries only dead runs' marks");
183
211
  }
184
- const children = new Map();
185
- for (const r of rows) {
186
- const siblings = children.get(r.ppid);
187
- if (siblings)
188
- siblings.push(r.pid);
189
- else
190
- children.set(r.ppid, [r.pid]);
191
- }
192
- // The root is cut from each shown command, longest spelling first: the resolved one can
193
- // contain the given one (/private/var/… holds /var/…).
194
- const cut = [...roots].sort((a, b) => b.length - a.length);
212
+ const children = childrenOf(rows);
195
213
  const listed = orphans.slice(0, ORPHANS_LISTED_MAX).map((r) => {
196
- const n = descendantCount(children, r.pid);
214
+ const n = descendantPids(children, r.pid).length;
197
215
  const tree = n > 0 ? `, +${plural(n, "descendant")}` : "";
198
- const command = cut.reduce((c, rt) => c.split(`${rt}/`).join(""), r.command);
199
216
  // A mark-caught orphan says which run marked it: the operator killing it wants to know
200
217
  // nothing else will, and the dead harness's pid is the evidence argv and cwd lacked.
201
218
  const mark = (marks.get(r.pid) ?? []).find((m) => harnessGone(m) === true);
202
219
  const marked = mark !== undefined ? ` (run ${mark}; tumwater ${mark.split("-")[0]} exited)` : "";
203
- return `pid ${r.pid} (age ${r.etime}, cpu ${r.time}${tree}) ${truncate(command, ORPHAN_COMMAND_MAX)}${marked}`;
220
+ return `pid ${r.pid} (age ${r.etime}, cpu ${r.time}${tree}) ${shownCommand(r.command, root)}${marked}`;
204
221
  });
205
222
  const more = orphans.length - listed.length;
206
223
  const count = `${plural(orphans.length, "orphaned process", "orphaned processes")} (PPID 1)`;
207
224
  return failOutcome(`${count}: ${listed.join("; ")}${more > 0 ? `; and ${more} more` : ""}` +
208
- " — nothing reaps these; kill each with its descendants" +
225
+ " — nothing reaps these; `tumwater doctor --kill-orphans` (or the dashboard Health card's Kill orphans, or the TUI Health tab's Ctrl+K pressed twice) kills each with its descendants" +
209
226
  (cwdProblem !== null ? ` (process cwds unreadable — ${cwdProblem}; argv matched only)` : ""));
210
227
  }
228
+ const systemSignals = {
229
+ kill: (pid, signal) => void process.kill(pid, signal),
230
+ alive: pidAlive,
231
+ graceMs: KILL_GRACE_MS,
232
+ pollMs: GROUP_POLL_MS,
233
+ };
234
+ /** The lines `doctor --kill-orphans` prints for a report, shared with the dashboard's Kill
235
+ * orphans toast. */
236
+ export function orphanKillLines(report) {
237
+ if ("error" in report)
238
+ return [`could not scan the process table — ${report.error}; nothing killed`];
239
+ const lines = report.killed.map((k) => {
240
+ const tree = k.descendants > 0 ? ` (+${plural(k.descendants, "descendant")})` : "";
241
+ return `killed pid ${k.pid}${tree} ${k.command}`;
242
+ });
243
+ for (const f of report.failed)
244
+ lines.push(`could not kill pid ${f.pid}: ${f.reason}`);
245
+ if (lines.length === 0)
246
+ lines.push("no orphaned processes to kill");
247
+ return lines;
248
+ }
249
+ /** Kill the processes checkOrphans lists, each with its descendants from the same scan:
250
+ * SIGTERM all, wait up to `graceMs` for them to go, SIGKILL what remains. A pid already gone
251
+ * (ESRCH) counts as killed; any other refusal (EPERM) is reported and the rest are still
252
+ * signalled. An orphan with a refused pid in its tree is reported failed, not killed. */
253
+ export async function killOrphans(root, probe = systemProcessProbe, signals = systemSignals) {
254
+ const scan = await findOrphans(root, probe);
255
+ if ("error" in scan)
256
+ return { error: scan.error };
257
+ return killTrees(root, scan.rows, scan.orphans, signals);
258
+ }
259
+ /** The signal-and-wait half of killOrphans: SIGTERM each chosen row's tree from `rows`, wait,
260
+ * SIGKILL the rest, and report what went. */
261
+ async function killTrees(root, rows, chosen, signals) {
262
+ const children = childrenOf(rows);
263
+ const refused = new Map();
264
+ const send = (pid, signal) => {
265
+ try {
266
+ signals.kill(pid, signal);
267
+ }
268
+ catch (err) {
269
+ if (err.code !== "ESRCH")
270
+ refused.set(pid, errorMessage(err));
271
+ }
272
+ };
273
+ const trees = chosen.map((r) => ({ r, pids: [r.pid, ...descendantPids(children, r.pid)] }));
274
+ const all = [...new Set(trees.flatMap((t) => t.pids))];
275
+ for (const pid of all)
276
+ send(pid, "SIGTERM");
277
+ const stillAlive = () => all.filter((pid) => !refused.has(pid) && signals.alive(pid));
278
+ const deadline = Date.now() + signals.graceMs;
279
+ while (stillAlive().length > 0 && Date.now() < deadline)
280
+ await new Promise((resolve) => setTimeout(resolve, signals.pollMs));
281
+ for (const pid of stillAlive())
282
+ send(pid, "SIGKILL");
283
+ const killed = [];
284
+ const failed = [];
285
+ for (const { r, pids } of trees) {
286
+ const bad = pids.find((pid) => refused.has(pid));
287
+ if (bad === undefined)
288
+ killed.push({ pid: r.pid, command: shownCommand(r.command, root), descendants: pids.length - 1 });
289
+ else
290
+ failed.push({ pid: bad, reason: refused.get(bad) ?? "" });
291
+ }
292
+ return { killed, failed };
293
+ }
294
+ /** Team start's reap of a crashed predecessor's leaks: kill the parentless processes whose run
295
+ * marks all name dead harnesses and at least one names `previousPid`, the orchestrator the
296
+ * info file recorded. Safe without an operator's judgment: a mark proves a tumwater start
297
+ * started the process, the previous-pid match scopes it to this repo, and the dead-harness
298
+ * test excludes any run whose own sweep is still coming. A live `previousPid` (a hand-off
299
+ * whose old orchestrator is draining) scans nothing. Orphans caught only by argv or cwd carry
300
+ * no mark read (findOrphans asks marks only of processes argv did not settle) and stay for
301
+ * `doctor --kill-orphans`. Returns the killed count; never throws. */
302
+ export async function reapPreviousTeamOrphans(root, previousPid, probe = systemProcessProbe, signals = systemSignals) {
303
+ try {
304
+ if (pidAlive(previousPid))
305
+ return 0;
306
+ const scan = await findOrphans(root, probe);
307
+ if ("error" in scan)
308
+ return 0;
309
+ const chosen = scan.orphans.filter((r) => {
310
+ const marks = scan.marks.get(r.pid) ?? [];
311
+ return marks.length > 0 && marks.every((m) => harnessGone(m) === true) &&
312
+ marks.some((m) => m.startsWith(`${previousPid}-`));
313
+ });
314
+ if (chosen.length === 0)
315
+ return 0;
316
+ const { killed, failed } = await killTrees(root, scan.rows, chosen, signals);
317
+ const items = killed.map((k) => {
318
+ const tree = k.descendants > 0 ? ` (+${plural(k.descendants, "descendant")})` : "";
319
+ return `pid ${k.pid}${tree} ${k.command}`;
320
+ });
321
+ const refused = failed.map((f) => `could not kill pid ${f.pid}: ${f.reason}`);
322
+ warnEventBestEffort(root, TUMWATER_SOURCE, `killed ${plural(killed.length, "orphaned process", "orphaned processes")} left by tumwater ` +
323
+ `${previousPid}'s runs at team start: ${[...items, ...refused].join("; ")}`);
324
+ return killed.length;
325
+ }
326
+ catch {
327
+ return 0;
328
+ }
329
+ }