tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,5 +1,6 @@
1
+ // vocabulary: plain
1
2
  import { runBuildCheck, } from "../build/build-check.js";
2
- import { SLEEP_SPAN_TOLERANCE_MS, owesCleanRetry } from "../build/build-check-events.js";
3
+ import { SLEEP_SPAN_TOLERANCE_MS, runWithCleanRetry } from "../build/build-check-events.js";
3
4
  import { sampleSleepClock } from "../build/host-sleep.js";
4
5
  import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
5
6
  import { detectBuildCheck } from "../build/build-check-detect.js";
@@ -10,11 +11,12 @@ import { isExemptDiff } from "../review/exemptions.js";
10
11
  import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
11
12
  import { isJsonObject } from "../files/json-object.js";
12
13
  import { baselineVerdictsPath } from "../paths.js";
13
- /** Fleet-shared verdict cache, keyed by main SHA. In-memory only: after a restart the cache is cold
14
+ import { memoSetLru } from "../collections.js";
15
+ /** Team-shared verdict cache, keyed by main SHA. In-memory only: after a restart the cache is cold
14
16
  * and one re-check per red SHA happens — cheap and deterministic, mirroring the budget gate's
15
17
  * stateless resume. Entries come from two sources: checkMainBaseline's own runs, and
16
18
  * noteGreenBaseline seeding a green verdict for a SHA that just became main
17
- * (src/landing/landing-merge.ts: either its in-lock post-rebase re-check passed on exactly that
19
+ * (src/merge/merge-to-main.ts: either its in-lock post-rebase re-check passed on exactly that
18
20
  * tree, or the rebase was a no-op so the review gate's pre-check had already run green on it).
19
21
  * A third kind of entry is an inherited green (MainBaseline.inheritedFrom), which no check
20
22
  * observed. It is carried over across doc-only differences (inheritedGreen, noteInheritedGreen)
@@ -26,7 +28,7 @@ import { baselineVerdictsPath } from "../paths.js";
26
28
  * tree: the same SHA can fail in one worktree and pass in another when the failure is really
27
29
  * about the environment the check ran in (a worktree with no install, a half-written
28
30
  * node_modules). On 2026-09-08 exactly that happened — a role worktree's environmental red
29
- * became the fleet-wide verdict that blocked the harness's own restart (BUGS.md) — so a caller
31
+ * became the team-wide verdict that blocked the harness's own restart (BUGS.md) — so a caller
30
32
  * whose false block is expensive may re-verify a red in its own worktree (`reverifyRed`), and a
31
33
  * green from that run promotes the SHA for everyone. Since 2026-09-18 that re-verification is
32
34
  * not opt-in only: a red carries the worktrees that observed it (`redFrom`) and is re-run by the
@@ -34,29 +36,24 @@ import { baselineVerdictsPath } from "../paths.js";
34
36
  * role until main moves — see shouldRerunRed. */
35
37
  const baselineCache = new Map();
36
38
  /** Safety cap so the per-process verdict cache can never grow unbounded: it is keyed by every
37
- * merged code SHA and a red entry carries the failed run's output tail, so a fleet running for
38
- * weeks would accumulate one entry per landing forever. Evicting the oldest entry (Map's
39
- * insertion order) costs at most one redundant suite run if that SHA is consulted again: a
40
- * green for a SHA that becomes main is re-seeded by the next landing (or its own
41
- * checkMainBaseline run), and a red is provisional by shouldRerunRed anyway. In-place updates
42
- * never grow the map and are never evicted. */
39
+ * merged code SHA and a red entry carries the failed run's output tail, so a team running for
40
+ * weeks would accumulate one entry per merge forever. Evicting the oldest entry (Map's insertion
41
+ * order) costs at most one redundant suite run if that SHA is consulted again: a green for a SHA
42
+ * that becomes main is re-seeded by the next merge (or its own checkMainBaseline run), and a red
43
+ * is provisional by shouldRerunRed anyway. In-place updates never grow the map and are never
44
+ * evicted. */
43
45
  const BASELINE_CACHE_MAX = 256;
44
46
  /** Insert or update `sha`'s verdict, evicting the oldest entry when a NEW SHA would exceed the
45
47
  * cap. Both write sites route through here so the cap cannot be bypassed, and so does the
46
48
  * mirror on disk (saveGreens). */
47
49
  function rememberBaseline(sha, baseline) {
48
- if (!baselineCache.has(sha) && baselineCache.size >= BASELINE_CACHE_MAX) {
49
- const oldest = baselineCache.keys().next().value;
50
- if (oldest !== undefined)
51
- baselineCache.delete(oldest);
52
- }
53
- baselineCache.set(sha, baseline);
50
+ memoSetLru(baselineCache, sha, baseline, BASELINE_CACHE_MAX);
54
51
  saveGreens();
55
52
  }
56
53
  /** The file the cache's greens are mirrored to (persistBaselineVerdicts), or undefined while no
57
- * fleet process has asked for it: the CLI and the tests keep the cache in memory only. */
54
+ * team process has asked for it: the CLI and the tests keep the cache in memory only. */
58
55
  let greensFile;
