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,34 +1,50 @@
1
- // vocabulary: plain
2
- import { baselineBlocked } from "../roles/roles.js";
1
+ import { baselineBlocked, BUGFIX_ROLE } from "../roles/roles.js";
2
+ import { baseRoleOf } from "../roles/runner-ids.js";
3
+ import { enqueueRolePrompt } from "../inbox/inbox.js";
3
4
  import { isCustomRole, liveConfig } from "../config/config.js";
4
- import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
5
- import { failureHeadline } from "../build/build-check-report.js";
5
+ import { BUILD_CHECK_TIMEOUT_MS, mergeCommandOf } from "../build/build-check-detect.js";
6
+ import { failureHeadline, FAILING_TESTS_MAX } from "../build/build-check-report.js";
6
7
  import { baselineCheckEventLogger, buildCheckSkipWarning, sleptPhrase } from "../build/build-check-events.js";
7
- import { checkMainBaseline } from "./main-baseline.js";
8
- import { buildMainRedNote } from "../prompt/gate-prompts.js";
8
+ import { checkMainBaseline, watchedBaseline } from "./main-baseline.js";
9
+ import { buildMainRedNote } from "../prompt/gate-notes.js";
9
10
  import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
10
11
  import { errorMessage } from "../text/text.js";
11
- import { mainRedPhrase } from "../text/phrases.js";
12
+ import { mainRedPhrase, plural } from "../text/phrases.js";
13
+ import { clipToWidth } from "../text/text-width.js";
14
+ import { shortSha } from "../text/format.js";
12
15
  import { branchHead } from "../git/git.js";
13
16
  import { gateMainWorktreePath } from "../paths.js";
14
17
  import { useWorktree } from "../git/worktree-use.js";
15
18
  import { ensureDetachedWorktree } from "../git/worktree.js";
16
- /** Red-main baseline gate for fresh authoring ticks: before an
19
+ /** Red-main baseline gate for fresh authoring runs: before an
17
20
  * authoring run is spent on top of pristine main, verify that MAIN ITSELF is green — a red main
18
21
  * rejects every code diff deterministically at the review gate (BUGS.md's "Build broken on main"
19
22
  * and "Tests red on main" entries), so while a SHA is known red the blocked roles skip authoring
20
- * instead of burning runs that are guaranteed to fail. Split out of loop.ts — which keeps the
21
- * tick lifecycle around it — because this is a self-contained policy with its own per-process
22
- * state (one harness-level `main_red_hold` event per newly-discovered red SHA) and its own
23
- * event contract; what it borrows from the loop is identity and the role's worktree, and it
23
+ * instead of burning runs that are guaranteed to fail. Split out of runner.ts — which keeps the
24
+ * run lifecycle around it — because this is a self-contained policy with its own per-process
25
+ * state (one harness-level `main_red_hold` event AND one queued healer prompt per
26
+ * newly-discovered red SHA) and its own
27
+ * event contract; what it borrows from the role is identity and the role's worktree, and it
24
28
  * consumes
25
29
  * build/build-check.ts's checkMainBaseline machinery (detection + execution + per-SHA cache),
26
- * which stays unaware of ticks. */
30
+ * which stays unaware of runs. */
27
31
  /** The last main SHA for which this process logged a red-main hold event (the baseline check): one
28
- * harness-level warning per newly-discovered red SHA, not one per blocked role's tick —
32
+ * harness-level warning per newly-discovered red SHA, not one per blocked role's run —
29
33
  * module-level so every runner in the team shares it. A restart re-logs once: the cache is cold
30
34
  * then too, and an operator restarting into a still-red main should see why nothing merges. */
31
35
  let lastMainRedSha = null;
36
+ /** The key of the last failing set the healer was sent a prompt for: the sorted failing test
37
+ * names, or the red SHA when the red names none. Two red commits with the same failing set send
38
+ * one heal prompt, not one per commit; a green main observed by the gate or the bugfix handoff
39
+ * clears it, so a set that returns after green is healed again. */
40
+ let lastHealKey = null;
41
+ /** The failing tests a red verdict names, when the list is complete — non-empty and short of
42
+ * FAILING_TESTS_MAX, which may have been cut short (the same rule as onlyKnownMainFailures).
43
+ * Empty when it names none: main's red is then not a set of known failures, and the block stays. */
44
+ function namedFailures(red) {
45
+ const tests = red.failingTests ?? [];
46
+ return tests.length > 0 && tests.length < FAILING_TESTS_MAX ? tests : [];
47
+ }
32
48
  /** The red-main message's shared spine — the SHA, script, and failure headline a red baseline
33
49
  * carries, phrased once so the once-per-SHA warning and the main_red outcome cannot drift apart
34
50
  * over what main looks like. `action` is the consequence each caller names. */
@@ -37,19 +53,74 @@ function redMainMessage(red, action) {
37
53
  return `${mainRedPhrase(red.sha)} (${red.script}${firstLine ? `: ${firstLine}` : ""}) — ${action}`;
38
54
  }
