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,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /** Pure formatting and description of a build check's failure output — split out of build-check.ts,
3
2
  * which keeps the execution side (detect → run → classify, the toolchain probe, the process-group
4
3
  * runner). This module turns a check's raw combined output and a BuildCheckOutcome into the machine
@@ -6,7 +5,7 @@
6
5
  * review-verdict.ts's parseVerdict), the clipped output tail (clipBuildTail), the one-line headline
7
6
  * a warning names the failure by (failureHeadline), the check's human-facing command name
8
7
  * (describeCheck), and the reasons a red check hands its author (checkFailureReasons). Every
9
- * consumer of a check's tail — the review gate, the merge path, the red-main gate, the tick prompt
8
+ * consumer of a check's tail — the review gate, the merge path, the red-main gate, the run prompt
10
9
  * — imports from here, so "which line is the headline" and "how much output survives" have one
11
10
  * answer each. Type-only back-reference to build-check.ts for BuildCheckOutcome (no runtime cycle);
12
11
  * BuildCheck comes from build-check-detect.ts. */
@@ -30,17 +29,47 @@ const ERROR_MESSAGE_LINE = /^\S*Error\b[^:]*:\s/;
30
29
  * `✖` line but the `✖ failing tests:` section header. The run's LAST one is the failure whose
31
30
  * detail ends the output, so it names the test the tail's message and stack belong to. */
32
31
  const FAILED_TEST_LINE = /^✖ (?!failing tests:)/;
32
+ /** A stack frame inside Node's own runtime (the test runner, the task queue): the same in every
33
+ * failure and silent about the code under test. Matches `at fn (node:internal/…)` and a bare
34
+ * `at node:internal/…`; not frames in other built-in modules (`node:fs`, `node:async_hooks`) nor
35
+ * in project files. */
36
+ const NODE_INTERNAL_FRAME = /^at\s(?:.*\()?node:internal\//;
37
+ /** How many failing test titles parseFailingTests keeps; a list that long may be cut short. */
38
+ export const FAILING_TESTS_MAX = 50;
39
+ /** The titles of the tests a check's COMPLETE output reports failed: every line FAILED_TEST_LINE
40
+ * matches, its trailing duration stripped, deduplicated (node:test prints a failure in the tree and
41
+ * again under `✖ failing tests:`), in output order, at most FAILING_TESTS_MAX. Read the full
42
+ * output, before clipBuildTail cuts it. Empty for output from another runner. */
43
+ export function parseFailingTests(output) {
44
+ const titles = new Set();
45
+ for (const raw of output.split("\n")) {
46
+ const line = raw.trim();
47
+ if (!FAILED_TEST_LINE.test(line))
48
+ continue;
49
+ const title = line.slice(2).replace(/ \([\d.]+m?s\)$/, "").trim();
50
+ if (title !== "")
51
+ titles.add(title);
52
+ if (titles.size >= FAILING_TESTS_MAX)
53
+ break;
54
+ }
55
+ return [...titles];
56
+ }
33
57
  /** Keep the TAIL of a build's combined output: last ≤10 meaningful lines, each via clipReason —
34
- * so a chatty build cannot bloat persisted state or the injected next-tick note. Blank lines
58
+ * so a chatty build cannot bloat persisted state or the injected next-run note. Blank lines
35
59
  * and npm's own script banner (`> pkg@1.0 script`, `> <command>`) are dropped: they name the
36
60
  * script that ran, not what broke in it. When the ten-line window cuts off the error-message
37
61
  * line that sits above the stack, the nearest such line is kept as well, so failureHeadline can
38
62
  * name the failure instead of a stack frame or an error property; likewise the failed test's
39
63
  * name line (FAILED_TEST_LINE) above it, so the headline says WHICH test — an assertion's
40
64
  * message alone (`Expected values to be strictly equal:`) names none, and a flake reported only
41
- * that way could not be traced (BUGS.md 2026-09-30). Twelve lines at most. */
65
+ * that way could not be traced (BUGS.md 2026-09-30). Frames NODE_INTERNAL_FRAME matches are dropped
66
+ * first so the window keeps more of the assertion's diff (output made only of them is kept as is);
67
+ * when the dropped frame is `at async Test.processPendingSubtests (…) {`, the property dump's
68
+ * opening `{` goes with it. Twelve lines at most. */
42
69
  export function clipBuildTail(output) {
43
- const lines = output.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !/^>\s/.test(l));
70
+ const meaningful = output.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !/^>\s/.test(l));
71
+ const external = meaningful.filter((l) => !NODE_INTERNAL_FRAME.test(l));
72
+ const lines = external.length > 0 ? external : meaningful;
44
73
  const tail = lines.slice(-10);
45
74
  const cut = [...lines.slice(0, -10)].reverse();
46
75
  const message = cut.find((l) => ERROR_MESSAGE_LINE.test(l));
@@ -89,18 +118,21 @@ export function failureHeadline(tail) {
89
118
  return what !== undefined ? `${name} — ${what}` : name;
90
119
  return what ?? tail[0];
91
120
  }
