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,37 +1,35 @@
1
1
  import { BUILD_CHECK_TIMEOUT_MS, detectBuildCheck, gateCommandOf, } from "./build-check-detect.js";
2
2
  import { runBuildCheck, checkScriptName, checkTimeoutMs, } from "./build-check.js";
3
3
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
- import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning, owesCleanRetry, installFailedPhrase, killedPhrase, loadedHostPhrase, timedOutPhrase, DEADLINE_LATE_TOLERANCE_MS, SLEEP_SPAN_TOLERANCE_MS, sleptPhrase, starvedDeadlineMs, UNVERIFIED_TREE, } from "./build-check-events.js";
4
+ import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning, runWithCleanRetry, installFailedPhrase, killedPhrase, loadedHostPhrase, timedOutPhrase, DEADLINE_LATE_TOLERANCE_MS, SLEEP_SPAN_TOLERANCE_MS, sleptPhrase, starvedDeadlineMs, UNVERIFIED_TREE, } from "./build-check-events.js";
5
5
  import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
6
6
  import { sampleHostLoad, sampleSleepClock } from "./host-sleep.js";
7
7
  import { npmInstall } from "./dep-install.js";
8
- /** Run the project's declared check for a named scope — the detect → run → build_check
9
- * event → skip-warning sequence the review gate's pre-check (scope "gate"), the landing
10
- * path's in-lock re-check (scope "landing"), and the batch lander's one check over the whole
11
- * stacked tree (scope "batch") previously each ran inline, kept in one place so
12
- * the event's shape and the skip warning cannot drift between them. Split out of
13
- * build-check.ts — which keeps the check's execution and outcome classification
14
- * (runBuildCheck, BuildCheckOutcome) — because orchestration is its own concern: the permit,
15
- * the event emission, the verdict-less retry, the merge-scope unverified remapping, and the
16
- * skip warning. Every run is an event with its cost: the deterministic checks are where the
17
- * fleet's compute goes after the authoring run, and "how long does npm test take per merge"
18
- * must be answerable from the feed, not by timing it by hand. Returns null when no check is
8
+ /** Run the project's declared check for a named scope — the detect → run → build_check event →
9
+ * skip-warning sequence the review gate's pre-check (scope "gate"), the merge path's in-lock
10
+ * re-check (scope "merge"), and the batch merger's one check over the whole stacked tree (scope
11
+ * "batch") previously each ran inline, kept in one place so the event's shape and the skip warning
12
+ * cannot drift between them. Split out of build-check.ts — which keeps the check's execution and
13
+ * outcome classification (runBuildCheck, BuildCheckOutcome) — because orchestration is its own
14
+ * concern: the permit, the event emission, the verdict-less retry, the merge-scope unverified
15
+ * remapping, and the skip warning. Every run is an event with its cost: the deterministic checks
16
+ * are where the team's compute goes after the authoring run, and "how long does npm test take per
17
+ * merge" must be answerable from the feed, not by timing it by hand. Returns null when no check is
19
18
  * declared (nothing to run — the caller passes, exactly as before this split), otherwise the
20
- * declared check plus its classified outcome. A "skipped" outcome also logs its standard
21
- * warning here (wording keyed on the scope, the timeout as actually set — and, when its
22
- * deadline fired late, as actually enforced); "failed" and "passed" are the caller's to
23
- * decide (deterministic reject vs. verifiedHead / baseline seeding). A timeout or signal kill
24
- * at a merge scope is remapped to a deterministic "failed" — the tree is unverified, so it
25
- * must not land. A check killed by a signal the harness did not send is retried once at any
26
- * scope — the first run's death says nothing about the tree (it is another run's `pkill`), so
27
- * one verdict from a clean attempt is owed before the skip is honored; a timeout whose deadline
28
- * demonstrably fired late (the harness's own host-sleep evidence) earns that same one retry at
29
- * any scope, and a failed run the host slept through does too; each attempt is priced
30
- * as its own build_check event, carrying when the check itself ran (buildCheckRunFields).
31
- * A configured `check.gateCommand` replaces the command at scope "gate" only (same cwd and
32
- * timeout); the landing and batch scopes — and the red-main baseline, which detects on its
33
- * own — keep running check.command. `permitWait` hears about a wait for the check permit (the
34
- * landing cell's "waiting for a check slot"). Never throws. */
19
+ * declared check plus its classified outcome. A "skipped" outcome also logs its standard warning
20
+ * here (wording keyed on the scope, the timeout as actually set — and, when its deadline fired
21
+ * late, as actually enforced); "failed" and "passed" are the caller's to decide (deterministic
22
+ * reject vs. verifiedHead / baseline seeding). A timeout or signal kill at a merge scope is
23
+ * remapped to a deterministic "failed" — the tree is unverified, so it must not merge. A check
24
+ * killed by a signal the harness did not send is retried once at any scope — the first run's death
25
+ * says nothing about the tree (it is another run's `pkill`), so one verdict from a clean attempt is
26
+ * owed before the skip is honored; a timeout whose deadline demonstrably fired late (the harness's
27
+ * own host-sleep evidence) earns that same one retry at any scope, and a failed run the host slept
28
+ * through does too; each attempt is priced as its own build_check event, carrying when the check
29
+ * itself ran (buildCheckRunFields). A configured `check.gateCommand` replaces the command at scope
30
+ * "gate" only (same cwd and timeout); the merge and batch scopes — and the red-main baseline, which
31
+ * detects on its own — keep running check.command. `permitWait` hears about a wait for the check
32
+ * permit (the merge cell's "waiting for a check slot"). Never throws. */
35
33
  export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
36
34
  const gateCommand = scope === "gate" ? gateCommandOf(config) : undefined;
37
35
  const check = detectBuildCheck(wt, gateCommand === undefined ? config : { check: { ...config?.check, command: gateCommand } });
@@ -41,7 +39,7 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
41
39
  // caller's bound. Effective here so the reason text and the skip warning agree with what
42
40
  // runBuildCheck enforced.
43
41
  // A merge scope under a starved host gets a longer deadline (starvedDeadlineMs): the check
44
- // runs as long as the fleet's load stretches it, and a kill at the unstretched bound would
42
+ // runs as long as the team's load stretches it, and a kill at the unstretched bound would
45
43
  // only measure the load (BUGS.md 2026-10-12). The load sampled here is also what the
46
44
  // unverified reason names.
47
45
  const load = sampleLoad();
@@ -53,42 +51,33 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
53
51
  // holds); durationMs prices the run itself, not the wait for a permit.
54
52
  let durationMs = 0;
55
53
  const raw = await withCheckPermit(config, MERGE_SCOPES.has(scope) ? CHECK_TIER.merge : CHECK_TIER.other, async () => {
56
- const startedAt = Date.now();
57
- const first = await runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install);
58
- durationMs = Date.now() - startedAt;
59
- // A check the harness did not stop itself says nothing about the tree — its death is
60
- // another run's doing. A timeout whose deadline demonstrably fired late is the same
61
- // weather: the harness's own evidence (run.deadlineLateMs, the measurement BUGS.md
62
- // 2026-09-21 added) says the deadline passed while the host slept or the harness
63
- // stalled, so the check ran seconds and was killed at a wake — it made no verdict about
64
- // the tree and was not a slow suite. At every scope it owes one retry from a clean
65
- // attempt; the second attempt's outcome stands. The verdict-less first attempt is priced
66
- // as its own event (the feed must answer how long a check took), then the final event
67
- // below records the retry's classified outcome.
68
- if (!owesCleanRetry(first))
69
- return first;
70
- logEventBestEffort(root, buildCheckEvent(role, scope, first, durationMs));
71
- const retryStart = Date.now();
72
- const retry = await runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install);
73
- durationMs = Date.now() - retryStart;
74
- return retry;
54
+ // One permit covers both attempts (the killed-check retry re-runs under the permit it
55
+ // holds); durationMs prices the run itself, not the wait for a permit. A check the
56
+ // harness did not stop itself says nothing about the tree; a timeout whose deadline
57
+ // demonstrably fired late, and a failed run the host slept through, are the same weather
58
+ // — at every scope each owes one retry from a clean attempt, and the second attempt's
59
+ // outcome stands (runWithCleanRetry). The verdict-less first attempt is priced as its own
60
+ // event (the feed must answer how long a check took), then the final event below records
61
+ // the retry's classified outcome.
62
+ const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur)));
63
+ durationMs = d;
64
+ return outcome;
75
65
  }, permitWait);