39
55
  /** Log the team-wide `main_red_hold` event (the notify hook pages it) for `red`'s SHA at
40
- * most once per process. Module-level (with lastMainRedSha) so the gate and the bugfix
41
- * handoff share one guard: whichever observes the red first logs it, the other stays silent. */
42
- function warnMainRedOnce(root, red) {
56
+ * most once per process, and hand the red suite to the healer. Module-level (with
57
+ * lastMainRedSha) so the gate and the bugfix handoff share one guard: whichever observes the
58
+ * red first logs it, the other stays silent. `foundBy` is the role whose path verified the
59
+ * red — when that role is not bugfix itself, one prompt is enqueued to bugfix's own inbox so
60
+ * the healer runs on the orchestrator's next poll instead of waiting out its own interval or
61
+ * deferral; bugfix finding its own red skips the enqueue because its running run already
62
+ * carries the note and IS the heal (and enqueuing from inside it would queue a second run). A
63
+ * queued per-role prompt makes the role due by itself (`isEligible`, scheduling.ts, returns
64
+ * `{ run: true, reason: "inbox" }` on a non-empty inbox before any interval, backoff, or
65
+ * deferral check), and `pollRunnerReasons` skips the need-based deferral block for reason
66
+ * `inbox`, so no scheduler change is needed. A heal prompt that cannot be queued is warned under
67
+ * `foundBy` and clears the heal key, never thrown. */
68
+ function warnMainRedOnce(root, red, foundBy) {
43
69
  if (lastMainRedSha === red.sha)
44
70
  return;
45
71
  lastMainRedSha = red.sha;
72
+ const named = namedFailures(red);
46
73
  logEventBestEffort(root, {
47
74
  source: TUMWATER_SOURCE,
48
75
  type: "main_red_hold",
49
76
  sha: red.sha,
50
77
  script: red.script,
51
- message: redMainMessage(red, "code merges blocked until main is green"),
78
+ ...(named.length > 0 ? { failingTests: named } : {}),
79
+ message: named.length > 0
80
+ ? `${mainRedPhrase(red.sha)} (${plural(named.length, "failing test")}: ${clipToWidth(named[0] ?? "", 80)}${named.length > 1 ? `, +${named.length - 1} more` : ""}); changes that add no new failures still merge`
81
+ : redMainMessage(red, "code merges blocked until main is green"),
52
82
  });
83
+ // Bugfix is the only repairer, and it is exempt from mainRedGate — but it is a work role
84
+ // that with no open bugs defers like a maintenance role, so a red main discovered by any
85
+ // other path could sit unrepaired until bugfix's own interval or a coincidental wake.
86
+ // Queue the heal: one prompt per newly-verified red SHA per process (a restart re-queues
87
+ // once, desirable because the cache is cold then too). With named failures the key is the
88
+ // failing set, so a second red commit with the same failures queues nothing.
89
+ const healKey = named.length > 0 ? [...named].sort().join("\n") : red.sha;
90
+ if (lastHealKey === healKey)
91
+ return;
92
+ lastHealKey = healKey;
93
+ if (baseRoleOf(foundBy) !== BUGFIX_ROLE) {
94
+ try {
95
+ enqueueRolePrompt(root, BUGFIX_ROLE, mainRedHealPrompt(red.sha));
96
+ }
97
+ catch (err) {
98
+ // Clear the key so a later verification retries the heal. lastMainRedSha stays set, so a
99
+ // second call for this same SHA still returns early: the retry comes with the next red
100
+ // SHA or a fresh process.
101
+ lastHealKey = null;
102
+ warnEventBestEffort(root, foundBy, `could not queue the heal prompt for bugfix: ${errorMessage(err)}`);
103
+ }
104
+ }
105
+ }
106
+ /** The one-line, self-limiting directive queued for the bugfix healer when a non-bugfix path
107
+ * newly verifies a red main. It names the short SHA the check was reported red at, tells the
108
+ * healer to verify main is green now, and to reproduce and land the fix only if it is still
109
+ * red. The self-limiting wording is the green race's whole handling: if main turned green
110
+ * before the healer consumes the prompt, bugfixMainRedNote returns undefined (no `<main-red>`
111
+ * note is appended) and the "verify green" clause makes the run a no-op. */
112
+ export function mainRedHealPrompt(sha, detail) {
113
+ const base = `Main was reported red at commit ${shortSha(sha)}. Verify main is green now; if it is still red, reproduce the failure and land the fix.`;
114
+ if (!detail)
115
+ return base;
116
+ const tests = detail.failingTests.length > 0 ? ` Failing: ${detail.failingTests.join("; ")}.` : "";
117
+ const tail = detail.outputTail && detail.outputTail.length > 0 ? ` Output tail: ${detail.outputTail.join(" | ")}` : "";
118
+ return `${base} The watch named ${shortSha(detail.culprit)} as the commit that turned it red.${tests}${tail}`;
119
+ }
120
+ /** Queue the heal prompt for bugfix with the culprit's detail: the fallback when the automatic
121
+ * revert (src/baseline/main-revert.ts) is skipped or fails. */
122
+ export function queueCulpritHeal(root, tip, detail) {
123
+ enqueueRolePrompt(root, BUGFIX_ROLE, mainRedHealPrompt(tip, detail));
53
124
  }
54
125
  /** checkMainBaseline's per-run hook: log the one run per SHA (cache misses only) under the role
55
126
  * that paid for it, with its duration — the gate's build_check sibling, so both halves of the
@@ -57,7 +128,7 @@ function warnMainRedOnce(root, red) {
57
128
  * check this module runs — the bugfix handoff, the authoring gate, and mainTipVerdict's tip
58
129
  * check (which review.ts's gate and merge-check-failures.ts's attributeRedCheck reach
59
130
  * through it) — so the event's shape cannot drift between them. */
60
- function baselineCheckLogger(root, role) {
131
+ export function baselineCheckLogger(root, role) {
61
132
  return baselineCheckEventLogger(role, (event) => logEventBestEffort(root, event));
62
133
  }
63
134
  /** Red-main handoff for the `bugfix` healer: mainRedGate exempts
@@ -66,19 +137,23 @@ function baselineCheckLogger(root, role) {
66
137
  * (cache and provisional-red re-verification reused unchanged, so it costs at most the one run
67
138
  * the gate would have paid for) and, when main is red, returns the note that points the healer
68
139
  * at the failure. Returns undefined on green, no declared check, or an environmental skip — the
69
- * healer's tick then proceeds exactly as it did before this existed. Never throws. */
140
+ * healer's run then proceeds exactly as it did before this existed. Never throws. */
70
141
  export async function bugfixMainRedNote(root, role, wt) {
71
142
  // The declared check is detected through the live config (plans/portability.md §6/7); a
72
- // broken file degrades to defaults, which declare none — the tick then proceeds as before.
143
+ // broken file degrades to defaults, which declare none — the run then proceeds as before.
73
144
  const config = liveConfig(root);
74
- const baseline = await checkMainBaseline(wt, config, baselineCheckLogger(root, role));
145
+ const baseline = mergeCommandOf(config)
146
+ ? { baseline: await watchedBaseline(wt) }
147
+ : await checkMainBaseline(wt, config, baselineCheckLogger(root, role));
75
148
  const red = baseline.baseline;
149
+ if (red?.status === "green")
150
+ lastHealKey = null;
76
151
  if (red?.status !== "red")
77
152
  return undefined;
78
- warnMainRedOnce(root, red);
153
+ warnMainRedOnce(root, red, role);
79
154
  return buildMainRedNote(red.sha, red.script, failureHeadline(red.outputTail), red.outputTail);
80
155
  }
81
- /** Gate one fresh tick on main's baseline. Returns null when authoring may proceed — role not
156
+ /** Gate one fresh run on main's baseline. Returns null when authoring may proceed — role not
82
157
  * blocked, no declared check, green, or an environmental skip (warned under the role and
83
158
  * proceeded with, exactly like the review gate's pre-check) — otherwise the terminal
84
159
  * `main_red` outcome for the caller to return as-is. Never throws: git, detection, and
@@ -86,8 +161,11 @@ export async function bugfixMainRedNote(root, role, wt) {
86
161
  export async function mainRedGate(root, role, wt,
87
162
  /** The sleep clock the baseline run measures host suspension with; defaults to the real
88
163
  * sampleSleepClock. A test seam — production callers leave it unset. */
89
- sampleSleep) {
90
- // User-defined loops are blocked alongside the built-in code roles (plans/user-defined-loops.md):
164
+ sampleSleep,
165
+ /** Called with main's failing tests when the red names them: the role then runs on the red
166
+ * main instead of ending `main_red`, and the caller puts them in its prompt. */
167
+ onKnownRed) {
168
+ // User-defined roles are blocked alongside the built-in code roles (plans/user-defined-loops.md):
91
169
  // an unknown charter may produce code, and on red main such diffs are rejected deterministically
92
170
  // at the gate's pre-check — an authoring run would be pure waste. Customs come from
93
171
  // tumwater.json, not the catalog, so read the live config (stat-cached; a broken file
@@ -95,7 +173,12 @@ sampleSleep) {
95
173
  const cfg = liveConfig(root);
96
174
  if (!baselineBlocked(role) && !isCustomRole(cfg, role))
97
175
  return null;
98
- const baseline = await checkMainBaseline(wt, cfg, baselineCheckLogger(root, role), false, false, sampleSleep);
176
+ // With check.mergeCommand set the lock no longer runs the full check, and neither does this
177
+ // gate: it reads the main watch's newest verdict for main's tip (or its nearest checked
178
+ // ancestor). No verdict yet reads as green.
179
+ const baseline = mergeCommandOf(cfg)
180
+ ? { baseline: await watchedBaseline(wt) }
181
+ : await checkMainBaseline(wt, cfg, baselineCheckLogger(root, role), false, false, sampleSleep);
99
182
  if (baseline.unverified) {
100
183
  // The baseline run spanned a host sleep: no verdict about main, nothing cached (BUGS.md
101
184
  // 2026-09-30). Warn-and-proceed, like a skip — the sleep is named, not a test failure.
@@ -108,17 +191,24 @@ sampleSleep) {
108
191
  }
109
192
  if (baseline.baseline?.status === "red") {
110
193
  const red = baseline.baseline;
111
- warnMainRedOnce(root, red);
194
+ warnMainRedOnce(root, red, role);
195
+ const named = namedFailures(red);
196
+ if (named.length > 0) {
197
+ onKnownRed?.(named);
198
+ return null;
199
+ }
112
200
  return {
113
201
  result: "main_red",
114
202
  summary: "code merges blocked until main is green",
115
- // The cause rides the outcome so the tick's `run_end` carries it
116
- // (tick-finalize.ts folds it into state.lastError before logging): the digest itemizes
203
+ // The cause rides the outcome so the run's `run_end` carries it
204
+ // (run-finalize.ts folds it into state.lastError before logging): the digest itemizes
117
205
  // the outcome that blocks every merge, instead of leaving it a bare count behind a
118
206
  // once-per-SHA warning (BUGS.md 2026-09-28).
119
207
  error: redMainMessage(red, "authoring skipped until main is green"),
120
208
  };
121
209
  }
210
+ if (baseline.baseline?.status === "green")
211
+ lastHealKey = null;
122
212
  return null;
123
213
  }
124
214
  /** Serializes mainTipVerdict's use of its one worktree: the merge pipeline's verifies run their
@@ -141,10 +231,27 @@ let gateMainQueue = Promise.resolve();
141
231
  * failure on a clock- or load-sensitive test whose cached green has gone stale (BUGS.md
142
232
  * 2026-10-06). The merge pipeline's attribution reads the cached verdict instead. */
143
233
  export function mainTipVerdict(root, role, mainBranch, config, forceFresh = false) {
144
- const run = gateMainQueue.then(() => verdictAtMainTip(root, role, mainBranch, config, forceFresh));
234
+ return queueBehindMainGates(() => verdictAtMainTip(root, role, mainBranch, config, forceFresh));
235
+ }
236
+ /** Run `task` after every earlier gate-main task has settled, and make it the queue's new tail. A
237
+ * task's rejection reaches only its own caller; the next task still runs. */
238
+ function queueBehindMainGates(task) {
239
+ const run = gateMainQueue.then(task);
145
240
  gateMainQueue = run.catch(() => undefined);
146
241
  return run;
147
242
  }
243
+ /** Run `fn` in the `_gate-main` mirror re-pointed at `mainBranch`'s tip, queued behind the gates
244
+ * mainTipVerdict serves (gateMainQueue) and holding the mirror as they do. The main watch's way in
245
+ * (main-watch.ts): it hands `fn` the checkout and the tip it holds. Rejects when main is
246
+ * unreadable or the checkout fails — the caller is a background task that must catch. */
247
+ export function withMainTipWorktree(root, mainBranch, fn) {
248
+ return queueBehindMainGates(() => useWorktree(root, gateMainWorktreePath(root), async () => {
249
+ const tip = await branchHead(root, mainBranch);
250
+ if (!tip)
251
+ throw new Error(`${mainBranch} is unreadable`);
252
+ return fn(await ensureDetachedWorktree(root, gateMainWorktreePath(root), tip));
253
+ }));
254
+ }
148
255
  async function verdictAtMainTip(root, role, mainBranch, config, forceFresh) {
149
256
  // Hold the `_gate-main` mirror from before ensureDetachedWorktree (its reset is part of the
150
257
  // use) through the baseline check (plans/disk-floor.md, part 2/4).
@@ -164,7 +271,7 @@ async function verdictAtMainTipIn(root, role, mainBranch, config, forceFresh) {
164
271
  if (baseline?.status === "green")
165
272
  return { status: "green", sha: baseline.sha };
166
273
  if (baseline?.status === "red") {
167
- warnMainRedOnce(root, baseline);
274
+ warnMainRedOnce(root, baseline, role);
168
275
  return { status: "red", sha: baseline.sha };
169
276
  }
170
277
  return {
@@ -0,0 +1,133 @@
1
+ import { queueCulpritHeal } from "./main-red.js";
2
+ import { gateCommandOf } from "../build/build-check-detect.js";
3
+ import { runScopedBuildCheck } from "../build/build-check-scoped.js";
4
+ import { withLock } from "../concurrency/lock.js";
5
+ import { logEventBestEffort, TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
6
+ import { branchHead, patchId } from "../git/git.js";
7
+ import { gitTry } from "../git/git-run.js";
8
+ import { stampedSubject } from "../git/commit-message.js";
9
+ import { ensureDetachedWorktree } from "../git/worktree.js";
10
+ import { useWorktree } from "../git/worktree-use.js";
11
+ import { ffMainToOrWhy } from "../merge/merge-git.js";
12
+ import { revertCommitIn } from "../operator/revert.js";
13
+ import { mergeLockDir, mergeWorktreePath } from "../paths.js";
14
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
15
+ import { baseRoleOf } from "../roles/runner-ids.js";
16
+ import { errorMessage } from "../text/text.js";
17
+ import { shortSha } from "../text/format.js";
18
+ const REVERT_PREFIXES = ["tumwater(operator): Revert", "tumwater(tumwater): Revert"]; // vocabulary-ok: commit prefixes are machine strings
19
+ /** Why the culprit is not reverted automatically, or null when it should be: no runner (an
20
+ * operator or hand commit), a director's merge, a revert itself, or a commit whose revert
21
+ * already merged. */
22
+ export async function revertSkipReason(root, mainBranch, c) {
23
+ if (c.runner === undefined)
24
+ return "no role runner merged it";
25
+ if (baseRoleOf(c.runner) === DIRECTOR_ROLE)
26
+ return "the director merged it";
27
+ const subject = ((await gitTry(root, "log", "-1", "--format=%s", c.commit)) ?? "").trim();
28
+ if (REVERT_PREFIXES.some((p) => subject.startsWith(p)))
29
+ return "it is itself a revert";
30
+ const merged = (await gitTry(root, "log", mainBranch, "--format=%s", "--fixed-strings", `--grep=Revert ${shortSha(c.commit)}`)) ?? "";
31
+ if (merged.split("\n").some((s) => REVERT_PREFIXES.some((p) => s === `${p} ${shortSha(c.commit)}`)))
32
+ return "a revert of it already merged";
33
+ return null;
34
+ }
35
+ /** The main watch's culprit hand-off: starts the automatic revert, or falls back to bugfix's heal
36
+ * prompt when the revert is skipped. Never rejects, because its caller discards the promise and an
37
+ * unhandled rejection would end the orchestrator: a heal prompt that cannot be queued is warned. */
38
+ export async function routeMainCulprit(root, mainBranch, c, startRevert) {
39
+ const skip = await revertSkipReason(root, mainBranch, c);
40
+ if (skip === null) {
41
+ startRevert(c);
42
+ return;
43
+ }
44
+ try {
45
+ queueCulpritHeal(root, c.tip, { culprit: c.commit, failingTests: c.failingTests });
46
+ }
47
+ catch (err) {
48
+ warnEventBestEffort(root, TUMWATER_SOURCE, `not reverting ${shortSha(c.commit)} (${skip}) and could not queue the heal prompt for bugfix: ${errorMessage(err)}`);
49
+ }
50
+ }
51
+ /** Run one revert task on main's current tip in the shared `_merge` checkout: revert the culprit,
52
+ * run `check.gateCommand` on the result when one is set (red aborts), and fast-forward main under
53
+ * the merge lock. Logs `main_revert` on success and calls `onReverted` (the hand-back to a role);
54
+ * every failure queues the heal prompt instead. Never throws: a hand-off that cannot be queued is
55
+ * warned, not thrown. */
56
+ export async function runMainRevert(root, mainBranch, config, c, onReverted, onFailed) {
57
+ /** Hand the failure to a role and return the warning's suffix saying truthfully what happened. */
58
+ const heal = (reason, outputTail) => {
59
+ const toReviewer = c.postReview && onFailed;
60
+ try {
61
+ if (toReviewer)
62
+ onFailed(c, reason);
63
+ else
64
+ queueCulpritHeal(root, c.tip, { culprit: c.commit, failingTests: c.failingTests, outputTail });
65
+ return toReviewer ? "sent the reviewer's reasons to its role" : "queued the heal prompt for bugfix";
66
+ }
67
+ catch (err) {
68
+ return toReviewer
69
+ ? `could not send the reviewer's reasons to its role: ${errorMessage(err)}`
70
+ : `could not queue the heal prompt for bugfix: ${errorMessage(err)}`;
71
+ }
72
+ };
73
+ try {
74
+ const failure = await useWorktree(root, mergeWorktreePath(root), async () => {
75
+ const tip = await branchHead(root, mainBranch);
76
+ if (tip === null)
77
+ return { reason: `${mainBranch} is unreadable` };
78
+ const wt = await ensureDetachedWorktree(root, mergeWorktreePath(root), tip);
79
+ const subject = ((await gitTry(wt, "log", "-1", "--format=%s", c.commit)) ?? "").trim();
80
+ const tests = c.failingTests.length > 0 ? `\n\nFailing tests: ${c.failingTests.join("; ")}` : "";
81
+ const why = c.postReview
82
+ ? `which a post-merge review rejected.\n\nReasons: ${c.postReview.reasons.join("; ")}`
83
+ : `which turned main red.${tests}`;
84
+ const message = `${stampedSubject("tumwater", `Revert ${shortSha(c.commit)}`)}\n\nReverts ${c.commit} (${subject}), ${why}`;
85
+ const reverted = await revertCommitIn(wt, c.commit, message);
86
+ if (!reverted.ok)
87
+ return { reason: reverted.error };
88
+ if (gateCommandOf(config) !== undefined) {
89
+ const gate = await runScopedBuildCheck(root, TUMWATER_SOURCE, "gate", wt, config);
90
+ if (gate && gate.outcome.status !== "passed")
91
+ return { reason: `the gate command was ${gate.outcome.status} on the revert`, tail: gate.outcome.outputTail };
92
+ }
93
+ const ffFailure = await withLock(mergeLockDir(root), () => ffMainToOrWhy(root, reverted.sha, mainBranch));
94
+ if (ffFailure !== null)
95
+ return { reason: ffFailure };
96
+ // The culprit's patch-id (`C^...C`, src/git/git.ts patchId, -U0 so a re-landing onto a
97
+ // moved main keeps the id): the fast-merge guardrail G2 matches a later `merged`
98
+ // event against it to spot a change that came back unchanged (plans/fast-merge.md).
99
+ const culpritPatch = await patchId(wt, `${c.commit}^`, c.commit).catch(() => null);
100
+ logEventBestEffort(root, {
101
+ source: TUMWATER_SOURCE,
102
+ type: "main_revert",
103
+ commit: c.commit,
104
+ revert: reverted.sha,
105
+ runner: c.runner,
106
+ failingTests: c.failingTests,
107
+ ...(culpritPatch ? { patchId: culpritPatch } : {}),
108
+ });
109
+ await onReverted?.(c).catch((err) => logEventBestEffort(root, {
110
+ source: TUMWATER_SOURCE,
111
+ type: "warning",
112
+ message: `could not hand ${shortSha(c.commit)} back to its role: ${errorMessage(err)}`,
113
+ }));
114
+ return null;
115
+ });
116
+ if (failure !== null) {
117
+ const suffix = heal(failure.reason, failure.tail);
118
+ logEventBestEffort(root, {
119
+ source: TUMWATER_SOURCE,
120
+ type: "warning",
121
+ message: `could not revert ${shortSha(c.commit)}: ${failure.reason} — ${suffix}`,
122
+ });
123
+ }
124
+ }
125
+ catch (err) {
126
+ const suffix = heal(errorMessage(err));
127
+ logEventBestEffort(root, {
128
+ source: TUMWATER_SOURCE,
129
+ type: "warning",
130
+ message: `revert of ${shortSha(c.commit)} failed: ${errorMessage(err)} — ${suffix}`,
131
+ });
132
+ }
133
+ }
@@ -0,0 +1,184 @@
1
+ import { checkMainBaseline } from "./main-baseline.js";
2
+ import { findRedCulprit } from "./main-culprit.js";
3
+ import { baselineCheckLogger, withMainTipWorktree } from "./main-red.js";
4
+ import { CHECK_TIER } from "../concurrency/check-permit.js";
5
+ import { FAILING_TESTS_MAX } from "../build/build-check-report.js";
6
+ import { detectBuildCheck } from "../build/build-check-detect.js";
7
+ import { gitTry } from "../git/git-run.js";
8
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
9
+ import { errorMessage } from "../text/text.js";
10
+ import { fileQuarantineBugsOnMain } from "../backlog/backlog-archive.js";
11
+ import { loadQuarantine, markQuarantineFiled, onlyQuarantined, quarantinedTests, quarantineTests, releaseQuarantine, } from "../review/known-flakes.js";
12
+ import { shortSha } from "../text/format.js";
13
+ /** The failing tests of each root's newest watch verdict, keyed by root. */
14
+ const knownFailures = new Map();
15
+ /** Main's known failures: the failing tests of the watch's newest verdict — main's tip, or the
16
+ * nearest tip it checked while the newest head waits — and every quarantined flaky test
17
+ * (review/known-flakes.ts). Empty when the verdict is green, red with no parsed test, or none has
18
+ * been recorded yet in this process, and nothing is quarantined. */
19
+ export function knownMainFailures(root) {
20
+ return [...new Set([...(knownFailures.get(root) ?? []), ...quarantinedTests(root)])];
21
+ }
22
+ /** The failing tests of a failed check when every one of them is a known main failure; null when
23
+ * the output names no test, any test is outside the known set, or none is known. A list at
24
+ * FAILING_TESTS_MAX may have been cut short, so it never counts as complete. */
25
+ export function onlyKnownMainFailures(root, failingTests) {
26
+ if (!failingTests || failingTests.length === 0 || failingTests.length >= FAILING_TESTS_MAX)
27
+ return null;
28
+ const known = new Set(knownMainFailures(root));
29
+ return failingTests.every((t) => known.has(t)) ? [...failingTests] : null;
30
+ }
31
+ export function createMainWatch(root, mainBranch, onCulprit) {
32
+ let lastWatched;
33
+ let inFlight = null;
34
+ let housekeeping = null;
35
+ let lastFilingWarning = "";
36
+ /** Release quarantined tests whose bug left `## Open`, and file the bugs of newly quarantined
37
+ * ones on main. A filing the primary checkout cannot take warns (once per reason) and is
38
+ * retried on a later poll. Never throws. */
39
+ async function settleQuarantine() {
40
+ try {
41
+ const released = releaseQuarantine(root);
42
+ if (released.length > 0) {
43
+ logEventBestEffort(root, {
44
+ source: TUMWATER_SOURCE,
45
+ type: "warning",
46
+ message: `quarantine released ${released.join(", ")}: its BUGS.md entry is no longer open`,
47
+ });
48
+ }
49
+ const pending = loadQuarantine(root).filter((e) => e.bug === "");
50
+ if (pending.length === 0)
51
+ return;
52
+ const outcome = await fileQuarantineBugsOnMain(root, pending);
53
+ if (outcome.status === "filed") {
54
+ markQuarantineFiled(root, outcome.bugs);
55
+ lastFilingWarning = "";
56
+ return;
57
+ }
58
+ if (outcome.reason !== lastFilingWarning) {
59
+ lastFilingWarning = outcome.reason;
60
+ logEventBestEffort(root, {
61
+ source: TUMWATER_SOURCE,
62
+ type: "warning",
63
+ message: `quarantined flaky tests are not filed in BUGS.md yet: ${outcome.reason}`,
64
+ });
65
+ }
66
+ }
67
+ catch (err) {
68
+ logEventBestEffort(root, {
69
+ source: TUMWATER_SOURCE,
70
+ type: "warning",
71
+ message: `quarantine housekeeping failed: ${errorMessage(err)}`,
72
+ });
73
+ }
74
+ }
75
+ async function watchOnce(mainHead, config) {
76
+ if (!detectBuildCheck(root, config)) {
77
+ lastWatched = mainHead; // no declared check: nothing to watch until main moves
78
+ return;
79
+ }
80
+ await withMainTipWorktree(root, mainBranch, async (wt) => {
81
+ const started = Date.now();
82
+ let ran = false;
83
+ const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
84
+ const hook = (run) => {
85
+ ran = true;
86
+ onRun(run);
87
+ };
88
+ let check = await checkMainBaseline(wt, config, hook, false, false, undefined, false, CHECK_TIER.merge);
89
+ if (check.baseline?.status === "red") {
90
+ const first = check.baseline;
91
+ check = await checkMainBaseline(wt, config, hook, true, false, undefined, false, CHECK_TIER.merge);
92
+ // A red the same tree's confirming re-run turns green is a flake: quarantine its tests.
93
+ if (check.baseline?.status === "green")
94
+ quarantineTests(root, first.failingTests, (first.failingTests ?? []).join("\n"));
95
+ }
96
+ const baseline = check.baseline;
97
+ const previous = lastWatched;
98
+ // A skipped or sleep-spanned run made no verdict: the head still counts as watched, so the
99
+ // next poll does not start another run for it.
100
+ if (!baseline)
101
+ return void (lastWatched = await headOf(wt));
102
+ lastWatched = baseline.sha;
103
+ // A red naming only quarantined tests counts as green: no bisect, no redeploy hold.
104
+ const quarantined = baseline.status === "red" ? onlyQuarantined(root, baseline.failingTests) : null;
105
+ const red = baseline.status === "red" && !quarantined;
106
+ knownFailures.set(root, red ? (baseline.failingTests ?? []) : []);
107
+ const counted = previous === undefined
108
+ ? 1
109
+ : Number(((await gitTry(wt, "rev-list", "--first-parent", "--count", `${previous}..${baseline.sha}`)) ?? "").trim());
110
+ logEventBestEffort(root, {
111
+ source: TUMWATER_SOURCE,
112
+ type: "main_check",
113
+ sha: baseline.sha,
114
+ status: red ? "failed" : "passed",
115
+ durationMs: ran ? Date.now() - started : 0,
116
+ covered: Number.isFinite(counted) && counted > 0 ? counted : 1,
117
+ ...(red ? { failingTests: baseline.failingTests ?? [] } : {}),
118
+ ...(quarantined ? { quarantined } : {}),
119
+ ...(baseline.inheritedFrom !== undefined ? { inherited: true } : {}),
120
+ });
121
+ if (!red)
122
+ return;
123
+ // Once per red head: lastWatched already holds it, so a later poll does not search again.
124
+ const found = await findRedCulprit(root, wt, config, baseline);
125
+ if (found.kind === "culprit") {
126
+ logEventBestEffort(root, {
127
+ source: TUMWATER_SOURCE,
128
+ type: "main_culprit",
129
+ commit: found.commit,
130
+ ...(found.runner !== undefined ? { runner: found.runner } : {}),
131
+ failingTests: found.failingTests,
132
+ green: found.green,
133
+ });
134
+ onCulprit?.({
135
+ commit: found.commit,
136
+ ...(found.runner !== undefined ? { runner: found.runner } : {}),
137
+ failingTests: found.failingTests,
138
+ tip: baseline.sha,
139
+ });
140
+ }
141
+ else {
142
+ logEventBestEffort(root, {
143
+ source: TUMWATER_SOURCE,
144
+ type: "warning",
145
+ message: found.kind === "environment"
146
+ ? `main's last green commit ${shortSha(found.green)} is red now too — not attributing`
147
+ : `main watch could not name the commit that turned main red: ${found.reason}`,
148
+ });
149
+ }
150
+ });
151
+ }
152
+ return {
153
+ poll(mainHead, held, config) {
154
+ if (!held && !housekeeping) {
155
+ housekeeping = settleQuarantine().finally(() => {
156
+ housekeeping = null;
157
+ });
158
+ }
159
+ if (held || mainHead === "" || inFlight || mainHead === lastWatched)
160
+ return;
161
+ inFlight = watchOnce(mainHead, config)
162
+ .catch((err) => {
163
+ // A failed watch (main unreadable, checkout refused) records nothing and is not
164
+ // retried until main moves, so a persistent failure warns once per head.
165
+ lastWatched = mainHead;
166
+ logEventBestEffort(root, {
167
+ source: TUMWATER_SOURCE,
168
+ type: "warning",
169
+ message: `main watch could not check main: ${errorMessage(err)}`,
170
+ });
171
+ })
172
+ .finally(() => {
173
+ inFlight = null;
174
+ });
175
+ },
176
+ idle: async () => {
177
+ while (inFlight || housekeeping)
178
+ await (inFlight ?? housekeeping);
179
+ },
180
+ };
181
+ }
182
+ async function headOf(wt) {
183
+ return ((await gitTry(wt, "rev-parse", "HEAD")) ?? "").trim();
184
+ }
@@ -1,34 +1,32 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
- import { INITIAL_PROMPT_MAX_CHARS, PROMPT_END, PROMPT_START, briefPath, replaceInitialPrompt } from "./brief.js";
3
+ import { BRIEF_TEXT_MAX_CHARS, PROMPT_END, PROMPT_START, briefPath, replaceBriefText } from "./brief.js";
5
4
  import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
6
- import { withStateLock } from "../concurrency/lock.js";
5
+ import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
7
6
  import { writeTextAtomic } from "../files/files.js";
8
7
  import { stampedSubject } from "../git/commit-message.js";
9
- import { primaryWorktreeLockPath } from "../paths.js";
10
8
  import { PREFILL_REASON } from "../text/phrases.js";
11
9
  import { tooLongMessage } from "../text/text.js";
12
10
  const NO_BRIEF = "no project brief with a prompt block found — run `tumwater init` first";
13
- /** Replace the initial prompt with `text` under the checks init applies (non-empty, at most
14
- * INITIAL_PROMPT_MAX_CHARS, no prompt marker), write it under the primary-worktree lock, and
11
+ /** Replace the brief text with `text` under the checks init applies (non-empty, at most
12
+ * BRIEF_TEXT_MAX_CHARS, no prompt marker), write it under the primary-worktree lock, and
15
13
  * commit that one file on main. The one writer behind `tumwater brief --file` and the
16
14
  * dashboard's Settings card, so the two cannot drift. */
17
- export async function editInitialPrompt(root, text) {
15
+ export async function editBriefText(root, text) {
18
16
  const prompt = text.trim();
19
17
  if (prompt === "")
20
18
  return { ok: false, error: "the new brief is empty" };
21
- if (prompt.length > INITIAL_PROMPT_MAX_CHARS)
22
- return { ok: false, error: tooLongMessage("the new brief", prompt.length, INITIAL_PROMPT_MAX_CHARS, PREFILL_REASON) };
19
+ if (prompt.length > BRIEF_TEXT_MAX_CHARS)
20
+ return { ok: false, error: tooLongMessage("the new brief", prompt.length, BRIEF_TEXT_MAX_CHARS, PREFILL_REASON) };
23
21
  if (prompt.includes(PROMPT_START) || prompt.includes(PROMPT_END))
24
22
  return { ok: false, error: "the new brief contains a tumwater:prompt marker, which would break the block for every reader" };
25
23
  const file = briefPath(root);
26
24
  if (file === null)
27
25
  return { ok: false, error: NO_BRIEF };
28
26
  const name = path.basename(file);
29
- const changed = withStateLock(primaryWorktreeLockPath(root), () => {
27
+ const changed = withPrimaryWorktreeLock(root, () => {
30
28
  const current = fs.readFileSync(file, "utf8");
31
- const next = replaceInitialPrompt(current, prompt);
29
+ const next = replaceBriefText(current, prompt);
32
30
  if (next === null)
33
31
  return `${name} no longer carries a prompt block`;
34
32
  if (next === current)