92
- /** The human/prompt-facing name of a check (plans/portability.md §6/7): the tick prompt and
121
+ /** The human/prompt-facing name of a check (plans/portability.md §6/7): the run prompt and
93
122
  * the gate's check reasons name the actual verification command instead of asserting npm —
94
123
  * "verify with `pytest -q`" in a Python repo, "`npm run test`" in an npm one. */
95
124
  export function describeCheck(check) {
96
125
  return check.kind === "npm" ? `\`npm run ${check.script}\`` : `\`${check.command}\``;
97
126
  }
98
127
  /** The machine-generated reasons a red check hands its author: the headline joined to the rest of
99
- * the clipped tail, so the compiler error sits right after it in the injected next-tick note. The
128
+ * the clipped tail, so the compiler error sits right after it in the injected next-run note. The
100
129
  * headline is failureHeadline's — the first line that is not a stack frame — not outputTail[0]: a
101
130
  * suite that dies on an unhandled rejection opens mid-stack, and naming the frame tells the author
102
131
  * where it broke, never what (BUGS.md 2026-09-19). One definition for every red that rejects a
103
- * change: the gate's pre-check, a batch's single-change red, and a single merge's in-lock check. */
132
+ * change: the gate's pre-check, a batch's single-change red, and a single merge's in-lock check.
133
+ * In the verified branch, tail lines the headline already carries are dropped: the headline
134
+ * itself, the test-name or marker line (duration removed) it opens with, and the message line it
135
+ * ends with. */
104
136
  export function checkFailureReasons(check, outcome) {
105
137
  const tail = outcome.outputTail ?? [];
106
138
  // An unverified rejection already says what happened — the check never finished, the tree is
@@ -114,6 +146,14 @@ export function checkFailureReasons(check, outcome) {
114
146
  const headline = failureHeadline(tail);
115
147
  const what = describeCheck(check);
116
148
  return headline !== undefined
117
- ? [`check failed (${what}): ${headline}`, ...tail.filter((l) => l !== headline)]
149
+ ? [`check failed (${what}): ${headline}`, ...tail.filter((l) => !headlineCarries(headline, l))]
118
150
  : [`check failed (${what})`];
119
151
  }
152
+ /** Whether a failureHeadline already says tail line `l`: equal to it, its name part (duration
153
+ * removed), or the message after its ` — `. */
154
+ function headlineCarries(headline, l) {
155
+ if (l === headline || headline.endsWith(` — ${l}`))
156
+ return true;
157
+ const bare = l.replace(/ \([\d.]+m?s\)$/, "");
158
+ return bare === headline || headline.startsWith(`${bare} — `);
159
+ }
@@ -1,7 +1,7 @@
1
- import { BUILD_CHECK_TIMEOUT_MS, detectBuildCheck, gateCommandOf, } from "./build-check-detect.js";
1
+ import { BUILD_CHECK_TIMEOUT_MS, detectBuildCheck, gateCommandOf, mergeCommandOf, } from "./build-check-detect.js";
2
2
  import { runBuildCheck, checkScriptName, checkTimeoutMs, } from "./build-check.js";
3
3
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
- import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning, runWithCleanRetry, installFailedPhrase, killedPhrase, loadedHostPhrase, timedOutPhrase, DEADLINE_LATE_TOLERANCE_MS, SLEEP_SPAN_TOLERANCE_MS, sleptPhrase, starvedDeadlineMs, UNVERIFIED_TREE, } from "./build-check-events.js";
4
+ import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning, runWithCleanRetry, installFailedPhrase, killedPhrase, loadedHostPhrase, timedOutPhrase, DEADLINE_LATE_TOLERANCE_MS, logCheckWaitEvent, SLEEP_SPAN_TOLERANCE_MS, sleptPhrase, starvedDeadlineMs, UNVERIFIED_TREE, } from "./build-check-events.js";
5
5
  import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
6
6
  import { sampleHostLoad, sampleSleepClock } from "./host-sleep.js";
7
7
  import { npmInstall } from "./dep-install.js";
@@ -27,14 +27,17 @@ import { npmInstall } from "./dep-install.js";
27
27
  * own host-sleep evidence) earns that same one retry at any scope, and a failed run the host slept
28
28
  * through does too; each attempt is priced as its own build_check event, carrying when the check
29
29
  * itself ran (buildCheckRunFields). A configured `check.gateCommand` replaces the command at scope