76
- // A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness,
77
- // a genuinely hung or oversized suite), made no verdict about the tree — and this is the
78
- // check whose whole job is to catch a semantic conflict before it lands, so it rejects
79
- // deterministically — the author keeps its commit and retries. A timeout whose deadline
80
- // fired late has already had its retry above: the lateness is the harness's own evidence the
81
- // host slept, so a second verdict-less attempt is recorded as unverified rather than read
82
- // again as weather (BUGS.md 2026-09-28). no-npm and a broken toolchain still say nothing
83
- // about the tree, and the gate scope still proceeds to the model reviewer, which the landing
84
- // path's own check backs up.
85
- // A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
66
+ // A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
67
+ // genuinely hung or oversized suite), made no verdict about the tree — and this is the check
68
+ // whose whole job is to catch a semantic conflict before it merges, so it rejects
69
+ // deterministically — the author keeps its commit and retries. A timeout whose deadline fired
70
+ // late has already had its retry above: the lateness is the harness's own evidence the host
71
+ // slept, so a second verdict-less attempt is recorded as unverified rather than read again as
72
+ // weather (BUGS.md 2026-09-28). no-npm and a broken toolchain still say nothing about the tree,
73
+ // and the gate scope still proceeds to the model reviewer, which the merge path's own check backs
74
+ // up. A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
86
75
  // attributed to the change, not cached as a red baseline — rather than read as a second
87
- // deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went
88
- // green under the project's own check, even if the host napped through parts of it.
76
+ // deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
77
+ // under the project's own check, even if the host napped through parts of it.
89
78
  const sleepFailed = raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
90
- // A failed dependency install is the same at a merge scope: nothing ran against the tree as
91
- // its lockfile pins it, so it must not land (BUGS.md 2026-10-01).
79
+ // A failed dependency install is the same at a merge scope: nothing ran against the tree as its
80
+ // lockfile pins it, so it must not merge (BUGS.md 2026-10-01).
92
81
  const unverifiedSkip = MERGE_SCOPES.has(scope) &&
