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,35 +1,50 @@
1
- import { commitAll } from "../git/git.js";
1
+ import { commitAll, foldCommitsSince, headOf } from "../git/git.js";
2
2
  import { changedFiles } from "../git/git-diff.js";
3
- import { buildCommitMessage, commitTrailer, extractCommitBody, extractSummary, fallbackSummary, formatCommitBody, mergeCommitBody, stampedSubject, } from "../git/commit-message.js";
3
+ import { buildCommitMessage, commitTrailer, extractCommitBody, extractSummary, summaryOverCap, fallbackSummary, formatCommitBody, mergeCommitBody, stampedSubject, } from "../git/commit-message.js";
4
4
  import { logEventBestEffort } from "../events/events.js";
5
- import { enqueueLanding } from "../landing/landing-queue.js";
5
+ import { enqueueMerge, entryRevisionFields } from "../merge/merge-queue.js";
6
6
  import { plural } from "../text/phrases.js";
7
7
  import { recordFlow } from "./qa-coverage.js";
8
8
  import { INSTANCE_ROLES, baseRoleOf, configuredInstances } from "../roles/loop-ids.js";
9
9
  import { stagedMovedEntries } from "../scheduling/claims.js";
10
10
  import { entryKey } from "../backlog/backlog-eligibility.js";
11
11
  import { assignedMovedFinding, heldMovedFinding } from "./stage-check.js";
12
- /** Stage a changed worktree for the land queue: derive the commit message from the reply's
12
+ import { archiveNewFinishedEntries } from "../backlog/backlog-archive.js";
13
+ /** Fold commits an agent run made since `base` into the tick's tree, keeping the harness commit
14
+ * the tick's only one (INV-H1); warns when any were folded. The one home of the fold-and-warn
15
+ * policy and its wording, shared by the two fold sites — the authoring run (loop-tick.ts) and
16
+ * this module's follow-up turns — so they cannot drift. */
17
+ export async function foldAgentCommitsSince(ctx, wt, base) {
18
+ const folded = await foldCommitsSince(wt, base);
19
+ if (folded > 0)
20
+ ctx.warn(`agent made ${plural(folded, "commit")} during the run; folded into tumwater's commit`);
21
+ }
22
+ /** Stage a changed worktree for the merge queue: derive the commit message from the reply's
13
23
  * SUMMARY block (one bounded follow-up turn when the run left none), flag high-friction ticks,
14
- * commit, pin the commit by the role's landing ref, and enqueue the landing. Extracted from
24
+ * commit, pin the commit by the role's merge ref, and enqueue the merge. Extracted from
15
25
  * LoopRunner.runTick so the tick's own method stays the outcome classifier and this module is
16
- * the commit-staging layer — the only place a fresh tick's work becomes a queued landing.
26
+ * the commit-staging layer — the only place a fresh tick's work becomes a queued merge.
17
27
  * Returns the tick's outcome; a failed pin or an aborted follow-up returns an error/aborted
18
28
  * outcome without anything queued. */