30
- * "gate" only (same cwd and timeout); the merge and batch scopes — and the red-main baseline, which
31
- * detects on its own — keep running check.command. `permitWait` hears about a wait for the check
32
- * permit (the merge cell's "waiting for a check slot"). Never throws. */
30
+ * "gate" only (same cwd and timeout); the in-lock command (Fast merge "Defaults", mergeCommandOf) —
31
+ * `check.mergeCommand` when set, else `check.gateCommand`, else the full check — replaces it at the
32
+ * merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
33
+ * detects on its own, keeps running check.command. `permitWait` hears
34
+ * about a wait for the check permit (the merge cell's "waiting for a check slot"). Never throws. */
33
35
  export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
34
- const gateCommand = scope === "gate" ? gateCommandOf(config) : undefined;
35
- const check = detectBuildCheck(wt, gateCommand === undefined ? config : { check: { ...config?.check, command: gateCommand } });
36
+ const swapped = scope === "gate" ? gateCommandOf(config) : MERGE_SCOPES.has(scope) ? mergeCommandOf(config) : undefined;
37
+ const check = detectBuildCheck(wt, swapped === undefined ? config : { check: { ...config?.check, command: swapped } });
36
38
  if (!check)
37
39
  return null;
40
+ const cheaper = scope !== "gate" && swapped !== undefined;
38
41
  // A configured check.timeoutSeconds bounds either kind; otherwise an npm check runs under the
39
42
  // caller's bound. Effective here so the reason text and the skip warning agree with what
40
43
  // runBuildCheck enforced.
@@ -59,10 +62,14 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
59
62
  // outcome stands (runWithCleanRetry). The verdict-less first attempt is priced as its own
60
63
  // event (the feed must answer how long a check took), then the final event below records
61
64
  // the retry's classified outcome.
62
- const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur)));
65
+ const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur, cheaper)));
63
66
  durationMs = d;
64
67
  return outcome;
65
- }, permitWait);
68
+ }, {
69
+ waiting: () => permitWait?.waiting?.(),
70
+ granted: () => permitWait?.granted?.(),
71
+ waited: (waitedMs, parked) => logCheckWaitEvent(root, role, scope, waitedMs, parked),
72
+ }, `${role} ${scope}`);
66
73
  // A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
67
74
  // genuinely hung or oversized suite), made no verdict about the tree — and this is the check
68
75
  // whose whole job is to catch a semantic conflict before it merges, so it rejects
@@ -102,10 +109,10 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
102
109
  ...(raw.install ? { install: raw.install } : {}),
103
110
  }
104
111
  : raw;