93
82
  (((raw.status === "skipped" &&
94
83
  (raw.skipReason === "timeout" || raw.skipReason === "killed" || raw.skipReason === "install")) ||
@@ -101,7 +90,7 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
101
90
  ? sleptPhrase(SCOPE_WORDS[scope].label, raw.run.sleptMs, UNVERIFIED_TREE)
102
91
  : `${SCOPE_WORDS[scope].label} ${timedOutPhrase(effectiveMs, raw.run)}; ${UNVERIFIED_TREE}${
103
92
  // An on-time kill (the late-deadline wording already names its own cause) names the
104
- // starved host when the load average says the fleet was the check's competition.
93
+ // starved host when the load average says the team was the check's competition.
105
94
  (raw.run?.deadlineLateMs ?? 0) > DEADLINE_LATE_TOLERANCE_MS ? "" : loadedHostPhrase(load)}`;
106
95
  const outcome = unverifiedSkip
107
96
  ? {
@@ -41,10 +41,11 @@ const TOOLCHAIN_ERROR_PATTERNS = [
41
41
  function toolchainErrorInOutput(output) {
42
42
  return TOOLCHAIN_ERROR_PATTERNS.some((p) => p.test(output));
43
43
  }
44
- /** The timeout a check actually runs under: `check.timeoutSeconds` when configured (detectBuildCheck
45
- * carries it as timeoutMs on both kinds), else a configured command's built-in 300 s and an npm
46
- * check's caller-given bound. One resolution so the run, the event, and the skip warning cannot
47
- * disagree.
44
+ /** The timeout a check actually runs under: `check.timeoutSeconds` when configured
45
+ * (detectBuildCheck carries it as timeoutMs on both kinds), else a configured command's
46
+ * built-in 300 s and an npm check's caller-given bound. One resolution so the run, the event,
47
+ * and the skip warning
48
+ * cannot disagree.
48
49
  *
49
50
  * checkTimeoutMs and checkScriptName are both shared with build-check-scoped.ts's
50
51
  * runScopedBuildCheck, whose reason text and event must agree with what runBuildCheck
@@ -81,16 +82,16 @@ export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_M
81
82
  if ((await probeToolchain()) === "broken") {
82
83
  return { status: "skipped", script, skipReason: "toolchain" };
83
84
  }
84
- // A tree whose lockfile pins direct dependencies the walk-up does not resolve (a change that
85
- // adds or bumps one — node_modules is gitignored, so the worktree resolves through a root
86
- // install that predates it) is installed first, into the tree itself: without it the check
87
- // fails TS2307 on a correct change and rejects it before any reviewer sees it (BUGS.md
88
- // 2026-10-01). An npm check only — a configured command owns its own environment. A failed
89
- // install made no verdict about the tree; runScopedBuildCheck treats it as unverified at the
90
- // merge scopes, so an uninstallable tree never lands. Never answer a missing dependency by
91
- // skipping instead (classifying a `Cannot find module` failure as environmental): that was
92
- // tried and reverted twice on 2026-10-01 by the user's decision — see BUGS.md's "Do not
93
- // reintroduce a missing-install skip" note for why it switches the gate off fleet-wide.
85
+ // A tree whose lockfile pins direct dependencies the walk-up does not resolve (a change that adds
86
+ // or bumps one — node_modules is gitignored, so the worktree resolves through a root install that
87
+ // predates it) is installed first, into the tree itself: without it the check fails TS2307 on a
88
+ // correct change and rejects it before any reviewer sees it (BUGS.md 2026-10-01). An npm check
89
+ // only — a configured command owns its own environment. A failed install made no verdict about
90
+ // the tree; runScopedBuildCheck treats it as unverified at the merge scopes, so an uninstallable
91
+ // tree never merges. Never answer a missing dependency by skipping instead (classifying a `Cannot
92
+ // find module` failure as environmental): that was tried and reverted twice on 2026-10-01 by the
93
+ // user's decision — see BUGS.md's "Do not reintroduce a missing-install skip" note for why it
94
+ // switches the gate off team-wide.
94
95
  let installed;
95
96
  if (check.kind === "npm") {
96
97
  const r = await syncInstall(wt, install);
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { gitTry } from "../git/git-run.js";
@@ -40,7 +41,7 @@ export async function stampBuild(root, dist, sha) {
40
41
  const info = { sha: head, builtAt: Date.now(), root: path.resolve(root) };
41
42
  // Atomic: a process killed mid-write (a redeploy swap, a timeout-killed `npm test`) would
42
43
  // otherwise leave a torn stamp, and readBuildInfo reads that as no provenance at all —
43
- // createRedeployer then returns null and the fleet never self-redeploys onto newer main,
44
+ // createRedeployer then returns null and the team never self-redeploys onto newer main,
44
45
  // the exact blind spot the stamp exists to close.
45
46
  writeJsonAtomic(buildInfoPath(dist), info);
46
47
  return info;
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { distDir, stampBuild } from "./build-info.js";
@@ -6,7 +7,7 @@ import { resolveFromNodeModules } from "./build-check-detect.js";
6
7
  import { ensureDir, removeTree } from "../files/files.js";
7
8
  import { stagingDir, stagingRootDir } from "../paths.js";
8
9
  import { execFileAsync } from "../process/process.js";
9
- import { warnEvent, warnEventBestEffort } from "../events/events.js";
10
+ import { warnEvent, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
10
11
  import { errorMessage } from "../text/text.js";
11
12
  import { shortSha } from "../text/format.js";
12
13
  /** Producing and swapping the compiled tree behind a self-redeploy (redeploy.ts): compile a
@@ -16,14 +17,14 @@ import { shortSha } from "../text/format.js";
16
17
  * reads without the staging and rename detail. Split out of redeploy.ts for that reason. */
17
18
  /** Hard cap on one compile of the harness; tsc on this codebase takes well under a minute. */
18
19
  const COMPILE_TIMEOUT_MS = 5 * 60_000;
19
- /** How old a superseded staged build may get before a later compile deletes it. A completed
20
- * compile stays consumable for one episode's tail — the drain hold is capped at
21
- * RESTART_DRAIN_MAX_MS (30 min) — so anything older than this grace cannot be swapped by any
22
- * live episode and is pure disk: a fleet whose restarts stay blocked (red main, a failing boot
23
- * gate) recompiles every new head it lands, and without a prune each landed commit left a full
24
- * staged tree behind until the NEXT successful swap cleaned them all up — ~750 MB across one
25
- * busy day (observed 2026-09-30: 128 staged builds, none swapped). A successful swap already
26
- * wipes every staging; this bounds the accumulation between swaps. */
20
+ /** How old a superseded staged build may get before a later compile deletes it. A completed compile
21
+ * stays consumable for one episode's tail — the drain hold is capped at RESTART_DRAIN_MAX_MS (30
22
+ * min) — so anything older than this grace cannot be swapped by any live episode and is pure disk:
23
+ * a team whose restarts stay blocked (red main, a failing boot gate) recompiles every new head it
24
+ * merges, and without a prune each merged commit left a full staged tree behind until the NEXT
25
+ * successful swap cleaned them all up — ~750 MB across one busy day (observed 2026-09-30: 128
26
+ * staged builds, none swapped). A successful swap already wipes every staging; this bounds the
27
+ * accumulation between swaps. */
27
28
  export const STAGED_PRUNE_AFTER_MS = 45 * 60_000;
28
29
  /** Delete staged builds other than `keep` that no live episode can still swap: directories
29
30
  * named for a head (40-hex SHA) whose mtime is older than STAGED_PRUNE_AFTER_MS. Everything
@@ -79,7 +80,7 @@ function rejectedCompile(detail) {
79
80
  * ancestor node_modules, and the mirror lives under <root>/.tumwater/. The compiler itself is
80
81
  * found the same way (resolveFromNodeModules): a checkout that never ran `npm install` — every
81
82
  * tumwater worktree — still has the install of an ancestor to borrow, and demanding a local one
82
- * is what left the fleet unable to rebuild itself on 2026-09-08 (BUGS.md). Never throws. */
83
+ * is what left the team unable to rebuild itself on 2026-09-08 (BUGS.md). Never throws. */
83
84
  export async function compileStaged(root, mirrorWt, mainHead, timeoutMs = COMPILE_TIMEOUT_MS,
84
85
  /** Test seam: the node binary the primary spawn runs tsc under. Production always uses
85
86
  * process.execPath; a test injects a stale path to reproduce an interpreter an upgrade
@@ -109,7 +110,7 @@ execPath = process.execPath) {
109
110
  // 2026-09-29) into a diagnosis instead of a mystery.
110
111
  const missing = missingSpawnInputs(execPath, mirrorWt, tsc);
111
112
  // The one spawn failure a retry can beat: the running build's own node binary vanished
112
- // under it (an upgrade while the fleet stays up leaves process.execPath naming a file that
113
+ // under it (an upgrade while the team stays up leaves process.execPath naming a file that
113
114
  // no longer exists, while every PATH-resolved spawn — git, npm, pi — keeps working). tsc's
114
115
  // own shebang resolves a live node through PATH at spawn time, so run it directly.
115
116
  if (fs.existsSync(execPath) || !fs.existsSync(mirrorWt))
@@ -128,13 +129,13 @@ execPath = process.execPath) {
128
129
  const stamped = await stampBuild(root, staged, mainHead);
129
130
  if (stamped)
130
131
  pruneStaleStagings(root, staged); // Superseded stagings die with the new build's success.
131
- return stamped ? { ok: true, detail: "" } : { ok: false, detail: "could not stamp the compiled build" };
132
+ return stamped ? { ok: true, detail: "" } : { ok: false, detail: "could not stamp the compiled update" };
132
133
  }
133
134
  /** Classify one failed compile spawn into its terminal CompileResult, or null when the error
134
135
  * carries no exit at all — a spawn failure the caller still has to diagnose. Shared by the
135
136
  * primary and shebang-fallback spawns so the two classification ladders cannot drift: a
136
137
  * numeric exit code is the one shape that is a verdict about the tree (tsc ran and chose it),
137
- * so latching anything else as a verdict pins the fleet on the stale build even after the
138
+ * so latching anything else as a verdict pins the team on the stale build even after the
138
139
  * environment recovers (BUGS.md 2026-09-28), and misclassifying a real exit as a rejection
139
140
  * makes the redeployer drop a genuinely failing head and re-attempt it forever instead of
140
141
  * blocking on it (review objection 2026-09-29). A timeout (killed) and a death by signal are
@@ -173,7 +174,7 @@ function missingSpawnInputs(execPath, cwd, tsc) {
173
174
  export function swapDist(root, dist, mainHead) {
174
175
  const staged = stagingDir(root, mainHead);
175
176
  if (!fs.existsSync(staged))
176
- throw new Error(`no staged build for ${shortSha(mainHead)}`);
177
+ throw new Error(`no staged update for ${shortSha(mainHead)}`);
177
178
  const prev = path.join(stagingRootDir(root), "dist.prev");
178
179
  removeTree(prev);
179
180
  const hadDist = fs.existsSync(dist);
@@ -190,21 +191,21 @@ export function swapDist(root, dist, mainHead) {
190
191
  catch (restoreErr) {
191
192
  // A restore failure leaves dist/ MISSING — the operator must hear that plus where
192
193
  // the old build sits, not just the raw ENOENT/ENOSPC of whichever rename lost.
193
- throw new Error(`could not restore the previous dist after a failed build swap for ${shortSha(mainHead)}` +
194
- `: ${errorMessage(err)}; dist is missing and the old build is preserved at ${prev}` +
194
+ throw new Error(`could not restore the previous dist after a failed update swap for ${shortSha(mainHead)}` +
195
+ `: ${errorMessage(err)}; dist is missing and the old update is preserved at ${prev}` +
195
196
  ` (restore failed: ${errorMessage(restoreErr)})`);
196
197
  }
197
198
  }
198
199
  // The raw error alone ("ENOTDIR", "EACCES") never says which move failed or which
199
200
  // staged build was lost: name the head and the dist path the swap was serving.
200
- throw new Error(`could not move the staged build for ${shortSha(mainHead)} into ${dist}: ${errorMessage(err)}`);
201
+ throw new Error(`could not move the staged update for ${shortSha(mainHead)} into ${dist}: ${errorMessage(err)}`);
201
202
  }
202
203
  // The swap is committed: the rename above put the new build at dist/. Everything below is
203
204
  // cleanup of superseded trees — the old build stepped aside at dist.prev, and the stagings
204
205
  // of heads that moved on before their own swap. It is dead weight a later swap or compile
205
206
  // collects, so a removal that fails (a directory surviving removeTree's retries, an
206
207
  // unreadable staging root) must not be reported as a failed swap: the redeployer would
207
- // block the restart while dist/ already holds the new build, leaving the fleet running the
208
+ // block the restart while dist/ already holds the new build, leaving the team running the
208
209
  // old code under a misleading `restart BLOCKED: swapping the new build into place failed`.
209
210
  // Warn and continue, the same best-effort policy pruneStaleStagings applies so pruning
210
211
  // never fails the work it follows.
@@ -244,10 +245,10 @@ export function recoverInterruptedSwap(root, dist) {
244
245
  export function repairInterruptedSwap(root, dist = distDir()) {
245
246
  try {
246
247
  if (recoverInterruptedSwap(root, dist))
247
- warnEventBestEffort(root, "harness", "an interrupted build swap left dist/ missing — restored it from .tumwater/build/dist.prev");
248
+ warnEventBestEffort(root, TUMWATER_SOURCE, "an interrupted update swap left dist/ missing — restored it from .tumwater/build/dist.prev");
248
249
  }
249
250
  catch (err) {
250
- warnEventBestEffort(root, "harness", `dist/ is missing after an interrupted build swap and could not be restored from .tumwater/build/dist.prev: ${errorMessage(err)}`);
251
+ warnEventBestEffort(root, TUMWATER_SOURCE, `dist/ is missing after an interrupted update swap and could not be restored from .tumwater/build/dist.prev: ${errorMessage(err)}`);
251
252
  }
252
253
  }
253
254
  /** Remove one post-swap leftover tree, warning instead of throwing: the swap is already
@@ -257,7 +258,7 @@ function removeSupersededQuiet(root, dir) {
257
258
  removeTree(dir);
258
259
  }
259
260
  catch (err) {
260
- warnCleanup(root, `could not remove superseded build tree ${dir}: ${errorMessage(err)}`);
261
+ warnCleanup(root, `could not remove superseded update tree ${dir}: ${errorMessage(err)}`);
261
262
  }
262
263
  }
263
264
  /** Best-effort sweep of the staging root after a successful swap: every directory left there
@@ -282,7 +283,7 @@ function sweepSupersededStagings(root) {
282
283
  * event log is unwritable too, the new dist/ is already in place and stays the outcome. */
283
284
  function warnCleanup(root, message) {
284
285
  try {
285
- warnEvent(root, "harness", message);
286
+ warnEvent(root, TUMWATER_SOURCE, message);
286
287
  }
287
288
  catch {
288
289
  // Cleanup must not throw; the swap already succeeded.
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import path from "node:path";
2
3
  import { resolveFromNodeModules } from "./build-check-detect.js";
3
4
  import { readJsonFile } from "../files/json-files.js";
@@ -6,14 +7,14 @@ import { KILL_GRACE_MS, runScriptGroup } from "../process/process-group.js";
6
7
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
7
8
  import { errorMessage } from "../text/text.js";
8
9
  /** Keeping a tree's install in step with its lockfile (BUGS.md 2026-10-01). node_modules is
9
- * gitignored, so a tumwater worktree has none of its own: its toolchain and every import
10
- * resolve by walking UP to the root checkout's install (detectBuildCheck,
11
- * resolveFromNodeModules). That install predates any dependency a change adds, and no loop ever
12
- * ran `npm install` — so a legitimate dependency-adding change failed its gate check with
13
- * TS2307 every time, and had it landed, every tree after it would have resolved against a root
14
- * install missing the package. Two callers close both halves: runBuildCheck installs into the
15
- * checked tree when its lockfile's direct dependencies are not what the walk-up resolves, and
16
- * the landing path re-syncs the root checkout after a fast-forward moves main's lockfile. */
10
+ * gitignored, so a tumwater worktree has none of its own: its toolchain and every import resolve by
11
+ * walking UP to the root checkout's install (detectBuildCheck, resolveFromNodeModules). That
12
+ * install predates any dependency a change adds, and no loop ever ran `npm install` — so a
13
+ * legitimate dependency-adding change failed its gate check with TS2307 every time, and had it
14
+ * merged, every tree after it would have resolved against a root install missing the package. Two
15
+ * callers close both halves: runBuildCheck installs into the checked tree when its lockfile's
16
+ * direct dependencies are not what the walk-up resolves, and the merge path re-syncs the root
17
+ * checkout after a fast-forward moves main's lockfile. */
17
18
  /** Hard cap on one install run — a registry fetch of a handful of packages takes seconds; the
18
19
  * cap only bounds a hung network. Same bound as the check itself. */
19
20
  const DEP_INSTALL_TIMEOUT_MS = 300_000;
@@ -53,7 +54,7 @@ function installedVersion(dir, name) {
53
54
  /** The production installer: `npm install` in `dir` from its own lockfile, run as a process
54
55
  * group under the timeout. `--no-save` keeps package.json and package-lock.json byte-identical,
55
56
  * so neither a worktree nor the root checkout goes dirty. Not `npm ci`: ci deletes node_modules
56
- * first, and at the root that is the install every in-flight check of a live fleet resolves
57
+ * first, and at the root that is the install every in-flight check of a live team resolves
57
58
  * through. `--ignore-scripts` runs no package's (or the project's own) lifecycle scripts inside
58
59
  * the harness. */
59
60
  export const npmInstall = async (dir, timeoutMs) => {
@@ -91,22 +92,22 @@ export async function syncInstall(dir, install = npmInstall, timeoutMs = DEP_INS
91
92
  return { packages, ok: true, durationMs };
92
93
  }
93
94
  /** Re-sync the root checkout's install after a fast-forward moved main's lockfile (BUGS.md
94
- * 2026-10-01). Every fleet worktree, the redeploy compile, and the running harness resolve
95
- * through `root/node_modules`, so a landed dependency the root never installed would leave each
96
- * of them resolving against the old tree. Called under the merge lock right after the ff, so
97
- * the next landing's in-lock check already sees it. A `dep_install` event prices a run; a
98
- * failure also warns, and is retried by the next landing's call (the drift is still there) —
99
- * meanwhile each check installs into its own tree, so nothing is rejected for it. No drift (the
100
- * common case — the landing did not touch dependencies) spawns nothing and logs nothing. */
95
+ * 2026-10-01). Every team worktree, the redeploy compile, and the running harness resolve through
96
+ * `root/node_modules`, so a merged dependency the root never installed would leave each of them
97
+ * resolving against the old tree. Called under the merge lock right after the ff, so the next
98
+ * merge's in-lock check already sees it. A `dep_install` event prices a run; a failure also warns,
99
+ * and is retried by the next merge's call (the drift is still there) — meanwhile each check
100
+ * installs into its own tree, so nothing is rejected for it. No drift (the common case — the merge
101
+ * did not touch dependencies) spawns nothing and logs nothing. */
101
102
  export async function syncRootInstall(root, role, install = npmInstall) {
102
103
  const r = await syncInstall(root, install);
103
104
  if (!r)
104
105
  return;
105
- // Best-effort: the root install already ran and the ff that prompted it already moved main,
106
- // so an unwritable feed must not reject the landing that awaited this (landing-merge.ts's
107
- // tryMerge, landing-stack.ts's ffStackToMain).
106
+ // Best-effort: the root install already ran and the ff that prompted it already moved main, so an
107
+ // unwritable feed must not reject the merge that awaited this (merge-to-main.ts's tryMerge,
108
+ // merge-stack.ts's ffStackToMain).
108
109
  logEventBestEffort(root, {
109
- loop: role,
110
+ source: role,
110
111
  type: "dep_install",
111
112
  packages: r.packages,
112
113
  status: r.ok ? "passed" : "failed",
@@ -114,5 +115,5 @@ export async function syncRootInstall(root, role, install = npmInstall) {
114
115
  ...(r.detail ? { error: r.detail } : {}),
115
116
  });
116
117
  if (!r.ok)
117
- warnEventBestEffort(root, role, `the root install did not pick up main's dependencies (${r.packages.join(", ")})${r.detail ? `: ${r.detail}` : ""}; checks install into their own trees until a later landing re-syncs it`);
118
+ warnEventBestEffort(root, role, `the root install did not pick up main's dependencies (${r.packages.join(", ")})${r.detail ? `: ${r.detail}` : ""}; checks install into their own trees until a later merge re-syncs it`);
118
119
  }
@@ -1,5 +1,5 @@
1
1
  /** Measured host sleep inside a time window — the evidence that lets the harness tell "the
2
- * build failed" from "the host was asleep while the build ran". The fleet's host is a laptop
2
+ * build failed" from "the host was asleep while the build ran". The team's host is a laptop
3
3
  * in clamshell maintenance sleep (BUGS.md 2026-09-21 logged 569 sleeps over three days), and
4
4
  * a sleep shorter than a check's remaining deadline but longer than a test's own wall-clock
5
5
  * wait expires that wait at the next wake: the suite exits nonzero INSIDE the deadline, so
@@ -1,13 +1,13 @@
1
1
  /** The collector behind `tumwater diff --role <id>`: what change a loop holds between its commit
2
- * and its merge. The committed half is the loop branch's unlanded work (commits ahead of the
3
- * fleet's main branch, with the patch); the uncommitted half is the loop worktree's dirty state.
2
+ * and its merge. The committed half is the loop branch's unmerged work (commits ahead of the
3
+ * team's main branch, with the patch); the uncommitted half is the loop worktree's dirty state.
4
4
  * Everything is read-only git plumbing already used elsewhere — this module only wires the pieces
5
- * into one operator view, degrading like `report` does when the fleet (or just this role's
5
+ * into one operator view, degrading like `report` does when the team (or just this role's
6
6
  * worktree) does not exist yet. Lives beside the other collectors (src/report/report-data.ts,
7
7
  * history-data.ts, status/status-data.ts); the terminal rendering of these views is
8
8
  * change-render.ts's half. */
9
9
  import { knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
10
- import { aheadOfMain, branchExists, currentBranch, targetBranch } from "../git/git.js";
10
+ import { aheadOfMain, branchExists, countCommitsInRange, currentBranch, targetBranch } from "../git/git.js";
11
11
  import { gitTry } from "../git/git-run.js";
12
12
  import { aheadOfMainDiff, changedFiles } from "../git/git-diff.js";
13
13
  import { branchName } from "../paths.js";
@@ -17,14 +17,14 @@ import { roleWorktreeDir } from "../git/slots-state.js";
17
17
  * order — disabled roles included, since a loop stopped mid-flight still holds its branch).
18
18
  * Each entry goes through collectRoleChange, which keeps the absent/no-base degradation
19
19
  * logic single-homed and only computes the patch git-diffs for roles actually holding work
20
- * (both are "" when a role holds nothing); the fleet view then drops the patch fields. */
21
- export async function collectFleetChanges(root) {
20
+ * (both are "" when a role holds nothing); the team view then drops the patch fields. */
21
+ export async function collectTeamChanges(root) {
22
22
  const roles = await Promise.all(knownRoleIdsCached(root).map(async (role) => {
23
23
  const view = await collectRoleChange(root, role);
24
- const { diff: _diff, uncommittedDiff: _uncommittedDiff, ...fleet } = view;
25
- return fleet;
24
+ const { diff: _diff, uncommittedDiff: _uncommittedDiff, ...team } = view;
25
+ return team;
26
26
  }));
27
- // mainBranch is fleet-wide: collectRoleChange resolves it once per role through the same
27
+ // mainBranch is team-wide: collectRoleChange resolves it once per role through the same
28
28
  // targetBranch call, so the first entry's value is every entry's value.
29
29
  return { mainBranch: roles[0]?.mainBranch ?? "main", roles };
30
30
  }
@@ -32,9 +32,9 @@ export async function collectFleetChanges(root) {
32
32
  * half of the view can print unbounded text. */
33
33
  const DIFF_MAX_BYTES = 200_000;
34
34
  /** Collect the change the role's loop holds: both halves of the view, or a degraded state
35
- * when the worktree or the base branch is missing. Never throws on a degraded fleet — a
35
+ * when the worktree or the base branch is missing. Never throws on a degraded team — a
36
36
  * query about a loop that has not run yet must answer, not fail (report's rationale). */
37
- export async function collectRoleChange(root, role) {
37
+ export async function collectRoleChange(root, role, opts = {}) {
38
38
  const branch = branchName(role);
39
39
  const { config } = loadConfigSafe(root);
40
40
  const mainBranch = targetBranch(config?.baseBranch, await currentBranch(root));
@@ -43,6 +43,7 @@ export async function collectRoleChange(root, role) {
43
43
  branch,
44
44
  mainBranch,
45
45
  ahead: 0,
46
+ behind: 0,
46
47
  commits: [],
47
48
  diff: "",
48
49
  dirtyFiles: [],
@@ -58,11 +59,21 @@ export async function collectRoleChange(root, role) {
58
59
  if (wt === null || !(await isUsableWorktree(wt)))
59
60
  return { ...empty, state: "absent" };
60
61
  const ahead = await aheadOfMain(wt, mainBranch);
62
+ const behind = await countCommitsInRange(wt, `HEAD..${mainBranch}`);
61
63
  const commits = parseOnelineLog(await gitTry(wt, "log", "--oneline", `${mainBranch}..HEAD`));
62
- const diff = ahead === 0 ? "" : await aheadOfMainDiff(wt, mainBranch);
64
+ // Under opts.stat both halves hold the uncapped `git diff --stat` summary (one line per file).
65
+ const diff = ahead === 0
66
+ ? ""
67
+ : opts.stat
68
+ ? ((await gitTry(wt, "diff", "--stat", `${mainBranch}...HEAD`)) ?? "")
69
+ : await aheadOfMainDiff(wt, mainBranch);
63
70
  const dirtyFiles = await changedFiles(wt);
64
- const uncommittedDiff = dirtyFiles.length === 0 ? "" : await cappedWorkingDiff(wt);
65
- return { ...empty, state: "ready", ahead, commits, diff, dirtyFiles, uncommittedDiff };
71
+ const uncommittedDiff = dirtyFiles.length === 0
72
+ ? ""
73
+ : opts.stat
74
+ ? ((await gitTry(wt, "diff", "--stat", "HEAD")) ?? "")
75
+ : await cappedWorkingDiff(wt);
76
+ return { ...empty, state: "ready", ahead, behind, commits, diff, dirtyFiles, uncommittedDiff };
66
77
  }
67
78
  /** Parse `git log --oneline` output into {sha, subject} pairs — sha up to the first space,
68
79
  * the rest of the line the subject. Blank trailing lines (runGit's trimEnd makes them
@@ -1,28 +1,29 @@
1
1
  /** The renderer behind `tumwater diff` — the operator-facing text for the change views the
2
2
  * collector (change-data.ts, beside src/report/report-data.ts and history-data.ts) gathers: one
3
- * role's pending work (`--role <id>`) and the fleet-wide roster (no --role). Pure function of the
3
+ * role's pending work (`--role <id>`) and the team-wide roster (no --role). Pure function of the
4
4
  * collected view — no I/O, no clock reads. */
5
5
  import { plural } from "../text/phrases.js";
6
6
  /** The work-summary phrase both change views open their work lines with — `N commits ahead
7
- * of <main>` plus `, M uncommitted file(s)` when the worktree is dirty (both halves through
8
- * plural, so the singular/plural wording the tests pin cannot drift between the fleet roster
9
- * and the per-role header). Exactly two call sites: renderFleetChange's per-role line and
10
- * renderRoleChange's header line. */
11
- function workSummary(ahead, mainBranch, dirtyFiles) {
7
+ * of <main>` plus `, K commits behind` when the branch lags main, plus `, M uncommitted
8
+ * file(s)` when the worktree is dirty (each count through plural, so the singular/plural
9
+ * wording the tests pin cannot drift between the team roster and the per-role header).
10
+ * Exactly two call sites: renderTeamChange's per-role line and renderRoleChange's header line. */
11
+ function workSummary(ahead, behind, mainBranch, dirtyFiles) {
12
12
  return (`${plural(ahead, "commit")} ahead of ${mainBranch}` +
13
+ (behind > 0 ? `, ${plural(behind, "commit")} behind` : "") +
13
14
  (dirtyFiles.length > 0 ? `, ${plural(dirtyFiles.length, "uncommitted file")}` : ""));
14
15
  }
15
16
  /** Render the roster as the operator-facing text `tumwater diff` (no --role) prints: one
16
17
  * line per role that holds pending work, in roster order; roles with no worktree and roles
17
18
  * holding nothing are skipped. Every entry no-base means the baseline itself is gone — the
18
- * per-role view's line says so, fleet-wide, exit 0 like every other degraded case. */
19
- export function renderFleetChange(view) {
19
+ * per-role view's line says so, team-wide, exit 0 like every other degraded case. */
20
+ export function renderTeamChange(view) {
20
21
  if (view.roles.length > 0 && view.roles.every((r) => r.state === "no-base")) {
21
22
  return `main branch ${view.mainBranch} does not exist`;
22
23
  }
23
24
  const lines = view.roles
24
25
  .filter((r) => r.state !== "no-base" && (r.ahead > 0 || r.dirtyFiles.length > 0))
25
- .map((r) => `${r.role}: ${workSummary(r.ahead, view.mainBranch, r.dirtyFiles)}`);
26
+ .map((r) => `${r.role}: ${workSummary(r.ahead, r.behind, view.mainBranch, r.dirtyFiles)}`);
26
27
  return lines.length > 0 ? lines.join("\n") : "no pending changes";
27
28
  }
28
29
  /** Render the view as the operator-facing text `tumwater diff --role <id>` prints. */
@@ -34,7 +35,7 @@ export function renderRoleChange(view) {
34
35
  if (view.ahead === 0 && view.dirtyFiles.length === 0)
35
36
  return `no pending change for ${view.role}`;
36
37
  const lines = [
37
- `${view.role}: ${view.branch}, ${workSummary(view.ahead, view.mainBranch, view.dirtyFiles)}`,
38
+ `${view.role}: ${view.branch}, ${workSummary(view.ahead, view.behind, view.mainBranch, view.dirtyFiles)}`,
38
39
  ];
39
40
  for (const c of view.commits)
40
41
  lines.push(`${c.sha} ${c.subject}`);