59
- /** Mirror the green verdicts to disk, so the fleet's next generation starts with them. A restart
56
+ /** Mirror the green verdicts to disk, so the team's next generation starts with them. A restart
60
57
  * used to empty the cache, and the first tick after every restart re-ran the suite on a main the
61
58
  * previous generation had already verified (41 of 319 baseline runs over 2026-10-02..12 started
62
59
  * within 10 minutes of an orchestrator start). Only greens are kept: a green is a fact about an
@@ -76,7 +73,7 @@ function saveGreens() {
76
73
  }
77
74
  }
78
75
  /** Load the green verdicts a previous generation saved under `root` and keep mirroring them
79
- * there (saveGreens). Called once at fleet startup (cli/cli-run.ts). An entry the cache already
76
+ * there (saveGreens). Called once at team startup (cli/cli-run.ts). An entry the cache already
80
77
  * holds is left as it is, and a malformed file or entry reads as no data. */
81
78
  export function persistBaselineVerdicts(root) {
82
79
  const file = baselineVerdictsPath(root);
@@ -100,14 +97,14 @@ export function persistBaselineVerdicts(root) {
100
97
  const baselineInFlight = new Map();
101
98
  /** Should a cached RED be re-run here instead of trusted? A red says as much about the
102
99
  * environment the check ran in as about the tree — a worktree mid-install, or a machine under
103
- * the load the fleet deliberately creates starving a timing-sensitive test (BUGS.md's
104
- * load-sensitive tests entry). The role loops consult this fleet-shared cache and, before
100
+ * the load the team deliberately creates starving a timing-sensitive test (BUGS.md's
101
+ * load-sensitive tests entry). The role loops consult this team-shared cache and, before
105
102
  * 2026-09-18, trusted a red forever: one environmental red blocked every code role until main
106
103
  * moved, and main could not move, because blocking the code roles is exactly what stops it —
107
104
  * a deadlock broken only by restarting the orchestrator, since the cache is per-process.
108
105
  *
109
106
  * So a red is provisional until two DIFFERENT worktrees have seen it. The second observer pays
110
- * one extra suite run and a green from it promotes the SHA fleet-wide; a second red makes the
107
+ * one extra suite run and a green from it promotes the SHA team-wide; a second red makes the
111
108
  * verdict authoritative for everyone. The cost is bounded at ONE confirmation run per SHA, not
112
109
  * one per role. `reverifyRed` still forces a run unconditionally for the redeploy gate, whose
113
110
  * false block is expensive enough to always pay for its own opinion. */
@@ -119,32 +116,32 @@ function shouldRerunRed(cached, wt, reverifyRed) {
119
116
  const seen = cached.redFrom ?? [];
120
117
  return seen.length < 2 && !seen.includes(wt);
121
118
  }
122
- /** Record a green baseline verdict for `sha` WITHOUT running anything. The callers are the landing
123
- * path (src/landing/landing-merge.ts's verifyLanding) and the stack lander
124
- * (src/landing/landing-stack.ts' landStack) — both call it only after their fast-forward
125
- * SUCCEEDED, with the exact SHA that just became main: verifyLanding with the POST-rebase head
126
- * in two cases — its own in-lock re-check just ran this project's declared check green on that
127
- * tree, or the rebase was a no-op so the review gate's pre-check (which runs outside the merge
128
- * lock) had already run green on exactly this tree — and landStack with the stacked tip when a
129
- * scope-`batch` check ran green on exactly it (a skipped check seeds nothing, and neither does
130
- * a merge_blocked stack or a doc-only re-stack that landed without a check of its own). Seeding
131
- * here means that once the merge lands — main now points at this very SHA — the next fresh
119
+ /** Record a green baseline verdict for `sha` WITHOUT running anything. The callers are the merge
120
+ * path (src/merge/merge-to-main.ts's verifyMerge) and the stack lander
121
+ * (src/merge/merge-stack.ts' mergeStack) — both call it only after their fast-forward SUCCEEDED,
122
+ * with the exact SHA that just became main: verifyMerge with the POST-rebase head in two cases —
123
+ * its own in-lock re-check just ran this project's declared check green on that tree, or the
124
+ * rebase was a no-op so the review gate's pre-check (which runs outside the merge lock) had
125
+ * already run green on exactly this tree — and mergeStack with the stacked tip when a
126
+ * scope-`batch` check ran green on exactly it (a skipped check seeds nothing, and neither does a
127
+ * merge_blocked stack or a doc-only re-stack that merged without a check of its own). Seeding
128
+ * here means that once the merge completes — main now points at this very SHA — the next fresh
132
129
  * tick's checkMainBaseline is a cache hit instead of re-running the full suite on an
133
130
  * already-verified tree: for tumwater itself that saves one redundant `npm test` (~1 min) per
134
131
  * merged code tick, plus every other role waking on "main moved" stalling behind that in-flight
135
132
  * run. Never seed a pre-rebase head: whenever main moved under the review, that SHA never
136
133
  * becomes main and the entry would silently miss (BUGS.md 2026-09-08). Safe because git trees
137
134
  * are immutable — a SHA's content cannot change under a cached verdict, the same staleness
138
- * semantics checkMainBaseline already has for its own entries. Only a directly observed pass
139
- * may seed this; skips and failures leave the baseline unknown (the caller decides). */
135
+ * semantics checkMainBaseline already has for its own entries. Only a directly observed pass may
136
+ * seed this; skips and failures leave the baseline unknown (the caller decides). */
140
137
  export function noteGreenBaseline(sha) {
141
138
  rememberBaseline(sha, { status: "green", sha });
142
139
  }
143
140
  /** Record an INHERITED green for `sha` (MainBaseline.inheritedFrom): a tree no check ran on, whose
144
141
  * only difference from the green-verified `from` is in review-exempt paths. The caller is
145
- * landing-merge.ts's verifyLanding, after it skipped the in-lock check because main moved
142
+ * merge-to-main.ts's verifyMerge, after it skipped the in-lock check because main moved
146
143
  * under the gate-verified head only by doc commits. It seeds the head about to become main, as
147
- * verifyLanding's own noteGreenBaseline calls do. If the fast-forward then fails, the entry is
144
+ * verifyMerge's own noteGreenBaseline calls do. If the fast-forward then fails, the entry is
148
145
  * still true of that tree and simply never consulted. It never displaces an observed verdict: a
149
146
  * check that actually ran on this tree knows more than the carry-over. */
150
147
  export function noteInheritedGreen(sha, from) {
@@ -153,22 +150,21 @@ export function noteInheritedGreen(sha, from) {
153
150
  return;
154
151
  rememberBaseline(sha, { status: "green", sha, inheritedFrom: from });
155
152
  }
156
- /** How far down main's first-parent history inheritedGreen looks for a verdict to carry over.
157
- * main gains about two dozen commits an hour under a full fleet, and the walk stops at the
158
- * first cached verdict anyway (every code landing seeds one), so this bound only caps the
159
- * rev-list on a cold cache. */
153
+ /** How far down main's first-parent history inheritedGreen looks for a verdict to carry over. main
154
+ * gains about two dozen commits an hour under a full team, and the walk stops at the first cached
155
+ * verdict anyway (every code merge seeds one), so this bound only caps the rev-list on a cold
156
+ * cache. */
160
157
  const INHERIT_WALK_MAX = 64;
161
- /** A green for `sha` carried over from its nearest verdict-bearing first-parent ancestor, or
162
- * null when there is none to carry. The nearest ancestor with a cached verdict must be GREEN, and
163
- * the tree diff from it to `sha` must touch only `exemptPaths` (config.review.exemptPaths, the
164
- * same patterns that let a doc-only change land with no build check at all). Under that policy
165
- * a doc commit cannot change the suite's verdict, so re-running the suite on every doc-only
166
- * move of main buys nothing. Before this, 82% of the baseline runs (260 of 319 over
167
- * 2026-10-02..12) ran on a main whose tip was doc commits on top of a code landing that had
168
- * already seeded green. The nearest RED stops the walk: a doc commit after a red may be the
169
- * fix, so it is re-checked, never inherited past. An uncached ancestor is walked through, since
170
- * the single diff from the green covers it. Never throws: any git failure means no inheritance,
171
- * and the caller runs the suite as before. */
158
+ /** A green for `sha` carried over from its nearest verdict-bearing first-parent ancestor, or null
159
+ * when there is none to carry. The nearest ancestor with a cached verdict must be GREEN, and the
160
+ * tree diff from it to `sha` must touch only `exemptPaths` (config.review.exemptPaths, the same
161
+ * patterns that let a doc-only change merge with no build check at all). Under that policy a doc
162
+ * commit cannot change the suite's verdict, so re-running the suite on every doc-only move of main
163
+ * buys nothing. Before this, 82% of the baseline runs (260 of 319 over 2026-10-02..12) ran on a
164
+ * main whose tip was doc commits on top of a code merge that had already seeded green. The nearest
165
+ * RED stops the walk: a doc commit after a red may be the fix, so it is re-checked, never inherited
166
+ * past. An uncached ancestor is walked through, since the single diff from the green covers it.
167
+ * Never throws: any git failure means no inheritance, and the caller runs the suite as before. */
172
168
  async function inheritedGreen(wt, sha, exemptPaths) {
173
169
  const ancestors = gitLines(await gitTry(wt, "rev-list", "--first-parent", `--max-count=${INHERIT_WALK_MAX}`, `${sha}^`));
174
170
  for (const ancestor of ancestors) {
@@ -191,14 +187,13 @@ function exemptPathsOf(config) {
191
187
  const patterns = config.review?.exemptPaths;
192
188
  return Array.isArray(patterns) ? patterns.filter((p) => typeof p === "string" && p !== "") : [];
193
189
  }
194
- /** The cached verdict for `sha` WITHOUT running anything — "green", "red", or undefined when
195
- * this process holds none. The redeployer's urgency carve-out consults it first (BUGS.md
196
- * 2026-09-30): a verdict already in hand answers the RUNNING build's redness for free — the
197
- * warm path, a red observed here while that SHA was still main's tip. Undefined — a cold
198
- * cache, e.g. a fresh orchestrator that inherited a stale build — sends the caller to run the
199
- * check itself (redeploy.ts's witness worktree at the build SHA), which lands in this same
200
- * cache and answers every later consult. Read-only: deciding a SHA's verdict is
201
- * checkMainBaseline's alone. */
190
+ /** The cached verdict for `sha` WITHOUT running anything — "green", "red", or undefined when this
191
+ * process holds none. The redeployer's urgency carve-out consults it first (BUGS.md 2026-09-30): a
192
+ * verdict already in hand answers the RUNNING build's redness for free — the warm path, a red
193
+ * observed here while that SHA was still main's tip. Undefined — a cold cache, e.g. a fresh
194
+ * orchestrator that inherited a stale build — sends the caller to run the check itself
195
+ * (redeploy.ts's witness worktree at the build SHA), which is recorded in this same cache and
196
+ * answers every later consult. Read-only: deciding a SHA's verdict is checkMainBaseline's alone. */
202
197
  export function cachedBaselineVerdict(sha) {
203
198
  return baselineCache.get(sha)?.status;
204
199
  }
@@ -219,8 +214,8 @@ onRun,
219
214
  /** Ignore a cached RED verdict and run the suite here instead (a cached green still short-
220
215
  * circuits — see baselineCache). For the caller whose false block is expensive enough to pay
221
216
  * one extra run: the redeploy gate, where trusting another worktree's environmental red
222
- * strands the whole fleet on a stale build until main moves. A green from the re-run promotes
223
- * the SHA fleet-wide, which unblocks the role loops too. */
217
+ * strands the whole team on a stale build until main moves. A green from the re-run promotes
218
+ * the SHA team-wide, which unblocks the role loops too. */
224
219
  reverifyRed = false,
225
220
  /** Ignore the cache entirely and run the suite here, even over a GREEN verdict. For the one
226
221
  * caller that cannot trust a green (BUGS.md 2026-10-06: the review gate re-attributing a
@@ -273,19 +268,11 @@ observedOnly = false) {
273
268
  // withCheckPermit): this runs the suite directly, so it must count against the cap too.
274
269
  // Only the one run per key holds a permit; deduped waiters above await `pending` holding
275
270
  // none.
276
- const { outcome, durationMs } = await withCheckPermit(config, CHECK_TIER.other, async () => {
277
- const startedAt = Date.now();
278
- const first = await runBuildCheck(wt, check, undefined, undefined, sampleSleep);
279
- if (!owesCleanRetry(first))
280
- return { outcome: first, durationMs: Date.now() - startedAt };
281
- // Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the tree:
282
- // the first attempt is priced as its own run, and the retry's outcome stands — the same
283
- // rule runScopedBuildCheck applies at every other scope.
284
- onRun?.({ outcome: first, durationMs: Date.now() - startedAt });
285
- const retryStart = Date.now();
286
- const retry = await runBuildCheck(wt, check, undefined, undefined, sampleSleep);
287
- return { outcome: retry, durationMs: Date.now() - retryStart };
288
- });
271
+ const { outcome, durationMs } = await withCheckPermit(config, CHECK_TIER.other, () => runWithCleanRetry(() => runBuildCheck(wt, check, undefined, undefined, sampleSleep),
272
+ // Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the
273
+ // tree: the first attempt is priced as its own run, and the retry's outcome stands —
274
+ // the same rule runScopedBuildCheck applies at every other scope.
275
+ (first, dur) => onRun?.({ outcome: first, durationMs: dur })));
289
276
  onRun?.({ outcome, durationMs });
290
277
  if (outcome.status === "skipped") {
291
278
  // Environmental (timeout/no-npm), after the clean retry above for weather shapes:
@@ -295,7 +282,7 @@ observedOnly = false) {
295
282
  if (outcome.status === "failed" && (outcome.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS) {
296
283
  // A failure the host slept through made no verdict about the tree either (BUGS.md
297
284
  // 2026-09-30): the sleep expired a test's own wall-clock wait at the wake and the suite
298
- // exited 1 inside the deadline. Caching it red would make sleep weather the fleet's
285
+ // exited 1 inside the deadline. Caching it red would make sleep weather the team's
299
286
  // authoritative red main, so nothing is cached — the next consult re-runs, and a clean
300
287
  // verdict then settles the SHA exactly as before.
301
288
  return { baseline: null, run: outcome.run, unverified: true };
@@ -311,9 +298,9 @@ observedOnly = false) {
311
298
  outputTail: outcome.outputTail,
312
299
  redFrom: priorRedFrom.includes(wt) ? priorRedFrom : [...priorRedFrom, wt],
313
300
  };
314
- // A green always lands (promoting a provisional red); a red never overwrites an observed
315
- // green — a concurrent run may have promoted this SHA while this one was still going. An
316
- // inherited green is no observation, so this run's verdict replaces it either way.
301
+ // A green is always recorded (promoting a provisional red); a red never overwrites an
302
+ // observed green — a concurrent run may have promoted this SHA while this one was still
303
+ // going. An inherited green is no observation, so this run's verdict replaces it either way.
317
304
  const current = baselineCache.get(sha);
318
305
  if (current?.status !== "green" || current.inheritedFrom !== undefined)
319
306
  rememberBaseline(sha, baseline);
@@ -329,19 +316,19 @@ observedOnly = false) {
329
316
  baselineInFlight.delete(key);
330
317
  }
331
318
  }
332
- /** Is main green at the mirror worktree's HEAD? The landing path seeds a green verdict for
333
- * every merged code SHA (noteGreenBaseline — its in-lock post-rebase re-check, or the gate's
334
- * pre-check when the rebase was a no-op), so the common case is a cache hit. A head that only
335
- * added doc commits on top of such a SHA inherits its green (checkMainBaseline's
336
- * inheritedGreen). The redeployer's cooldown pre-warm asks this for every new head, and those doc
337
- * heads were most of the harness's baseline runs (247 of 319 over 2026-10-02..12). Otherwise the
338
- * project's declared check runs once in the mirror. No declared check or an
339
- * environmental skip reads as green — the gates' warn-and-proceed policy.
319
+ /** Is main green at the mirror worktree's HEAD? The merge path seeds a green verdict for every
320
+ * merged code SHA (noteGreenBaseline — its in-lock post-rebase re-check, or the gate's pre-check
321
+ * when the rebase was a no-op), so the common case is a cache hit. A head that only added doc
322
+ * commits on top of such a SHA inherits its green (checkMainBaseline's inheritedGreen). The
323
+ * redeployer's cooldown pre-warm asks this for every new head, and those doc heads were most of the
324
+ * harness's baseline runs (247 of 319 over 2026-10-02..12). Otherwise the project's declared check
325
+ * runs once in the mirror. No declared check or an environmental skip reads as green — the gates'
326
+ * warn-and-proceed policy.
340
327
  *
341
328
  * A cached RED, though, is re-verified here (checkMainBaseline's `reverifyRed`) instead of
342
329
  * being taken as given. A red verdict can belong to the worktree that produced it rather than
343
330
  * to the tree, and the redeploy gate that asks this is the one place where believing a wrong
344
- * red is expensive: it strands the whole fleet on a stale build with no retry until main
331
+ * red is expensive: it strands the whole team on a stale build with no retry until main
345
332
  * moves. One extra suite run per red head buys that, and a green from it promotes the SHA for
346
333
  * every other gate too. */
347
334
  export async function mainIsGreen(mirrorWt,
@@ -349,7 +336,7 @@ export async function mainIsGreen(mirrorWt,
349
336
  * §6/7), so a configured command makes the green check run on a non-npm repo too; its
350
337
  * maxConcurrentChecks sizes the check permit this run takes. */
351
338
  config,
352
- /** Hook for the build_check event — this run is a minute of the fleet's time and belongs in
339
+ /** Hook for the build_check event — this run is a minute of the team's time and belongs in
353
340
  * the feed like the role loops' own baseline checks. */
354
341
  onRun) {
355
342
  const check = await checkMainBaseline(mirrorWt, config, onRun, true);
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import { baselineBlocked } from "../roles/roles.js";
2
3
  import { isCustomRole, liveConfig } from "../config/config.js";
3
4
  import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
@@ -5,7 +6,7 @@ import { failureHeadline } from "../build/build-check-report.js";
5
6
  import { baselineCheckEventLogger, buildCheckSkipWarning, sleptPhrase } from "../build/build-check-events.js";
6
7
  import { checkMainBaseline } from "./main-baseline.js";
7
8
  import { buildMainRedNote } from "../prompt/gate-prompts.js";
8
- import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
9
+ import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
9
10
  import { errorMessage } from "../text/text.js";
10
11
  import { mainRedPhrase } from "../text/phrases.js";
11
12
  import { branchHead } from "../git/git.js";
@@ -18,14 +19,15 @@ import { ensureDetachedWorktree } from "../git/worktree.js";
18
19
  * and "Tests red on main" entries), so while a SHA is known red the blocked roles skip authoring
19
20
  * instead of burning runs that are guaranteed to fail. Split out of loop.ts — which keeps the
20
21
  * tick lifecycle around it — because this is a self-contained policy with its own per-process
21
- * state (one harness-level warning per newly-discovered red SHA) and its own event contract; what
22
- * it borrows from the loop is identity and the role's worktree, and it consumes
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
24
+ * consumes
23
25
  * build/build-check.ts's checkMainBaseline machinery (detection + execution + per-SHA cache),
24
26
  * which stays unaware of ticks. */
25
- /** The last main SHA for which this process logged a red-main warning (the baseline check):
26
- * one harness-level warning per newly-discovered red SHA, not one per blocked role's tick —
27
- * module-level so every runner in the fleet shares it. A restart re-logs once: the cache is
28
- * cold then too, and an operator restarting into a still-red main should see why nothing lands. */
27
+ /** 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 —
29
+ * module-level so every runner in the team shares it. A restart re-logs once: the cache is cold
30
+ * then too, and an operator restarting into a still-red main should see why nothing merges. */
29
31
  let lastMainRedSha = null;
30
32
  /** The red-main message's shared spine — the SHA, script, and failure headline a red baseline
31
33
  * carries, phrased once so the once-per-SHA warning and the main_red outcome cannot drift apart
@@ -34,26 +36,32 @@ function redMainMessage(red, action) {
34
36
  const firstLine = failureHeadline(red.outputTail);
35
37
  return `${mainRedPhrase(red.sha)} (${red.script}${firstLine ? `: ${firstLine}` : ""}) — ${action}`;
36
38
  }
37
- /** Log the fleet-wide red-main warning for `red`'s SHA at most once per process. Module-level
38
- * (with lastMainRedSha) so the gate and the bugfix handoff share one guard: whichever observes
39
- * the red first logs it, the other stays silent. */
39
+ /** 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. */
40
42
  function warnMainRedOnce(root, red) {
41
43
  if (lastMainRedSha === red.sha)
42
44
  return;
43
45
  lastMainRedSha = red.sha;
44
- warnEventBestEffort(root, "harness", redMainMessage(red, "code merges blocked until main is green"));
46
+ logEventBestEffort(root, {
47
+ source: TUMWATER_SOURCE,
48
+ type: "main_red_hold",
49
+ sha: red.sha,
50
+ script: red.script,
51
+ message: redMainMessage(red, "code merges blocked until main is green"),
52
+ });
45
53
  }
46
54
  /** checkMainBaseline's per-run hook: log the one run per SHA (cache misses only) under the role
47
55
  * that paid for it, with its duration — the gate's build_check sibling, so both halves of the
48
- * fleet's deterministic verification are priced in the feed. The one logger for every baseline
56
+ * team's deterministic verification are priced in the feed. The one logger for every baseline
49
57
  * check this module runs — the bugfix handoff, the authoring gate, and mainTipVerdict's tip
50
- * check (which review.ts's gate and landing-check-failures.ts's attributeRedCheck reach
58
+ * check (which review.ts's gate and merge-check-failures.ts's attributeRedCheck reach
51
59
  * through it) — so the event's shape cannot drift between them. */
52
60
  function baselineCheckLogger(root, role) {
53
61
  return baselineCheckEventLogger(role, (event) => logEventBestEffort(root, event));
54
62
  }
55
63
  /** Red-main handoff for the `bugfix` healer: mainRedGate exempts
56
- * bugfix so the only role that can unblock the fleet may author, but its prompt then starts from
64
+ * bugfix so the only role that can unblock the team may author, but its prompt then starts from
57
65
  * BUGS.md and knows nothing about the red suite. This runs the same per-SHA baseline check
58
66
  * (cache and provisional-red re-verification reused unchanged, so it costs at most the one run
59
67
  * the gate would have paid for) and, when main is red, returns the note that points the healer
@@ -68,7 +76,7 @@ export async function bugfixMainRedNote(root, role, wt) {
68
76
  if (red?.status !== "red")
69
77
  return undefined;
70
78
  warnMainRedOnce(root, red);
71
- return buildMainRedNote(red.sha, red.script, failureHeadline(red.outputTail));
79
+ return buildMainRedNote(red.sha, red.script, failureHeadline(red.outputTail), red.outputTail);
72
80
  }
73
81
  /** Gate one fresh tick on main's baseline. Returns null when authoring may proceed — role not
74
82
  * blocked, no declared check, green, or an environmental skip (warned under the role and
@@ -104,7 +112,7 @@ sampleSleep) {
104
112
  return {
105
113
  result: "main_red",
106
114
  summary: "code merges blocked until main is green",
107
- // The cause rides the outcome so the tick's `tick_end` carries it
115
+ // The cause rides the outcome so the tick's `run_end` carries it
108
116
  // (tick-finalize.ts folds it into state.lastError before logging): the digest itemizes
109
117
  // the outcome that blocks every merge, instead of leaving it a bare count behind a
110
118
  // once-per-SHA warning (BUGS.md 2026-09-28).
@@ -113,25 +121,25 @@ sampleSleep) {
113
121
  }
114
122
  return null;
115
123
  }
116
- /** Serializes mainTipVerdict's use of its one worktree: the landing pipeline's vets run their
124
+ /** Serializes mainTipVerdict's use of its one worktree: the merge pipeline's verifies run their
117
125
  * gates concurrently, and a second gate re-pointing the checkout at a newer main while the first's
118
- * check ran in it would measure a tree that is neither. Each link is bounded — a few git
119
- * commands plus at most one check run, which runBuildCheck kills at its timeout — and takes no
120
- * other lock, so a waiter waits at most for the gates queued ahead of it. */
126
+ * check ran in it would measure a tree that is neither. Each link is bounded — a few git commands
127
+ * plus at most one check run, which runBuildCheck kills at its timeout — and takes no other lock,
128
+ * so a waiter waits at most for the gates queued ahead of it. */
121
129
  let gateMainQueue = Promise.resolve();
122
130
  /** Main's baseline verdict at its current tip, for attributing a gate check that failed twice
123
131
  * (src/review/review.ts): gateMainWorktreePath is re-pointed at `mainBranch`'s tip and asked
124
- * through checkMainBaseline — the same per-SHA, fleet-wide cache mainRedGate reads, which every
125
- * landing seeds green for the SHA it moved main to, so the common case is a cache hit. A miss
126
- * (or a provisional red from another worktree, or a green that was only inherited across doc
127
- * commits) runs the declared check once here, bounded by its timeout. A red is warned fleet-wide once per SHA, exactly as mainRedGate warns it. Never
128
- * throws: an unreadable main, a checkout that fails, no declared check on main, or a skipped
129
- * run all read as `unavailable`.
132
+ * through checkMainBaseline — the same per-SHA, team-wide cache mainRedGate reads, which every
133
+ * merge seeds green for the SHA it moved main to, so the common case is a cache hit. A miss (or a
134
+ * provisional red from another worktree, or a green that was only inherited across doc commits)
135
+ * runs the declared check once here, bounded by its timeout. A red is warned team-wide once per
136
+ * SHA, exactly as mainRedGate warns it. Never throws: an unreadable main, a checkout that fails, no
137
+ * declared check on main, or a skipped run all read as `unavailable`.
130
138
  *
131
- * `forceFresh` bypasses that per-SHA cache and runs main's declared check now, even over a
132
- * cached green (checkMainBaseline's forceFresh): the review gate needs it to re-attribute a
133
- * repeated gate failure on a clock- or load-sensitive test whose cached green has gone stale
134
- * (BUGS.md 2026-10-06). The landing pipeline's attribution reads the cached verdict instead. */
139
+ * `forceFresh` bypasses that per-SHA cache and runs main's declared check now, even over a cached
140
+ * green (checkMainBaseline's forceFresh): the review gate needs it to re-attribute a repeated gate
141
+ * failure on a clock- or load-sensitive test whose cached green has gone stale (BUGS.md
142
+ * 2026-10-06). The merge pipeline's attribution reads the cached verdict instead. */
135
143
  export function mainTipVerdict(root, role, mainBranch, config, forceFresh = false) {
136
144
  const run = gateMainQueue.then(() => verdictAtMainTip(root, role, mainBranch, config, forceFresh));
137
145
  gateMainQueue = run.catch(() => undefined);
@@ -0,0 +1,43 @@
1
+ // vocabulary: plain
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { INITIAL_PROMPT_MAX_CHARS, PROMPT_END, PROMPT_START, briefPath, replaceInitialPrompt } from "./brief.js";
5
+ import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
6
+ import { withStateLock } from "../concurrency/lock.js";
7
+ import { writeTextAtomic } from "../files/files.js";
8
+ import { stampedSubject } from "../git/commit-message.js";
9
+ import { primaryWorktreeLockPath } from "../paths.js";
10
+ import { PREFILL_REASON } from "../text/phrases.js";
11
+ import { tooLongMessage } from "../text/text.js";
12
+ 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
15
+ * commit that one file on main. The one writer behind `tumwater brief --file` and the
16
+ * dashboard's Settings card, so the two cannot drift. */
17
+ export async function editInitialPrompt(root, text) {
18
+ const prompt = text.trim();
19
+ if (prompt === "")
20
+ 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) };
23
+ if (prompt.includes(PROMPT_START) || prompt.includes(PROMPT_END))
24
+ return { ok: false, error: "the new brief contains a tumwater:prompt marker, which would break the block for every reader" };
25
+ const file = briefPath(root);
26
+ if (file === null)
27
+ return { ok: false, error: NO_BRIEF };
28
+ const name = path.basename(file);
29
+ const changed = withStateLock(primaryWorktreeLockPath(root), () => {
30
+ const current = fs.readFileSync(file, "utf8");
31
+ const next = replaceInitialPrompt(current, prompt);
32
+ if (next === null)
33
+ return `${name} no longer carries a prompt block`;
34
+ if (next === current)
35
+ return false;
36
+ writeTextAtomic(file, next);
37
+ return true;
38
+ });
39
+ if (typeof changed === "string")
40
+ return { ok: false, error: changed };
41
+ const committed = changed && (await commitOperatorBacklogEdit(root, name, stampedSubject("operator", "Edit brief"))); // vocabulary-ok: commit prefix stays
42
+ return { ok: true, file: name, changed, committed };
43
+ }
@@ -1,7 +1,8 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
- import { briefCandidates } from "./paths.js";
4
- import { errorMessage, truncateWithNote } from "./text/text.js";
4
+ import { briefCandidates } from "../paths.js";
5
+ import { errorMessage, truncateWithNote } from "../text/text.js";
5
6
  /** The managed sections of the project brief (TUMWATER.md, with README.md as the
6
7
  * compatibility path — plans/portability.md §7a/7): marker constants, the templates a fresh
7
8
  * repo gets, and reading the project's initial prompt back out. */
@@ -9,7 +10,7 @@ export const PROMPT_START = "<!-- tumwater:prompt:start -->";
9
10
  export const PROMPT_END = "<!-- tumwater:prompt:end -->";
10
11
  /** Cap on the initial prompt (see readInitialPrompt). The prompt is the project's reason to
11
12
  * exist and rides into EVERY tick and director prompt, so unbounded text would be a standing
12
- * per-tick prefill cost — the same budget customLoops.task and roles.<id>.instructions are
13
+ * per-tick prefill cost — the same budget customRoles.task and roles.<id>.instructions are
13
14
  * held to. `tumwater init` rejects an over-long prompt before it is committed (init.ts); this
14
15
  * constant is the shared bound so the read path and the init check cannot drift. */
15
16
  export const INITIAL_PROMPT_MAX_CHARS = 4096;
@@ -18,6 +19,11 @@ export const INITIAL_PROMPT_MAX_CHARS = 4096;
18
19
  // prompt markers above stay exported because init.ts names them in its error message.
19
20
  export const STATUS_START = "<!-- tumwater:status:start -->";
20
21
  export const STATUS_END = "<!-- tumwater:status:end -->";
22
+ /** The placeholder every fresh template seeds between the status markers. A brief still
23
+ * carrying exactly this body (or an empty one) is "description incomplete"; once the readme
24
+ * role replaces it with a real status the tight-loop rule turns itself off. Hoisted so the
25
+ * template and briefStatusIncomplete cannot drift on the wording. */
26
+ export const EMPTY_STATUS = "_No status yet. The readme role keeps this section up to date._";
21
27
  /** The two managed sections a fresh brief carries — the initial prompt in its prompt
22
28
  * markers, an empty status in its status markers — under the given H1. Both homes render
23
29
  * the identical body (README.md for a fresh repo, TUMWATER.md for an adopted one), so the
@@ -26,7 +32,7 @@ export const STATUS_END = "<!-- tumwater:status:end -->";
26
32
  function managedSections(heading, initialPrompt) {
27
33
  return `# ${heading}
28
34
 
29
- ## Initial prompt
35
+ ## Brief
30
36
 
31
37
  ${PROMPT_START}
32
38
  ${initialPrompt.trim()}
@@ -35,7 +41,7 @@ ${PROMPT_END}
35
41
  ## Status
36
42
 
37
43
  ${STATUS_START}
38
- _No status yet. The readme loop keeps this section up to date._
44
+ ${EMPTY_STATUS}
39
45
  ${STATUS_END}
40
46
  `;
41
47
  }
@@ -67,7 +73,7 @@ function ownsBrief(text) {
67
73
  * A candidate that exists but cannot be read (README.md created as a directory, a
68
74
  * permission-lost file) is an operator-visible break, not a silent skip: the brief is
69
75
  * every loop's reason to exist, read each tick, so falling through to the next candidate
70
- * could run the fleet on the wrong brief and throwing raw would crash every tick with an
76
+ * could run the team on the wrong brief and throwing raw would crash every tick with an
71
77
  * errno that names neither the file nor the fix — the message below names both. */
72
78
  function firstBriefText(root, accepts) {
73
79
  for (const candidate of briefCandidates(root)) {
@@ -78,7 +84,7 @@ function firstBriefText(root, accepts) {
78
84
  text = fs.readFileSync(candidate, "utf8");
79
85
  }
80
86
  catch (err) {
81
- throw new Error(`cannot read the project brief ${JSON.stringify(path.basename(candidate))}: ${errorMessage(err)} — every loop reads this file's prompt each tick, so make it a readable file carrying the ${PROMPT_START}/${PROMPT_END} markers (or move it aside)`);
87
+ throw new Error(`cannot read the project brief ${JSON.stringify(path.basename(candidate))}: ${errorMessage(err)} — every role reads this file's brief on each run, so make it a readable file carrying the ${PROMPT_START}/${PROMPT_END} markers (or move it aside)`);
82
88
  }
83
89
  if (accepts(text))
84
90
  return { file: path.basename(candidate), text };
@@ -92,6 +98,25 @@ function firstBriefText(root, accepts) {
92
98
  export function briefFile(root) {
93
99
  return readBrief(root)?.file ?? null;
94
100
  }
101
+ /** False when there is no resolved brief, or the resolved brief carries no well-formed
102
+ * STATUS_START/STATUS_END pair (a malformed brief is doctor's warning, not a tight loop);
103
+ * otherwise true when the status body between the markers is empty or still the EMPTY_STATUS
104
+ * placeholder. The one machine-detectable "description not yet complete" signal: the initial
105
+ * prompt half of the description is always present (init rejects an empty one). One small
106
+ * single-file read, like readInitialPrompt. */
107
+ export function briefStatusIncomplete(root) {
108
+ const found = firstBriefText(root, ownsBrief);
109
+ if (!found)
110
+ return false;
111
+ const bodyStart = found.text.indexOf(STATUS_START);
112
+ if (bodyStart < 0)
113
+ return false;
114
+ const bodyEnd = found.text.indexOf(STATUS_END, bodyStart + STATUS_START.length);
115
+ if (bodyEnd < 0)
116
+ return false;
117
+ const body = found.text.slice(bodyStart + STATUS_START.length, bodyEnd).trim();
118
+ return body === "" || body === EMPTY_STATUS;
119
+ }
95
120
  /** The project's initial prompt, extracted from the resolved brief file's managed section —
96
121
  * TUMWATER.md when it exists with markers, else README.md (plans/portability.md §7a/7). The
97
122
  * closing marker is searched for only AFTER the opening one: an end marker mentioned in prose
@@ -101,19 +126,45 @@ export function briefFile(root) {
101
126
  export function readInitialPrompt(root) {
102
127
  return readBrief(root)?.prompt ?? "";
103
128
  }
104
- function extractPrompt(text) {
129
+ /** The full path of the file that owns the prompt block (the same candidate readBrief
130
+ * resolves), or null when none does — so a writer edits the file every reader reads. */
131
+ export function briefPath(root) {
132
+ const found = firstBriefText(root, ownsBrief);
133
+ return found ? path.join(root, found.file) : null;
134
+ }
135
+ /** `text` with the prompt block's content replaced by `prompt` (trimmed), every byte outside
136
+ * the two markers kept; null when the text has no well-formed block. Finds the block by
137
+ * extractPrompt's rule (first start marker, then the first end marker after it). */
138
+ export function replaceInitialPrompt(text, prompt) {
139
+ const start = text.indexOf(PROMPT_START);
140
+ if (start < 0)
141
+ return null;
142
+ const bodyStart = start + PROMPT_START.length;
143
+ const end = text.indexOf(PROMPT_END, bodyStart);
144
+ if (end < 0)
145
+ return null;
146
+ return `${text.slice(0, bodyStart)}\n${prompt.trim()}\n${text.slice(end)}`;
147
+ }
148
+ /** The trimmed, untruncated text between the first start marker and the first end marker after
149
+ * it; null when `text` has no well-formed block. */
150
+ export function promptBlockOf(text) {
105
151
  const start = text.indexOf(PROMPT_START);
106
152
  if (start < 0)
107
153
  return null;
108
154
  const end = text.indexOf(PROMPT_END, start + PROMPT_START.length);
109
155
  if (end < 0)
110
156
  return null;
111
- const prompt = text.slice(start + PROMPT_START.length, end).trim();
157
+ return text.slice(start + PROMPT_START.length, end).trim();
158
+ }
159
+ function extractPrompt(text) {
160
+ const prompt = promptBlockOf(text);
161
+ if (prompt === null)
162
+ return null;
112
163
  // A hand-edited brief can carry an over-long prompt past init's length check. Truncate it
113
164
  // for every tick instead of injecting the whole thing (the same defensive cap readPrinciples
114
165
  // applies, through text.ts's truncateWithNote), with a visible note so the loss is not
115
166
  // silent. init rejects the normal path before it is committed, so this is the backstop.
116
- return truncateWithNote(prompt, INITIAL_PROMPT_MAX_CHARS, "initial prompt");
167
+ return truncateWithNote(prompt, INITIAL_PROMPT_MAX_CHARS, "brief");
117
168
  }
118
169
  /** The resolved brief as one value: the owning file's basename (briefFile) and its extracted
119
170
  * initial prompt (readInitialPrompt) from a SINGLE firstBriefText scan. Tick prompt assembly
@@ -125,3 +176,8 @@ export function readBrief(root) {
125
176
  const found = firstBriefText(root, ownsBrief);
126
177
  return found ? { file: found.file, prompt: extractPrompt(found.text) ?? "" } : null;
127
178
  }
179
+ /** The brief's plain-text form: its file name, then the initial prompt — what `tumwater brief`
180
+ * prints and the TUI's Brief tab shows, so the two cannot drift. */
181
+ export function renderBrief(brief) {
182
+ return `${brief.file}\n${brief.prompt}`;
183
+ }