19
- export async function stageTickLanding(ctx) {
29
+ export async function stageTickMerge(ctx) {
20
30
  const s = ctx.state;
21
31
  // A revision tick carries the round it applied on its context (loop-tick.ts): it rides onto the
22
- // queued landing so a rejection of this change records the NEXT round (plans/revise-rejected.md).
32
+ // queued merge so a rejection of this change records the NEXT round (plans/revise-rejected.md).
23
33
  // Undefined for a fresh change — including a user-request tick that leaves a pending revision
24
34
  // untouched — so only the tick that actually held the rejected diff is labeled a revision.
25
35
  const revisionRound = ctx.revisionRound;
26
- // A revision landing carries the review that rejected the change (plans/revise-rejected.md
27
- // part 2/2): the prior head and its numbered objections ride the queued entry so the re-review
28
- // can check each one against the interdiff. Only a tick that actually held the rejected diff
29
- // attaches it; a fresh change — including a user-request tick that leaves a pending revision
30
- // untouched — and a recovery landing carry none.
36
+ // A revision merge carries the review that rejected the change (plans/revise-rejected.md
37
+ // part 2/2): the prior head, its numbered objections, and each objection's persistence count
38
+ // ride the queued entry so the re-review can check each one against the interdiff and the next
39
+ // rejection can resolve a `(prior N)` restatement (BUGS.md 2026-10-14). Only a tick that
40
+ // actually held the rejected diff attaches it; a fresh change — including a user-request tick
41
+ // that leaves a pending revision untouched — and a recovery merge carry none.
31
42
  const priorReview = revisionRound !== undefined && s.lastReview?.head
32
- ? { sha: s.lastReview.head, reasons: s.lastReview.reasons }
43
+ ? {
44
+ sha: s.lastReview.head,
45
+ reasons: s.lastReview.reasons,
46
+ ...(s.revision?.reasonCounts?.length ? { counts: s.revision.reasonCounts } : {}),
47
+ }
33
48
  : undefined;
34
49
  // The commit subject and body come from the reply's closing block. A run that changed files
35
50
  // without one — a cut-off final message, or plain non-compliance — gets one bounded follow-up
@@ -38,7 +53,9 @@ export async function stageTickLanding(ctx) {
38
53
  let summary = extractSummary(ctx.finalText);
39
54
  let body = extractCommitBody(ctx.finalText);
40
55
  if (summary === null) {
56
+ const base = await headOf(ctx.wt, "HEAD");
41
57
  const followUp = await ctx.requestSummary(ctx.wt);
58
+ await foldAgentCommitsSince(ctx, ctx.wt, base);
42
59
  if (followUp?.aborted)
43
60
  return ctx.finishAbortedTick();
44
61
  if (followUp) {
@@ -46,7 +63,7 @@ export async function stageTickLanding(ctx) {
46
63
  body = body ?? extractCommitBody(followUp.finalText);
47
64
  }
48
65
  if (summary === null)
49
- summary = fallbackSummary(await changedFiles(ctx.wt), ctx.role, s.ticks);
66
+ summary = fallbackSummary(await changedFiles(ctx.wt), ctx.role, s.runs);
50
67
  ctx.warn(`reply had no SUMMARY line — ` +
51
68
  (followUp && extractSummary(followUp.finalText) !== null
52
69
  ? "recovered it with a follow-up turn"
@@ -57,15 +74,34 @@ export async function stageTickLanding(ctx) {
57
74
  ? `follow-up gave none; subject derived from the changed files: "${summary}"`
58
75
  : `no follow-up session was available; subject derived from the changed files: "${summary}"`));
59
76
  }
77
+ else {
78
+ // A SUMMARY the cap would cut mid-word gets one follow-up turn asking for it restated short.
79
+ const over = summaryOverCap(ctx.finalText);
80
+ if (over !== null) {
81
+ const base = await headOf(ctx.wt, "HEAD");
82
+ const followUp = await ctx.requestSummary(ctx.wt, over);
83
+ await foldAgentCommitsSince(ctx, ctx.wt, base);
84
+ if (followUp?.aborted)
85
+ return ctx.finishAbortedTick();
86
+ const short = followUp && summaryOverCap(followUp.finalText) === null ? extractSummary(followUp.finalText) : null;
87
+ if (short !== null) {
88
+ summary = short;
89
+ body = body ?? extractCommitBody(followUp.finalText);
90
+ }
91
+ ctx.warn(short !== null
92
+ ? `SUMMARY was ${over} characters — shortened with a follow-up turn`
93
+ : `SUMMARY was ${over} characters — follow-up gave no shorter one; subject truncated`);
94
+ }
95
+ }
60
96
  // The gate's deterministic backlog checks run here, before the commit, while the authoring
61
- // session can still be continued: a finding that reaches review costs a queue slot, a vet,
62
- // and a rejection that discards the work. One bounded follow-up turn on the author's own
63
- // session gets to fix them; the gate still has the final say, and the change commits and
64
- // queues either way.
97
+ // session can still be continued: a finding that reaches review costs a queue slot,
98
+ // a verification, and a rejection that discards the work. One bounded follow-up turn on the
99
+ // author's own session gets to fix them; the gate still has the final say, and the change commits
100
+ // and queues either way.
65
101
  // Claims made at staging (plans/parallel-work-instances.md "Claims", part 4/7): an
66
102
  // unassigned tick of a MULTI-instance base whose diff moved a backlog entry out of its
67
103
  // actionable section records the claim so a sibling is not assigned it while this change
68
- // lands. A single-runner base has no sibling to hold the entry from, so it stages nothing
104
+ // merges. A single-runner base has no sibling to hold the entry from, so it stages nothing
69
105
  // and is unchanged by claims. An assigned tick that moved a DIFFERENT entry gets a finding,
70
106
  // so the one fix-up turn can put it back. So does an unassigned tick that moved an entry a
71
107
  // sibling holds (the poll's heldElsewhere, which its prompt listed as off-limits): that entry
@@ -83,14 +119,14 @@ export async function stageTickLanding(ctx) {
83
119
  const first = claimable[0];
84
120
  s.claim = { file: first.file, key: first.key, title: first.title, at: Date.now(), source: "staged" };
85
121
  // The assignment is durable in state before this event; an unwritable feed must not reject
86
- // the tick and make a claim that did land look unapplied (logEventBestEffort).
122
+ // the tick and make a claim that did take effect look unapplied (logEventBestEffort).
87
123
  logEventBestEffort(ctx.root, {
88
- loop: ctx.role,
124
+ source: ctx.role,
89
125
  type: "claim",
90
126
  action: "assigned",
91
127
  key: first.key,
92
128
  title: first.title,
93
- source: "staged",
129
+ origin: "staged",
94
130
  });
95
131
  }
96
132
  const assignedMoved = s.claim ? assignedMovedFinding(s.claim.key, moved) : undefined;
@@ -101,7 +137,9 @@ export async function stageTickLanding(ctx) {
101
137
  findings.push(heldMoved);
102
138
  if (findings.length > 0) {
103
139
  const before = findings.length;
140
+ const fixBase = await headOf(ctx.wt, "HEAD");
104
141
  const fixUp = await ctx.requestStageFix(ctx.wt, findings);
142
+ await foldAgentCommitsSince(ctx, ctx.wt, fixBase);
105
143
  if (fixUp?.aborted)
106
144
  return ctx.finishAbortedTick();
107
145
  if (fixUp) {
@@ -137,53 +175,59 @@ export async function stageTickLanding(ctx) {
137
175
  const authoringTurns = ctx.tickTurns;
138
176
  const highFriction = authoringTurns > ctx.config.thrashTurns && minutes > ctx.config.thrashMinutes;
139
177
  if (highFriction) {
140
- ctx.warn(`high-friction tick: ${authoringTurns} turns in ${Math.round(minutes)} min ` +
178
+ ctx.warn(`high-friction run: ${authoringTurns} turns in ${Math.round(minutes)} min ` +
141
179
  `(thresholds: ${ctx.config.thrashTurns} turns / ${ctx.config.thrashMinutes} min)`);
142
180
  }
143
181
  // The trailer is harness-stamped truth: turns and peak ctx over this tick's pre-commit
144
182
  // runs only (conflict-resolution and review runs fold after the commit). A high-friction
145
183
  // tick appends its Friction line here — both values are already computed above.
146
- const message = buildCommitMessage(stampedSubject(baseRoleOf(ctx.role), summary), body, commitTrailer(ctx.role, s.ticks, authoringTurns, s.peakContextTokens, highFriction ? minutes : undefined, revisionRound));
184
+ const message = buildCommitMessage(stampedSubject(baseRoleOf(ctx.role), summary), body, commitTrailer(ctx.role, s.runs, authoringTurns, s.peakContextTokens, highFriction ? minutes : undefined, revisionRound));
185
+ // The backlog archive (plans/backlog-archive.md): an entry this change pasted under PLANS.md's
186
+ // ## Done or BUGS.md's ## Fixed moves to the top of PLANS-DONE.md or BUGS-FIXED.md now, after
187
+ // the self-check and its fix-up turn worked on the tree the author wrote and before the
188
+ // commit, so the review and the merge see the archived form and the live files keep only
189
+ // open work. A failed sweep leaves the entry where the author put it; nothing reads it lost.
190
+ if (ctx.mainBranch)
191
+ await archiveNewFinishedEntries(ctx.wt, ctx.mainBranch).catch(() => []);
147
192
  const commit = await commitAll(ctx.wt, message);
148
- // Pin the commit by its landing ref BEFORE freeing the worktree (invariant 4), then hand it
149
- // to the land queue: from here on the review gate and the rebase run in a vet's leased pool
150
- // slot, never in this worktree (plans/merge-queue.md 2/5). A failed pin defers to next-tick
151
- // recovery — landing without a pin would lose the ref lifecycle this whole flow depends on.
152
- // The reviewer checks the author's claimed WHY/VERIFIED against the actual diff; no diff
193
+ // Pin the commit by its merge ref BEFORE freeing the worktree (invariant 4), then hand it
194
+ // to the merge queue: from here on the review gate and the rebase run in a verification's
195
+ // leased pool slot, never in this worktree (plans/merge-queue.md 2/5). A failed pin defers to
196
+ // next-tick recovery — merge without a pin would lose the ref lifecycle this whole flow depends
197
+ // on. The reviewer checks the author's claimed WHY/VERIFIED against the actual diff; no diff
153
198
  // reaches main unreviewed.
154
199
  if (!(await ctx.pinAndReset(ctx.wt, commit))) {
155
- s.lastError = "failed to pin the landing ref; left for next-tick recovery";
200
+ s.lastError = "failed to pin the merge ref; left for the next run's recovery";
156
201
  return { result: "error", summary: s.lastError };
157
202
  }
158
- // The commit is pinned and the worktree is free: enqueue the landing and END the tick — the
159
- // orchestrator drains the queue on its single landing slot, outside the author semaphore, so
160
- // this slot is free the moment the work is committed (plans/merge-queue.md 3/5). The landing
161
- // runs the same gate + landing flow through runLandingPi/foldLandingUsage on this same state
203
+ // The commit is pinned and the worktree is free: enqueue the merge and END the tick — the
204
+ // orchestrator drains the queue on its single merge slot, outside the author semaphore, so
205
+ // this slot is free the moment the work is committed (plans/merge-queue.md 3/5). The merge
206
+ // runs the same gate + merge flow through runMergePi/foldMergeUsage on this same state
162
207
  // object — recording the commit count, the outcome, and the reviewer spend into it — and logs
163
- // landed/land_failed; a non-terminal outcome keeps the pin for next-tick leftover recovery.
164
- // `commits` was NOT incremented above: it counts landed changes only.
165
- enqueueLanding(ctx.root, {
208
+ // merge_done/merge_failed; a non-terminal outcome keeps the pin for next-tick leftover recovery.
209
+ // `commits` was NOT incremented above: it counts merged changes only.
210
+ enqueueMerge(ctx.root, {
166
211
  role: ctx.role,
167
212
  sha: commit,
168
- tick: s.ticks,
213
+ run: s.runs,
169
214
  summary,
170
215
  body: body ? formatCommitBody(body) : undefined,
171
216
  highFriction: highFriction || undefined,
172
- ...(revisionRound !== undefined ? { revisionRound } : {}),
173
- ...(priorReview !== undefined ? { priorReview } : {}),
217
+ ...entryRevisionFields({ revisionRound, priorReview }),
174
218
  enqueuedAt: Date.now(),
175
219
  });
176
- // The revision is now in flight: the rejected ref's lifecycle continues in the lander
177
- // (landing-core.ts), which deletes it when this landing lands and repoints it on a rejection.
220
+ // The revision is now in flight: the rejected ref's lifecycle continues in the merger
221
+ // (merge-core.ts), which deletes it when this merge merges and repoints it on a rejection.
178
222
  // The round is also stamped on the commit's Revision trailer above, so leftover recovery can
179
- // rebuild a retriable revision's landing entry with its round when the queue marker is lost.
223
+ // rebuild a retriable revision's merge entry with its round when the queue marker is lost.
180
224
  if (revisionRound !== undefined)
181
225
  s.revision = undefined;
182
- // The landing sits in the durable queue above and the commit is pinned; the event is
226
+ // The merge sits in the durable queue above and the commit is pinned; the event is
183
227
  // observability, so an unwritable feed must not reject a tick whose work is already queued.
184
- logEventBestEffort(ctx.root, { loop: ctx.role, type: "land_queued", commit, summary });
228
+ logEventBestEffort(ctx.root, { source: ctx.role, type: "merge_queued", commit, summary });
185
229
  // The flag's durable record is the Friction trailer line stamped on the commit above;
186
- // lastSummary and the tick_end event carry it too for dashboards and logs.
230
+ // lastSummary and the run_end event carry it too for dashboards and logs.
187
231
  const finalSummary = highFriction
188
232
  ? `${summary} (high friction: ${authoringTurns} turns / ${Math.round(minutes)}m)`
189
233
  : summary;
@@ -1,16 +1,17 @@
1
+ // vocabulary: plain
1
2
  import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
2
- import { warnEvent } from "../events/events.js";
3
+ import { warnEvent, TUMWATER_SOURCE } from "../events/events.js";
3
4
  import { removeQuiet } from "../files/files.js";
4
- import { landingStatePath } from "../paths.js";
5
+ import { mergeStatePath } from "../paths.js";
5
6
  import { runOnAbort } from "../process/process.js";
6
7
  /** The orchestrator's tick-timing and scheduling seams (src/orchestrator/orchestrator.ts
7
8
  * keeps the poll loop itself — the control flow that calls these). Each is exported as a
8
9
  * unit-test seam (test/orchestrator-seams.test.ts): timing a semaphore-gated tick, an
9
- * abort-interruptible sleep, and the restart hand-off's bounded wait on the in-flight landing.
10
+ * abort-interruptible sleep, and the restart hand-off's bounded wait on the in-flight merge.
10
11
  * Split out of orchestrator.ts — which had grown into both the poll loop and the timing helpers
11
- * it schedules with — so the loop reads as control flow over these named steps. The fleet-health
12
- * polls (the fleet hold, the error-storm warning, the failure-spread alarm) are their own family
13
- * in src/fleet/fleet-polls.ts. */
12
+ * it schedules with — so the loop reads as control flow over these named steps. The team-health
13
+ * polls (the team hold, the error-storm warning, the failure-spread alarm) are their own family
14
+ * in src/team/team-polls.ts. */
14
15
  /** How many completed role-tick samples the p75 needs before it is trusted as the drain window.
15
16
  * Below this the orchestrator reports no p75 and poll keeps the cold-start constant. */
16
17
  const DRAIN_P75_MIN_SAMPLES = 10;
@@ -25,39 +26,39 @@ export function p75TickDurationMs(durations) {
25
26
  const sorted = [...durations].sort((a, b) => a - b);
26
27
  return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.75))] ?? null;
27
28
  }
28
- /** How long a restart's hand-off waits on an in-flight landing, per phase: one window for it to
29
+ /** How long a restart's hand-off waits on an in-flight merge, per phase: one window for it to
29
30
  * finish on its own, then — having aborted it — one more for the step it was in to end
30
- * (awaitLandingForHandoff; BUGS.md 2026-09-23). By the time a restart reaches its `finally` the new
31
- * build is already in dist/ and every role tick is done or aborted, so the landing is all the fleet
32
- * is still running: each minute spent on it is a minute nothing else ticks. A landing's unbounded
31
+ * (awaitMergeForHandoff; BUGS.md 2026-09-23). By the time a restart reaches its `finally` the new
32
+ * build is already in dist/ and every role tick is done or aborted, so the merge is all the team
33
+ * is still running: each minute spent on it is a minute nothing else ticks. A merge's unbounded
33
34
  * part is model work — its reviewer run (and, until 2026-09-24, a gate build-fix run: the
34
35
  * 2026-09-23 one held the slot for 4 h 35 m and that day's hand-off sat through 97 minutes of it).
35
36
  * Its deterministic part is bounded: at most one build check (BUILD_CHECK_TIMEOUT_MS) plus git
36
- * steps. So a check's bound plus a minute lets a landing already past its model runs — in its
37
- * last check and fast-forward — land, and gives an aborted landing's current step (a check, which
38
- * no abort reaches) time to end, so the hand-off does not leave a check running in a lander
37
+ * steps. So a check's bound plus a minute lets a merge already past its model runs — in its
38
+ * last check and fast-forward — merge, and gives an aborted merge's current step (a check, which
39
+ * no abort reaches) time to end, so the hand-off does not leave a check running in a merger
39
40
  * worktree the next generation is about to reset. Not the drain's window: that is the p75 of whole
40
- * role ticks (over an hour on this fleet) — the very lag this bounds. */
41
- export const HANDOFF_LANDING_WINDOW_MS = BUILD_CHECK_TIMEOUT_MS + 60_000;
41
+ * role ticks (over an hour on this team) — the very lag this bounds. */
42
+ export const HANDOFF_MERGE_WINDOW_MS = BUILD_CHECK_TIMEOUT_MS + 60_000;
42
43
  /** Run one role tick under the concurrency semaphore and return how long the tick itself ran,
43
44
  * in ms — null when it never started (the harness is already stopping, or `held`) or was cut
44
45
  * off by an abort. This is the restart drain's p75 sample (BUGS.md 2026-09-18). The clock starts
45
46
  * only once the permit is granted, so time a tick spends parked in the semaphore queue is not
46
47
  * counted as work: the drain waits on ticks that are already running, and folding queue wait
47
- * into the window would overstate how long they have left (the `tick_start`..`tick_end` span
48
+ * into the window would overstate how long they have left (the `run_start`..`run_end` span
48
49
  * the window was sized against excludes it too). Aborted ticks return null so their short
49
50
  * cut-off lengths cannot drag the window down. So do ticks that ended with nothing to show
50
51
  * (`no_change`, `skipped`): a `main moved` re-check runs the model and still finishes in
51
- * seconds, and sampling it equally let a fleet's window collapse to the re-check scale and
52
+ * seconds, and sampling it equally let a team's window collapse to the re-check scale and
52
53
  * cut real work off minutes into its run (BUGS.md 2026-09-30) — only a work-bearing outcome
53
54
  * says how long the work the drain waits on takes. `now` is a test seam.
54
55
  *
55
- * `held` is the fleet-wide hold on new ticks (today the restart drain's), re-checked at the one
56
+ * `held` is the team-wide hold on new ticks (today the restart drain's), re-checked at the one
56
57
  * moment a reserved tick actually begins: when its permit is granted. A tick scheduled before
57
58
  * the hold may have parked in the semaphore queue long before it; checking the hold only at
58
59
  * scheduling let every such waiter start a fresh pi run mid-drain as slots freed — the very
59
60
  * ticks the restart then aborted (BUGS.md 2026-09-23). A held tick releases its permit without
60
- * calling `tick`, so it writes no state and logs no tick_start/tick_end; the caller hands its
61
+ * calling `tick`, so it writes no state and logs no run_start/run_end; the caller hands its
61
62
  * reservation back. */
62
63
  export async function runTimedRoleTick(signal, acquire, release, tick, now = Date.now, held = () => false) {
63
64
  await acquire();
@@ -118,78 +119,78 @@ function settlesWithin(promise, ms, after) {
118
119
  promise.then(() => done(true), () => done(true));
119
120
  });
120
121
  }
121
- /** The restart hand-off's bounded wait on the in-flight landing (BUGS.md 2026-09-23): the shutdown
122
- * `finally`'s landing await when this process is about to exit for the next generation. The
123
- * landings run a reviewer and a build check per change and a merge's check after them, so an
124
- * unbounded await here is a fleet-wide drain in disguise — the 2026-09-23 hand-off lagged its own
122
+ /** The restart hand-off's bounded wait on the in-flight merge (BUGS.md 2026-09-23): the shutdown
123
+ * `finally`'s merge await when this process is about to exit for the next generation. The
124
+ * merges run a reviewer and a build check per change and a merge's check after them, so an
125
+ * unbounded await here is a team-wide drain in disguise — the 2026-09-23 hand-off lagged its own
125
126
  * swap by 97 minutes, in silence. The wait is announced as it starts (one warning naming the
126
- * landing's roles), so the feed says why `restarting onto build …` is not yet followed by
127
- * `orchestrator stopped`. Past `windowMs` the landing is stopped the way the drain gives up on role
127
+ * merge's roles), so the feed says why `restarting onto build …` is not yet followed by
128
+ * `orchestrator stopped`. Past `windowMs` the merge is stopped the way the drain gives up on role
128
129
  * ticks: `abort` fires the harness's internal stop, which kills its pi runs at once and makes it
129
- * stop at its next step boundary (landing-core.ts, landing-batch.ts, and landing-stack.ts check the
130
- * signal before every gate, every approved landing, and each of a stack's check attempts), and a
131
- * warning names what was still awaited. An aborted landing records `aborted` like any shutdown
130
+ * stop at its next step boundary (merge-core.ts, merge-batch.ts, and merge-stack.ts check the
131
+ * signal before every gate, every approved merge, and each of a stack's check attempts), and a
132
+ * warning names what was still awaited. An aborted merge records `aborted` like any shutdown
132
133
  * abort — pins kept, entries dropped, marker removed — for its roles' leftover recovery on the
133
134
  * new build. One still running a window after the abort (a step wedged past its own bound) is left
134
135
  * behind: its 4/5 marker is cleared here, since this process exits the moment the hand-off returns,
135
136
  * and its entries and pins survive exactly as a crash leaves them — the next generation's first
136
- * drain re-lands them. Exported as a unit-test seam, like runTimedRoleTick. */
137
- export async function awaitLandingForHandoff(root, landing, windowMs, abort, after = realAfterDelay) {
138
- const roles = landing.roles.join(", ");
137
+ * drain re-merges them. Exported as a unit-test seam, like runTimedRoleTick. */
138
+ export async function awaitMergeForHandoff(root, merge, windowMs, abort, after = realAfterDelay) {
139
+ const roles = merge.roles.join(", ");
139
140
  const deadline = `${windowMs / 1000}s`;
140
- warnEvent(root, "harness", `restart hand-off waiting on the in-flight landing of ${roles} (deadline ${deadline})`);
141
- if (await settlesWithin(landing.promise, windowMs, after))
141
+ warnEvent(root, TUMWATER_SOURCE, `restart waiting on the merge in progress for ${roles} (deadline ${deadline})`);
142
+ if (await settlesWithin(merge.promise, windowMs, after))
142
143
  return "finished";
143
144
  abort();
144
- warnEvent(root, "harness", `restart hand-off: the landing of ${roles} outlived its ${deadline} deadline — aborted; its pinned commits survive for the next generation`);
145
- if (await settlesWithin(landing.promise, windowMs, after))
145
+ warnEvent(root, TUMWATER_SOURCE, `restart: the merge for ${roles} outlived its ${deadline} deadline — aborted; its pinned commits survive for the next restart`);
146
+ if (await settlesWithin(merge.promise, windowMs, after))
146
147
  return "aborted";
147
- removeQuiet(landingStatePath(root));
148
- warnEvent(root, "harness", `restart hand-off: the aborted landing of ${roles} was still running ${deadline} later — handing off without it; its queue entries and pinned commits survive for the next generation`);
148
+ removeQuiet(mergeStatePath(root));
149
+ warnEvent(root, TUMWATER_SOURCE, `restart: the aborted merge for ${roles} was still running ${deadline} later — restarting without it; its queue entries and pinned commits survive for the next restart`);
149
150
  return "abandoned";
150
151
  }
151
152
  /**
152
153
  * Drain the orchestrator's in-flight work at shutdown: wait out the reserved role and director
153
- * ticks, and await the landing pipeline's tasks beside them — and how depends on what comes
154
- * next. On an operator stop the harness signal has already aborted the landing — its pi runs die
154
+ * ticks, and await the merge pipeline's tasks beside them — and how depends on what comes
155
+ * next. On an operator stop the harness signal has already aborted the merge — its pi runs die
155
156
  * and it stops at its next step boundary, ending "aborted" with its ref kept and its entry
156
157
  * dropped for next-start recovery — and it is waited out: returning early would remove
157
- * orchestrator.json while the process still lands, letting a second `tumwater run` start a
158
- * concurrent lander (a second Ctrl+C still forces the exit). On a restart the caller exits for
159
- * the next generation the moment this returns, and a landing is not bounded by one reviewer run
160
- * — a merge's check follows the vets' — so the wait is a bounded hand-off that announces itself
161
- * (awaitLandingForHandoff; BUGS.md 2026-09-23). "The landing" is every landing task at once —
162
- * each vet holding a permit, and the merge — waited on (and, past the hand-off's window,
163
- * aborted) together under the roles of them all. The vetted changes waiting for the merge have
164
- * no task, and a vet still parked for its permit has started nothing (a shutdown settles it at
165
- * once; a restart's closed start gate at its grant): all of them keep their entries and pins
166
- * for the next start.
158
+ * orchestrator.json while the process is still merging, letting a second `tumwater run` start a
159
+ * concurrent merger (a second Ctrl+C still forces the exit). On a restart the caller exits for
160
+ * the next generation the moment this returns, and a merge is not bounded by one reviewer run
161
+ * — a merge's check follows the verifications' — so the wait is a bounded hand-off that
162
+ * announces itself (awaitMergeForHandoff; BUGS.md 2026-09-23). "The merge" is every merge task
163
+ * at once — each verification holding a permit, and the merge — waited on (and, past the hand-off's
164
+ * window, aborted) together under the roles of them all. The approved changes waiting for the
165
+ * merge have no task, and a verification still parked for its permit has started nothing (a
166
+ * shutdown settles it at once; a restart's closed start gate at its grant): all of them keep their
167
+ * entries and pins for the next start.
167
168
  */
168
169
  export async function drainInFlightWork(root, roleInFlight, directorInFlight, inFlight, restart, handoffWindowMs, abort, after = realAfterDelay) {
169
170
  const ticks = Promise.allSettled([...roleInFlight, ...directorInFlight]);
170
- const landing = inFlight.length === 0
171
+ const merge = inFlight.length === 0
171
172
  ? null
172
173
  : {
173
174
  promise: Promise.allSettled(inFlight.map((t) => t.promise)).then(() => { }),
174
175
  roles: inFlight.flatMap((t) => t.roles),
175
176
  };
176
- if (landing && restart) {
177
- // The abort is the caller's internal stop, not just the landing's own controller: each task
177
+ if (merge && restart) {
178
+ // The abort is the caller's internal stop, not just the merge's own controller: each task
178
179
  // wires the harness signal to its controller, and a conflict-resolution run watches the
179
- // harness signal alone (runLandingPi). Nothing else it reaches can start work here — the
180
+ // harness signal alone (runMergePi). Nothing else it reaches can start work here — the
180
181
  // director has finished, permit holders were aborted at the restart, and a parked waiter
181
182
  // meets the closed start gate (tickStartHeld) whenever it is granted a permit.
182
- const outcome = await awaitLandingForHandoff(root, landing, handoffWindowMs, abort, after);
183
- // An abandoned landing still holds its permit, so a waiter parked behind it would never be
183
+ const outcome = await awaitMergeForHandoff(root, merge, handoffWindowMs, abort, after);
184
+ // An abandoned merge still holds its permit, so a waiter parked behind it would never be
184
185
  // granted one and never settle. The ticks have had both windows beside the hand-off, so
185
- // whatever is still reserved then started nothing (or is wedged like the landing): the
186
+ // whatever is still reserved then started nothing (or is wedged like the merge): the
186
187
  // hand-off goes ahead without it too.
187
188
  if (outcome !== "abandoned")
188
189
  await ticks;
189
190
  }
190
191
  else {
191
- if (landing)
192
- warnEvent(root, "harness", `shutdown waiting on the in-flight landing of ${landing.roles.join(", ")}`);
193
- await Promise.allSettled([ticks, landing?.promise]);
192
+ if (merge)
193
+ warnEvent(root, TUMWATER_SOURCE, `shutdown waiting on the merge in progress for ${merge.roles.join(", ")}`);
194
+ await Promise.allSettled([ticks, merge?.promise]);
194
195
  }
195
196
  }
@@ -3,12 +3,12 @@ import { recordDailyCost } from "../budget/budget.js";
3
3
  import { nonNegativeNumber } from "../files/json-object.js";
4
4
  /** Usage accounting for one role loop, split out of loop.ts — which keeps the tick lifecycle —
5
5
  * because token/cost/turn bookkeeping is a self-contained concern with its own consumers: the
6
- * commit trailer reads turns, the tick_end event reads costUsd, and the orchestrator's fleet-wide
7
- * hold reads lastRateLimit and lastBackendFailure (src/fleet/fleet-hold.ts,
8
- * src/fleet/fleet-polls.ts).
6
+ * commit trailer reads turns, the run_end event reads costUsd, and the orchestrator's team-wide
7
+ * hold reads lastRateLimit and lastBackendFailure (src/team/team-hold.ts,
8
+ * src/team/team-polls.ts).
9
9
  *
10
10
  * fold() is the once-per-run choke point: every pi run of a tick — main attempt, transient
11
- * retry, conflict resolution, landing runs via foldLandingUsage — lands here exactly once, so
11
+ * retry, conflict resolution, merge runs via foldMergeUsage — ends up here exactly once, so
12
12
  * adding a usage field to PiRunResult touches this single place. */
13
13
  /** One persisted counter, healed to a finite non-negative number before it is read and
14
14
  * rewritten: `generatedTokens`/`peakContextTokens` are per-tick windows and `totalCostUsd` a
@@ -32,12 +32,12 @@ export class TickUsage {
32
32
  turns = 0;
33
33
  /** USD cost folded into THIS tick so far (non-persisted): reset at tick start alongside
34
34
  * turns, grown in fold. Deliberately not on LoopState — unlike generatedTokens, no
35
- * dashboard reads it mid-run; its only consumer is the tick_end event, which fires before
35
+ * dashboard reads it mid-run; its only consumer is the run_end event, which fires before
36
36
  * the next tick resets it (plans: per-tick usage in the event feed). */
37
37
  costUsd = 0;
38
38
  /** Subscription runs' cost folded into THIS tick (non-persisted): API-equivalent spend the
39
39
  * run reported but that never charges the daily cap (plans/agent-backends.md part 6a). The
40
- * tick_end event carries it and the report shows it on its own notional line. Reset at tick
40
+ * run_end event carries it and the report shows it on its own notional line. Reset at tick
41
41
  * start like costUsd. */
42
42
  notionalCostUsd = 0;
43
43
  /** How the tick's runs were last reported as paid for, and the newest subscription
@@ -48,31 +48,38 @@ export class TickUsage {
48
48
  rateWindows;
49
49
  /** Prompt tokens folded into THIS tick so far (non-persisted, reset at tick start). Every
50
50
  * pi run of the tick contributes — the authoring run, its transient retry, the SUMMARY
51
- * follow-up, and (through foldLandingUsage) review and conflict-resolution runs — because
51
+ * follow-up, and (through foldMergeUsage) review and conflict-resolution runs — because
52
52
  * all of them re-send context the operator paid for. */
53
53
  promptTokens = 0;
54
54
  /** The cache-read share of promptTokens folded so far. */
55
55
  cacheReadTokens = 0;
56
56
  /** Prompt tokens folded before the tick's first edit: authoring runs only, frozen once an
57
- * authoring run reports an edit (plans/per-tick-prompt-telemetry). Landing, review and
57
+ * authoring run reports an edit (plans/per-tick-prompt-telemetry). Merge, review and
58
58
  * conflict-resolution runs are excluded — their context is the gate's, not the authoring
59
59
  * tick's pre-edit search. */
60
60
  preEditPromptTokens = 0;
61
61
  /** True once an authoring run has reported a first edit; the pre-edit counter freezes. */
62
62
  editSeen = false;
63
63
  /** This loop's most recent pi run that ended on a provider 429 — author run, retry,
64
- * reviewer or landing run alike, since every one folds through fold — with the provider's
64
+ * reviewer or merge run alike, since every one folds through fold — with the provider's
65
65
  * Retry-After hint when it sent one. The orchestrator reads it every poll as this role's
66
- * rate-limit input to the fleet-wide hold (src/fleet/fleet-hold.ts): the per-run retry has no
66
+ * rate-limit input to the team-wide hold (src/team/team-hold.ts): the per-run retry has no
67
67
  * cross-role view, and a field on the runner the orchestrator already holds carries the
68
68
  * fact without a new cross-module channel. In memory only — a hold is minutes long, so a
69
69
  * restart forgetting it costs nothing. Undefined until the first such run. */
70
70
  lastRateLimit;
71
+ /** Per-provider spend folded into THIS tick (non-persisted, reset at tick start): each
72
+ * run's real/notional cost keyed by the provider it ran on, so the run_end event can say
73
+ * which seam of a mixed team spent what (plans: per-provider spend in the day report). A
74
+ * run whose provider was never resolved attributes nothing — the `provider !== undefined`
75
+ * guard below skips the bump — so an old single-provider tick keeps today's byte-identical
76
+ * behavior. Parallel to costUsd/notionalCostUsd, never subtracted from them. */
77
+ providerCost = new Map();
71
78
  /** This loop's most recent pi run that ended on a provider-wide failure that was NOT rate
72
79
  * limiting — the connection down, a 5xx, the model failing to load (PiRunResult.
73
80
  * transientBackend) — with the kind the hold's storm test groups storms by. Stamped under
74
- * the same rule and for the same consumer as lastRateLimit above: the fleet-wide hold
75
- * (src/fleet/fleet-hold.ts) reads it every poll. In memory only, like lastRateLimit.
81
+ * the same rule and for the same consumer as lastRateLimit above: the team-wide hold
82
+ * (src/team/team-hold.ts) reads it every poll. In memory only, like lastRateLimit.
76
83
  * Undefined until the first such run. */
77
84
  lastBackendFailure;
78
85
  /** Clear the per-tick windows (turns, costUsd, the prompt-token counters) at tick start.
@@ -88,12 +95,13 @@ export class TickUsage {
88
95
  this.cacheReadTokens = 0;
89
96
  this.preEditPromptTokens = 0;
90
97
  this.editSeen = false;
98
+ this.providerCost = new Map();
91
99
  }
92
100
  /** Fold one pi run's usage into the tick's counters (gen / peak ctx / cost / turns) and
93
- * the loop's lifetime totals on `s`. `authoring` is false for a landing/review run folded
94
- * through foldLandingUsage: those never advance the tick's pre-edit counter, which measures
101
+ * the loop's lifetime totals on `s`. `authoring` is false for a merge/review run folded
102
+ * through foldMergeUsage: those never advance the tick's pre-edit counter, which measures
95
103
  * the authoring search. */
96
- fold(s, run, authoring = true) {
104
+ fold(s, run, authoring = true, provider, pair) {
97
105
  s.generatedTokens = counter(s.generatedTokens) + run.outputTokens;
98
106
  s.peakContextTokens = Math.max(counter(s.peakContextTokens), run.peakContextTokens);
99
107
  s.totalCostUsd = counter(s.totalCostUsd) + run.costUsd;
@@ -103,12 +111,14 @@ export class TickUsage {
103
111
  if (runCostIsReal(run)) {
104
112
  this.costUsd += run.costUsd;
105
113
  // The daily cost budget window (plans/daily-cost-budget.md): every real-money pi run of
106
- // a tick folds here exactly once, so the fleet's spend for the local day is complete at
114
+ // a tick folds here exactly once, so the team's spend for the local day is complete at
107
115
  // each tick end.
108
- recordDailyCost(s, run.costUsd);
116
+ recordDailyCost(s, run.costUsd, Date.now(), pair);
117
+ this.bumpProviderCost(provider, run.costUsd, 'costUsd');
109
118
  }
110
119
  else {
111
120
  this.notionalCostUsd += run.costUsd;
121
+ this.bumpProviderCost(provider, run.costUsd, 'notionalCostUsd');
112
122
  }
113
123
  if (run.billing !== undefined)
114
124
  this.billing = run.billing;
@@ -118,7 +128,7 @@ export class TickUsage {
118
128
  this.cacheReadTokens += run.cacheReadTokens;
119
129
  // The pre-edit prefix freezes at the tick's first edit: every authoring run while no edit
120
130
  // has been seen adds its pre-edit count (including the run that edits), and once one
121
- // reports an edit later runs add nothing. A review/landing run is skipped entirely.
131
+ // reports an edit later runs add nothing. A review/merge run is skipped entirely.
122
132
  if (authoring) {
123
133
  if (!this.editSeen)
124
134
  this.preEditPromptTokens += run.preEditPromptTokens;
@@ -126,17 +136,37 @@ export class TickUsage {
126
136
  this.editSeen = true;
127
137
  }
128
138
  this.turns += run.turns;
129
- // The fleet-wide hold's rate-limit input (lastRateLimit above), from the same
139
+ // The team-wide hold's rate-limit input (lastRateLimit above), from the same
130
140
  // once-per-run choke point. Only a run that ENDED on the 429 counts: pi exits on the error,
131
141
  // so "now" is when the provider refused — a run that merely logged one inside pi's own
132
142
  // auto-retry and then
133
143
  // finished would stamp a 429 at its end, possibly hours late, and could trip a false storm.
134
144
  if (run.transientRateLimit && !run.ok)
135
- this.lastRateLimit = { at: Date.now(), retryAfterSeconds: run.retryAfterSeconds };
145
+ this.lastRateLimit = { at: Date.now(), retryAfterSeconds: run.retryAfterSeconds, provider };
136
146
  // The backend-failure sibling of the stamp above, same rule: only a run that ENDED on the
137
- // failure counts, and the kind travels with it (src/pi/pi-stream.ts backendKind classified
138
- // the text at parse time). A 429 never lands here — pi-stream checks rate-limit first.
147
+ // failure counts, and the kind travels with it (src/pi/pi-failure.ts backendKind classified
148
+ // the text at parse time). A 429 never ends up here — pi-stream checks rate-limit first.
139
149
  if (run.transientBackend && !run.ok && run.backendKind)
140
- this.lastBackendFailure = { at: Date.now(), kind: run.backendKind };
150
+ this.lastBackendFailure = { at: Date.now(), kind: run.backendKind, provider };
151
+ }
152
+ /** Add `amount` to one field of the tick's per-provider spend ledger, initializing the
153
+ * provider's entry on first contact. `provider` undefined (a run that never resolved one)
154
+ * is a no-op, matching how fold calls it: only runs with a provider reach this. */
155
+ bumpProviderCost(provider, amount, field) {
156
+ if (provider === undefined)
157
+ return;
158
+ const e = this.providerCost.get(provider) ?? { costUsd: 0, notionalCostUsd: 0 };
159
+ e[field] += amount;
160
+ this.providerCost.set(provider, e);
161
+ }
162
+ /** Serialize the per-provider split for the run_end payload: a `Record<provider,
163
+ * {costUsd, notionalCostUsd}>`, or `{}` when no run carried a provider — the
164
+ * omit-when-empty idiom tick-finalize.ts applies, so a tick whose runs never resolved a
165
+ * provider renders no providerCost field at all. */
166
+ serializeProviderCost() {
167
+ const out = {};
168
+ for (const [provider, e] of this.providerCost)
169
+ out[provider] = e;
170
+ return out;
141
171
  }
142
172
  }