105
- logEventBestEffort(root, buildCheckEvent(role, scope, outcome, durationMs));
112
+ logEventBestEffort(root, buildCheckEvent(role, scope, outcome, durationMs, cheaper));
106
113
  if (outcome.status === "skipped") {
107
114
  // Environmental — deliberately NOT fail-closed, so a hung build script cannot wedge every
108
- // code tick into the 3-strike discard (gate) or a merge behind the merge lock.
115
+ // code run into the 3-strike discard (gate) or a merge behind the merge lock.
109
116
  const w = SCOPE_WORDS[scope];
110
117
  warnEventBestEffort(root, role, buildCheckSkipWarning(outcome.skipReason, w.label, w.proceeding, effectiveMs, outcome.skipReason === "killed" && outcome.killedBy
111
118
  ? { signal: outcome.killedBy, durationMs }
@@ -1,7 +1,7 @@
1
1
  import { BUILD_CHECK_TIMEOUT_MS } from "./build-check-detect.js";
2
2
  import { EXEC_MAX_BUFFER, execFileAsync } from "../process/process.js";
3
3
  import { KILL_GRACE_MS, runScriptGroup } from "../process/process-group.js";
4
- import { clipBuildTail } from "./build-check-report.js";
4
+ import { clipBuildTail, parseFailingTests } from "./build-check-report.js";
5
5
  import { parseTestCounts } from "./build-check-counts.js";
6
6
  import { sampleSleepClock, sleptMsBetween } from "./host-sleep.js";
7
7
  import { npmInstall, syncInstall } from "./dep-install.js";
@@ -168,6 +168,7 @@ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sam
168
168
  status: "failed",
169
169
  script,
170
170
  outputTail: clipBuildTail(output),
171
+ failingTests: parseFailingTests(output),
171
172
  run,
172
173
  counts: parseTestCounts(output),
173
174
  };
@@ -1,10 +1,9 @@
1
- // vocabulary: plain
2
1
  import path from "node:path";
3
2
  import { fileURLToPath } from "node:url";
4
3
  import { gitTry } from "../git/git-run.js";
5
4
  import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
6
5
  import { finiteNumber } from "../files/json-object.js";
7
- import { refSha } from "../git/git.js";
6
+ import { commitExists, refSha } from "../git/git.js";
8
7
  /** The paths whose change makes a build stale — everything tsc compiles into dist/ plus the
9
8
  * files that steer the compile. Tests are compiled too but never loaded by the running
10
9
  * harness, so a test-only commit does not stale the build; docs and markdown never do. */
@@ -53,7 +52,7 @@ export async function stampBuild(root, dist, sha) {
53
52
  export async function isSelfHosted(root, info) {
54
53
  if (path.resolve(root) !== info.root)
55
54
  return false;
56
- return (await gitTry(root, "cat-file", "-e", `${info.sha}^{commit}`)) !== null;
55
+ return commitExists(root, info.sha);
57
56
  }
58
57
  /** Compare a build's commit against main's head over the build inputs only: commits touching
59
58
  * just tests or markdown leave the running code unchanged and never count as stale. Null when
@@ -1,10 +1,9 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { distDir, stampBuild } from "./build-info.js";
5
4
  import { clipBuildTail } from "./build-check-report.js";
6
5
  import { resolveFromNodeModules } from "./build-check-detect.js";
7
- import { ensureDir, removeTree } from "../files/files.js";
6
+ import { ensureDir, removeTree, tryReadDirEntries } from "../files/files.js";
8
7
  import { stagingDir, stagingRootDir } from "../paths.js";
9
8
  import { execFileAsync } from "../process/process.js";
10
9
  import { warnEvent, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
@@ -36,13 +35,9 @@ export const STAGED_PRUNE_AFTER_MS = 45 * 60_000;
36
35
  export function pruneStaleStagings(root, keep, now = Date.now()) {
37
36
  const stagingRoot = stagingRootDir(root);
38
37
  let removed = 0;
39
- let entries;
40
- try {
41
- entries = fs.readdirSync(stagingRoot, { withFileTypes: true });
42
- }
43
- catch {
38
+ const entries = tryReadDirEntries(stagingRoot);
39
+ if (!entries)
44
40
  return 0; // No staging root yet (first compile) — nothing to prune.
45
- }
46
41
  for (const entry of entries) {
47
42
  if (!entry.isDirectory() || !/^[0-9a-f]{40}$/.test(entry.name))
48
43
  continue;
@@ -61,7 +56,7 @@ export function pruneStaleStagings(root, keep, now = Date.now()) {
61
56
  removed += 1;
62
57
  }
63
58
  catch {
64
- // Left for the next compile or swap: dead weight one tick longer is fine.
59
+ // Left for the next compile or swap: dead weight one run longer is fine.
65
60
  }
66
61
  }
67
62
  return removed;
@@ -216,9 +211,9 @@ export function swapDist(root, dist, mainHead) {
216
211
  * build aside to `dist.prev` and then moves the staged build into `dist`, and a process killed
217
212
  * (SIGKILL — the OOM killer) in that microsecond window leaves `dist` missing. Nothing in the
218
213
  * swap can close the window, because the survivor is the supervisor and it exits on the dead
219
- * generation; the next `tumwater run` then cannot even load `dist/src/cli.js` and fails with
214
+ * generation; the next `tumwater start` then cannot even load `dist/src/cli.js` and fails with
220
215
  * MODULE_NOT_FOUND, so the repo is wedged until someone renames `dist.prev` back by hand. The
221
- * supervisor calls this before it exits (src/cli/cli-run.ts, via superviseRun's onDone): while
216
+ * supervisor calls this before it exits (src/cli/cli-start.ts, via superviseStart's onDone): while
222
217
  * it is still up, it renames the preserved old build back into place so the next boot works.
223
218
  *
224
219
  * Only acts when `dist` is missing and a `dist.prev` exists — both conditions together are the
@@ -233,10 +228,10 @@ export function recoverInterruptedSwap(root, dist) {
233
228
  fs.renameSync(prev, dist); // Atomic: dist is either the restored old build or still missing.
234
229
  return true;
235
230
  }
236
- /** The supervisor's end-of-supervision repair (src/cli/cli-run.ts wires this into
237
- * superviseRun's onDone): recover dist/ after an interrupted swap and, either way, put the
231
+ /** The supervisor's end-of-supervision repair (src/cli/cli-start.ts wires this into
232
+ * superviseStart's onDone): recover dist/ after an interrupted swap and, either way, put the
238
233
  * outcome on the event feed. Kept beside recoverInterruptedSwap so the CLI wiring is one call.
239
- * cli-run.ts imports this module statically, so the code is in memory before a swap can move
234
+ * cli-start.ts imports this module statically, so the code is in memory before a swap can move
240
235
  * dist/ away — recovering via an import at call time would fail ERR_MODULE_NOT_FOUND exactly
241
236
  * when it is needed, the window this closes. `dist` defaults to this process's own dist
242
237
  * (build-info.distDir, a pure path computation captured at load), overridable so a test can
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  import path from "node:path";
3
2
  import { resolveFromNodeModules } from "./build-check-detect.js";
4
3
  import { readJsonFile } from "../files/json-files.js";
@@ -9,7 +8,7 @@ import { errorMessage } from "../text/text.js";
9
8
  /** Keeping a tree's install in step with its lockfile (BUGS.md 2026-10-01). node_modules is
10
9
  * gitignored, so a tumwater worktree has none of its own: its toolchain and every import resolve by
11
10
  * walking UP to the root checkout's install (detectBuildCheck, resolveFromNodeModules). That
12
- * install predates any dependency a change adds, and no loop ever ran `npm install` — so a
11
+ * install predates any dependency a change adds, and no role ever ran `npm install` — so a
13
12
  * legitimate dependency-adding change failed its gate check with TS2307 every time, and had it
14
13
  * merged, every tree after it would have resolved against a root install missing the package. Two
15
14
  * callers close both halves: runBuildCheck installs into the checked tree when its lockfile's
@@ -1,6 +1,6 @@
1
- /** The collector behind `tumwater diff --role <id>`: what change a loop holds between its commit
2
- * and its merge. The committed half is the loop branch's unmerged work (commits ahead of the
3
- * team's main branch, with the patch); the uncommitted half is the loop worktree's dirty state.
1
+ /** The collector behind `tumwater diff --role <id>`: what change a role holds between its commit
2
+ * and its merge. The committed half is the role branch's unmerged work (commits ahead of the
3
+ * team's main branch, with the patch); the uncommitted half is the role worktree's dirty state.
4
4
  * Everything is read-only git plumbing already used elsewhere — this module only wires the pieces
5
5
  * into one operator view, degrading like `report` does when the team (or just this role's
6
6
  * worktree) does not exist yet. Lives beside the other collectors (src/report/report-data.ts,
@@ -9,35 +9,55 @@
9
9
  import { knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
10
10
  import { aheadOfMain, branchExists, countCommitsInRange, currentBranch, targetBranch } from "../git/git.js";
11
11
  import { gitTry } from "../git/git-run.js";
12
- import { aheadOfMainDiff, changedFiles } from "../git/git-diff.js";
12
+ import { aheadOfMainDiff, aheadOfMainFiles, changedFiles } from "../git/git-diff.js";
13
+ import { isAbsolute } from "node:path";
13
14
  import { branchName } from "../paths.js";
14
15
  import { isUsableWorktree } from "../git/worktree.js";
15
16
  import { roleWorktreeDir } from "../git/slots-state.js";
16
- /** Collect the pending change of every known role (built-in plus custom loop ids, config
17
- * order — disabled roles included, since a loop stopped mid-flight still holds its branch).
18
- * Each entry goes through collectRoleChange, which keeps the absent/no-base degradation
19
- * logic single-homed and only computes the patch git-diffs for roles actually holding work
20
- * (both are "" when a role holds nothing); the team view then drops the patch fields. */
17
+ /** Resolve the team-wide scope both collection entry points share: read the config once for a
18
+ * configured baseBranch, ask the primary checkout for its current branch once, and check the
19
+ * resolved main branch exists once. */
20
+ async function resolveChangeScope(root) {
21
+ const { config } = loadConfigSafe(root);
22
+ const mainBranch = targetBranch(config?.baseBranch, await currentBranch(root));
23
+ return { mainBranch, mainPresent: await branchExists(root, mainBranch) };
24
+ }
25
+ /** Collect the pending change of every known role (built-in plus custom role ids, config
26
+ * order — disabled roles included, since a role stopped mid-flight still holds its branch).
27
+ * Each entry goes through collectRoleChangeAt with the team-wide scope resolved once up
28
+ * front, which keeps the absent/no-base degradation logic single-homed and only computes
29
+ * the patch git-diffs for roles actually holding work (both are "" when a role holds
30
+ * nothing); the team view then drops the patch fields. */
21
31
  export async function collectTeamChanges(root) {
32
+ const scope = await resolveChangeScope(root);
22
33
  const roles = await Promise.all(knownRoleIdsCached(root).map(async (role) => {
23
- const view = await collectRoleChange(root, role);
34
+ const view = await collectRoleChangeAt(root, role, scope);
24
35
  const { diff: _diff, uncommittedDiff: _uncommittedDiff, ...team } = view;
25
36
  return team;
26
37
  }));
27
- // mainBranch is team-wide: collectRoleChange resolves it once per role through the same
28
- // targetBranch call, so the first entry's value is every entry's value.
29
- return { mainBranch: roles[0]?.mainBranch ?? "main", roles };
38
+ return { mainBranch: scope.mainBranch, roles };
39
+ }
40
+ /** Collect each listed role's full change view, entries in the caller's order. */
41
+ export async function collectRoleChanges(root, roles, opts = {}) {
42
+ const scope = await resolveChangeScope(root);
43
+ const views = await Promise.all(roles.map((role) => collectRoleChangeAt(root, role, scope, opts)));
44
+ return { mainBranch: scope.mainBranch, roles: views };
30
45
  }
31
46
  /** The uncommitted patch's cap, matching aheadOfMainDiff's — one shared bound so neither
32
47
  * half of the view can print unbounded text. */
33
48
  const DIFF_MAX_BYTES = 200_000;
34
- /** Collect the change the role's loop holds: both halves of the view, or a degraded state
49
+ /** Collect the change the role holds: both halves of the view, or a degraded state
35
50
  * when the worktree or the base branch is missing. Never throws on a degraded team — a
36
- * query about a loop that has not run yet must answer, not fail (report's rationale). */
51
+ * query about a role that has not run yet must answer, not fail (report's rationale). */
37
52
  export async function collectRoleChange(root, role, opts = {}) {
53
+ return collectRoleChangeAt(root, role, await resolveChangeScope(root), opts);
54
+ }
55
+ /** One role's change against a pre-resolved team scope: the per-role body of every entry point.
56
+ * Shared so the team-wide mainBranch/branch-exists reads are spent once per collection instead
57
+ * of once per role (the roster and list forms pass a scope resolved before the Promise.all). */
58
+ async function collectRoleChangeAt(root, role, scope, opts = {}) {
38
59
  const branch = branchName(role);
39
- const { config } = loadConfigSafe(root);
40
- const mainBranch = targetBranch(config?.baseBranch, await currentBranch(root));
60
+ const mainBranch = scope.mainBranch;
41
61
  const empty = {
42
62
  role,
43
63
  branch,
@@ -49,31 +69,45 @@ export async function collectRoleChange(root, role, opts = {}) {
49
69
  dirtyFiles: [],
50
70
  uncommittedDiff: "",
51
71
  };
52
- if (!(await branchExists(root, mainBranch)))
72
+ if (!scope.mainPresent)
53
73
  return { ...empty, state: "no-base" };
54
74
  // The role's checkout comes from the pool resolver (slots-state.ts): a slot leased for its
55
- // tick or pinned for it, else the legacy worktree. A null or unusable directory is the same
56
- // operator situation — this loop holds nothing inspectable yet — so worktree.ts's shared
75
+ // run or pinned for it, else the legacy worktree. A null or unusable directory is the same
76
+ // operator situation — this role holds nothing inspectable yet — so worktree.ts's shared
57
77
  // usability probe still answers the final question.
58
78
  const wt = await roleWorktreeDir(root, role);
59
79
  if (wt === null || !(await isUsableWorktree(wt)))
60
80
  return { ...empty, state: "absent" };
61
81
  const ahead = await aheadOfMain(wt, mainBranch);
62
82
  const behind = await countCommitsInRange(wt, `HEAD..${mainBranch}`);
63
- const commits = parseOnelineLog(await gitTry(wt, "log", "--oneline", `${mainBranch}..HEAD`));
83
+ // Under opts.path every git call below is limited to that path through a literal pathspec.
84
+ const spec = opts.path === undefined ? [] : ["--", `:(literal)${opts.path}`];
85
+ const commits = parseOnelineLog(await gitTry(wt, "log", "--oneline", `${mainBranch}..HEAD`, ...spec));
64
86
  // Under opts.stat both halves hold the uncapped `git diff --stat` summary (one line per file).
65
- const diff = ahead === 0
87
+ // A view: a diff git fails to produce shows empty, as it always did.
88
+ const diff = commits.length === 0
66
89
  ? ""
67
90
  : opts.stat
68
- ? ((await gitTry(wt, "diff", "--stat", `${mainBranch}...HEAD`)) ?? "")
69
- : await aheadOfMainDiff(wt, mainBranch);
70
- const dirtyFiles = await changedFiles(wt);
91
+ ? ((await gitTry(wt, "diff", "--stat", `${mainBranch}...HEAD`, ...spec)) ?? "")
92
+ : await aheadOfMainDiff(wt, mainBranch, DIFF_MAX_BYTES, spec).catch(() => "");
93
+ // A view: a git that fails to answer shows no uncommitted edits, as it always did.
94
+ const dirtyFiles = (await changedFiles(wt).catch(() => [])).filter((f) => inPath(f, opts.path));
71
95
  const uncommittedDiff = dirtyFiles.length === 0
72
96
  ? ""
73
97
  : opts.stat
74
- ? ((await gitTry(wt, "diff", "--stat", "HEAD")) ?? "")
75
- : await cappedWorkingDiff(wt);
76
- return { ...empty, state: "ready", ahead, behind, commits, diff, dirtyFiles, uncommittedDiff };
98
+ ? ((await gitTry(wt, "diff", "--stat", "HEAD", ...spec)) ?? "")
99
+ : await cappedWorkingDiff(wt, spec);
100
+ return {
101
+ ...empty,
102
+ state: "ready",
103
+ ahead,
104
+ behind,
105
+ commits,
106
+ diff,
107
+ dirtyFiles,
108
+ uncommittedDiff,
109
+ ...(opts.path === undefined ? {} : { path: opts.path }),
110
+ };
77
111
  }
78
112
  /** Parse `git log --oneline` output into {sha, subject} pairs — sha up to the first space,
79
113
  * the rest of the line the subject. Blank trailing lines (runGit's trimEnd makes them
@@ -89,16 +123,74 @@ function parseOnelineLog(out) {
89
123
  return cut < 0 ? { sha: line, subject: "" } : { sha: line.slice(0, cut), subject: line.slice(cut + 1) };
90
124
  });
91
125
  }
126
+ /** The normalised repo-relative `path` scope of a role's change view (shared by `diff --role
127
+ * --path` and the dashboard's `GET /api/diff?role&path`): leading `./` pieces and trailing `/`
128
+ * characters stripped; null for an empty, `.`, absolute or `..`-bearing result. */
129
+ export function normalizeDiffPath(raw) {
130
+ const norm = raw.replace(/^(\.\/)+/, "").replace(/\/+$/, "");
131
+ const bad = norm === "" || norm === "." || isAbsolute(norm) || norm.split("/").includes("..");
132
+ return bad ? null : norm;
133
+ }
134
+ /** Whether a porcelain path lies under the `--path` scope (always true without one): the path
135
+ * itself, a file below it, or an untracked-directory entry (trailing `/`) that contains it. */
136
+ function inPath(f, path) {
137
+ if (path === undefined)
138
+ return true;
139
+ return f === path || f.startsWith(path + "/") || (f.endsWith("/") && path.startsWith(f));
140
+ }
92
141
  /** The worktree's uncommitted tracked-change patch, capped at DIFF_MAX_BYTES like
93
142
  * aheadOfMainDiff: over the cap, a truncation note plus the --stat summary — bounded
94
143
  * output that still names every changed file. `git diff HEAD` covers staged and unstaged
95
144
  * tracked edits in one patch (plain `git diff` would show unstaged only, dropping a staged
96
145
  * edit the dirtyFiles list already counts); untracked paths have no diff content anywhere,
97
146
  * so they appear in the file list alone. */
98
- async function cappedWorkingDiff(wt) {
99
- const full = (await gitTry(wt, "diff", "HEAD")) ?? "";
147
+ async function cappedWorkingDiff(wt, spec = []) {
148
+ const full = (await gitTry(wt, "diff", "HEAD", ...spec)) ?? "";
100
149
  if (full.length <= DIFF_MAX_BYTES)
101
150
  return full;
102
- const stat = (await gitTry(wt, "diff", "--stat", "HEAD")) ?? "";
151
+ const stat = (await gitTry(wt, "diff", "--stat", "HEAD", ...spec)) ?? "";
103
152
  return (`[uncommitted diff truncated: the full diff is ${full.length} bytes; showing --stat]\n\n` + stat);
104
153
  }
154
+ /** The paths two or more distinct roles hold: a role's duplicate path counts once, roles stay
155
+ * in holdings order, paths sort by plain `<` comparison. */
156
+ export function fileOverlaps(holdings) {
157
+ const byPath = new Map();
158
+ for (const { role, files } of holdings) {
159
+ for (const path of new Set(files)) {
160
+ const roles = byPath.get(path);
161
+ if (roles === undefined)
162
+ byPath.set(path, [role]);
163
+ else
164
+ roles.push(role);
165
+ }
166
+ }
167
+ return [...byPath.entries()]
168
+ .filter(([, roles]) => roles.length >= 2)
169
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
170
+ .map(([path, roles]) => ({ path, roles }));
171
+ }
172
+ /** Keep only the overlaps naming a listed role; each kept entry still names every holder. */
173
+ export function scopeChangeOverlaps(view, roles) {
174
+ return { ...view, overlaps: view.overlaps.filter((o) => o.roles.some((r) => roles.includes(r))) };
175
+ }
176
+ /** Keep only the overlaps at or under `path` (normalized), by the `inPath` rule
177
+ * `diff --role --path` uses. */
178
+ export function scopeChangeOverlapsToPath(view, path) {
179
+ return { ...view, overlaps: view.overlaps.filter((o) => inPath(o.path, path)) };
180
+ }
181
+ /** Collect the files two or more roles hold, committed ahead of main or uncommitted. Computes
182
+ * file names only — no patch — and skips a role with no usable checkout. */
183
+ export async function collectChangeOverlaps(root) {
184
+ const { mainBranch, mainPresent } = await resolveChangeScope(root);
185
+ if (!mainPresent)
186
+ return { mainBranch, state: "no-base", overlaps: [] };
187
+ const held = await Promise.all(knownRoleIdsCached(root).map(async (role) => {
188
+ const wt = await roleWorktreeDir(root, role);
189
+ if (wt === null || !(await isUsableWorktree(wt)))
190
+ return null;
191
+ const committed = (await aheadOfMain(wt, mainBranch)) > 0 ? await aheadOfMainFiles(wt, mainBranch).catch(() => []) : [];
192
+ return { role, files: [...committed, ...(await changedFiles(wt).catch(() => []))] };
193
+ }));
194
+ const holdings = held.filter((h) => h !== null);
195
+ return { mainBranch, state: "ready", overlaps: fileOverlaps(holdings) };
196
+ }
@@ -13,14 +13,25 @@ function workSummary(ahead, behind, mainBranch, dirtyFiles) {
13
13
  (behind > 0 ? `, ${plural(behind, "commit")} behind` : "") +
14
14
  (dirtyFiles.length > 0 ? `, ${plural(dirtyFiles.length, "uncommitted file")}` : ""));
15
15
  }
16
+ /** The one-line phrase a change view prints when the resolved main branch is absent — shared
17
+ * by the roster, the per-role view, the list of roles, and the overlaps view so the operator
18
+ * wording the tests pin cannot drift between them (the workSummary precedent). */
19
+ function mainMissingPhrase(mainBranch) {
20
+ return `main branch ${mainBranch} does not exist`;
21
+ }
22
+ /** True when a team-wide roster is all no-base (every role found the same missing main branch)
23
+ * and holds at least one role, so it opens with the single baseline phrase instead of a column
24
+ * of per-role "does not exist" lines. */
25
+ function allRolesNoBase(roles) {
26
+ return roles.length > 0 && roles.every((r) => r.state === "no-base");
27
+ }
16
28
  /** Render the roster as the operator-facing text `tumwater diff` (no --role) prints: one
17
29
  * line per role that holds pending work, in roster order; roles with no worktree and roles
18
30
  * holding nothing are skipped. Every entry no-base means the baseline itself is gone — the
19
31
  * per-role view's line says so, team-wide, exit 0 like every other degraded case. */
20
32
  export function renderTeamChange(view) {
21
- if (view.roles.length > 0 && view.roles.every((r) => r.state === "no-base")) {
22
- return `main branch ${view.mainBranch} does not exist`;
23
- }
33
+ if (allRolesNoBase(view.roles))
34
+ return mainMissingPhrase(view.mainBranch);
24
35
  const lines = view.roles
25
36
  .filter((r) => r.state !== "no-base" && (r.ahead > 0 || r.dirtyFiles.length > 0))
26
37
  .map((r) => `${r.role}: ${workSummary(r.ahead, r.behind, view.mainBranch, r.dirtyFiles)}`);
@@ -31,11 +42,15 @@ export function renderRoleChange(view) {
31
42
  if (view.state === "absent")
32
43
  return `no worktree for ${view.role}`;
33
44
  if (view.state === "no-base")
34
- return `main branch ${view.mainBranch} does not exist`;
45
+ return mainMissingPhrase(view.mainBranch);
46
+ if (view.path !== undefined && view.commits.length === 0 && view.dirtyFiles.length === 0) {
47
+ return `no pending change to ${view.path} for ${view.role}`;
48
+ }
35
49
  if (view.ahead === 0 && view.dirtyFiles.length === 0)
36
50
  return `no pending change for ${view.role}`;
37
51
  const lines = [
38
- `${view.role}: ${view.branch}, ${workSummary(view.ahead, view.behind, view.mainBranch, view.dirtyFiles)}`,
52
+ `${view.role}: ${view.branch}, ${workSummary(view.ahead, view.behind, view.mainBranch, view.dirtyFiles)}` +
53
+ (view.path === undefined ? "" : `; limited to ${view.path}`),
39
54
  ];
40
55
  for (const c of view.commits)
41
56
  lines.push(`${c.sha} ${c.subject}`);
@@ -48,3 +63,29 @@ export function renderRoleChange(view) {
48
63
  }
49
64
  return lines.join("\n");
50
65
  }
66
+ /** Render several roles' views in the listed order, a blank line apart; every block opens with
67
+ * its role's name, so the blank line separates them. */
68
+ export function renderRoleChanges(view) {
69
+ if (allRolesNoBase(view.roles))
70
+ return mainMissingPhrase(view.mainBranch);
71
+ return view.roles.map(renderRoleChange).join("\n\n");
72
+ }
73
+ /** Render the overlaps view as the operator-facing text `tumwater diff --overlaps` prints:
74
+ * one `<path>: <role>, <role>` line per shared file. `scope` is the `--role` list the view was
75
+ * narrowed to and `path` the `--path` it was narrowed to; they only change the empty-list
76
+ * wording. */
77
+ export function renderChangeOverlaps(view, scope, path) {
78
+ if (view.state === "no-base")
79
+ return mainMissingPhrase(view.mainBranch);
80
+ if (view.overlaps.length === 0 && path !== undefined) {
81
+ return scope !== undefined
82
+ ? `no file under ${path} held by ${scope.join(", ")} is touched by another pending change`
83
+ : `no file under ${path} is touched by two pending changes`;
84
+ }
85
+ if (view.overlaps.length === 0 && scope !== undefined) {
86
+ return `no file held by ${scope.join(", ")} is touched by another pending change`;
87
+ }
88
+ if (view.overlaps.length === 0)
89
+ return "no file is touched by two pending changes";
90
+ return view.overlaps.map((o) => `${o.path}: ${o.roles.join(", ")}`).join("\n");
91
+ }
@@ -1,7 +1,6 @@
1
- // vocabulary: plain
2
1
  import path from "node:path";
3
2
  import { readBrief, renderBrief } from "../brief/brief.js";
4
- import { editInitialPrompt } from "../brief/brief-edit.js";
3
+ import { editBriefText } from "../brief/brief-edit.js";
5
4
  import { editInEditor } from "./cli-editor.js";
6
5
  import { readPromptFileArg } from "./cli-command-args.js";
7
6
  import { JSON_FLAG, rejectUnknownArgs } from "./cli-flag-specs.js";
@@ -11,7 +10,7 @@ import { requireReadyRepo } from "./cli-repo-gate.js";
11
10
  const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
12
11
  /** `brief`'s `--edit` flag: open the prompt in `$VISUAL`/`$EDITOR`. */
13
12
  const EDIT_SPEC = { names: ["--edit"] };
14
- /** `tumwater brief`: show the initial prompt and the file holding it, or with `--file <path>` to
13
+ /** `tumwater brief`: show the brief text and the file holding it, or with `--file <path>` to
15
14
  * replace it (or with `--edit`, open it in the editor) under the checks init applies and commit
16
15
  * that one file on main. */
17
16
  export async function cmdBrief(root, args) {
@@ -45,7 +44,7 @@ export async function cmdBrief(root, args) {
45
44
  }
46
45
  else
47
46
  text = readPromptFileArg(args[fileIdx + 1]);
48
- const outcome = await editInitialPrompt(root, text);
47
+ const outcome = await editBriefText(root, text);
49
48
  if (!outcome.ok)
50
49
  fail(outcome.error);
51
50
  const { file: name, changed, committed } = outcome;