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,57 +1,58 @@
1
+ // vocabulary: plain
1
2
  import { aheadOfMain, commitMessage, deleteRef, headOf, isMergedInto, refSha, setRef } from "../git/git.js";
2
3
  import { parseCommitMetadata } from "../git/commit-message.js";
3
4
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
- import { enqueueLanding, queuedLandings } from "../landing/landing-queue.js";
5
- import { landingRefName } from "../paths.js";
5
+ import { enqueueMerge, queuedMerges } from "../merge/merge-queue.js";
6
+ import { mergeRefName } from "../paths.js";
6
7
  import { shortSha } from "../text/format.js";
7
- /** Salvaging a commit a previous tick left unlanded (plans/merge-queue.md). Since merge queue
8
+ /** Salvaging a commit a previous tick left unmerged (plans/merge-queue.md). Since merge queue
8
9
  * 2/5 the role's branch is reset to main the moment its commit is pinned, so the leftover
9
10
  * normally lives in `refs/tumwater/landing/<role>`: recovery puts that sha back on the durable
10
- * land queue, so it lands through the SAME slot, gate, and strike cap a fresh tick's change
11
+ * merge queue, so it merges through the SAME slot, gate, and strike cap a fresh tick's change
11
12
  * does — no crash or abort path smuggles unreviewed work into main (invariant 1), and main keeps
12
- * exactly one writer, the orchestrator's landing slot (an in-tick recovery landing used to race
13
+ * exactly one writer, the orchestrator's merge slot (an in-tick recovery merge used to race
13
14
  * the slot's batches into `merge_blocked`). A commit with NO pin — a crash in the window between
14
15
  * the tick's commit and its pin write, or a failed pin write itself — still sits on the branch
15
16
  * ahead of main; recovery adopts that tip into the pin scheme and queues it, so invariant 1
16
17
  * holds whether or not the pin survived. Split out of loop.ts — which keeps the tick lifecycle
17
18
  * around it — because this is a self-contained concern with its own entry condition and git
18
19
  * surface; the only things it borrows from the loop are identity, the tick number, the
19
- * worktree (for the no-pin fallback), and three pieces of the role's landing state: its
20
+ * worktree (for the no-pin fallback), and three pieces of the role's merge state: its
20
21
  * conflict streak, outstanding hand-back, and permanent-reviewer hold. */
21
- /** Consecutive landings of one pinned sha that may end `merge_conflict` before recovery stops
22
+ /** Consecutive merges of one pinned sha that may end `merge_conflict` before recovery stops
22
23
  * re-queuing it (LoopState.mergeConflicts). Each attempt costs a conflict-resolution run, and
23
24
  * a role holding a pin never authors: without a cap an unmergeable change would re-queue
24
25
  * forever, burning one resolution run per drain. At the cap the pin is handed back to its
25
26
  * author with the markers in place rather than discarded (PLANS.md "Robust conflict landing,
26
27
  * part 2/2"); a lineage already handed back once is discarded instead. */
27
28
  export const MERGE_CONFLICT_LIMIT = 2;
28
- /** Queue a commit a previous tick left unlanded. Entry condition: the landing ref exists and
29
+ /** Queue a commit a previous tick left unmerged. Entry condition: the merge ref exists and
29
30
  * its sha is not yet contained in main — or, with no pin at all, the role's branch is ahead of
30
31
  * main (crash between the commit and the pin). A present-but-contained ref is stale — a crash
31
32
  * between the ff-merge and the ref deletion — and is deleted without queuing anything. Returns
32
33
  * null when there was nothing to salvage. The queued entry carries the pin's sha plus the
33
34
  * summary, body, and high-friction flag read back from its commit message, because the
34
- * authoring run that set them is gone; from there the landing slot keeps or deletes the ref per
35
- * the lander's usual policy. An unreadable ref or worktree reads as no leftover — only git-level
35
+ * authoring run that set them is gone; from there the merge slot keeps or deletes the ref per
36
+ * the merger's usual policy. An unreadable ref or worktree reads as no leftover — only git-level
36
37
  * errors in the stale-pin cleanup propagate. */
37
38
  export async function recoverLeftover(ctx) {
38
- // One landing ref per role, so at most one outstanding landing per role: a second entry beside
39
+ // One merge ref per role, so at most one outstanding merge per role: a second entry beside
39
40
  // one the slot has yet to finish would share — and fight over — the same ref.
40
- const queued = queuedLandings(ctx.root).find((e) => e.role === ctx.role);
41
+ const queued = queuedMerges(ctx.root).find((e) => e.role === ctx.role);
41
42
  if (queued)
42
43
  return { kind: "already_queued", entry: queued };
43
- const ref = landingRefName(ctx.role);
44
+ const ref = mergeRefName(ctx.role);
44
45
  let sha = await refSha(ctx.root, ref).catch(() => null);
45
46
  if (sha) {
46
47
  if (await isMergedInto(ctx.root, sha, ctx.mainBranch)) {
47
- // Stale pin: the work already landed and a crash skipped its un-pinning. Clean it up so
48
- // the next tick does not re-land what main already holds.
48
+ // Stale pin: the work already merged and a crash skipped its un-pinning. Clean it up so
49
+ // the next tick does not re-merge what main already holds.
49
50
  await deleteRef(ctx.root, ref);
50
51
  return null;
51
52
  }
52
53
  }
53
54
  else {
54
- // No pin: either nothing was left behind, or a crash landed in the commit→pin window (or
55
+ // No pin: either nothing was left behind, or a crash hit the commit→pin window (or
55
56
  // the pin write failed) and the commit still sits on the branch ahead of main. Recover that
56
57
  // tip too — invariant 1 must hold whether or not the pin survived. An unreadable worktree
57
58
  // reads as "no leftover", exactly like a failed ref read: never propagate into the tick.
@@ -62,11 +63,11 @@ export async function recoverLeftover(ctx) {
62
63
  if (!sha)
63
64
  return null;
64
65
  // Adopt the unpinned commit into the pin scheme so every downstream outcome — kept on an
65
- // under-cap review failure (the strike cap's retry), deleted on reject/land/discard,
66
+ // under-cap review failure (the strike cap's retry), deleted on reject/merge/discard,
66
67
  // discarded on a user abort — behaves exactly as for a normally pinned one. A failed
67
68
  // adoption is logged and leaves the commit on the branch (see `unpinned`).
68
69
  if (!(await setRef(ctx.root, ref, sha))) {
69
- warnEventBestEffort(ctx.root, ctx.role, `failed to adopt unpinned leftover ${shortSha(sha)} into its landing ref`);
70
+ warnEventBestEffort(ctx.root, ctx.role, `failed to adopt unpinned leftover ${shortSha(sha)} into its merge ref`);
70
71
  return { kind: "unpinned", sha };
71
72
  }
72
73
  }
@@ -75,7 +76,7 @@ export async function recoverLeftover(ctx) {
75
76
  // full gate check for a request that can never succeed until the model config changes. The
76
77
  // caller clears the hold (and re-queues) once the resolved reviewer selector differs, so a
77
78
  // fixed config recovers automatically (BUGS.md 2026-10-06).
78
- const held = ctx.landingReviewError;
79
+ const held = ctx.mergeReviewError;
79
80
  if (held?.sha === sha)
80
81
  return { kind: "held", sha, message: held.message };
81
82
  const attempts = ctx.mergeConflicts?.sha === sha ? ctx.mergeConflicts.count : 0;
@@ -85,7 +86,7 @@ export async function recoverLeftover(ctx) {
85
86
  // This lineage already used its one hand-back and still conflicts at the cap: drop the
86
87
  // pin (the warning names its sha and both attempts) and let the author start over.
87
88
  await deleteRef(ctx.root, ref);
88
- warnEventBestEffort(ctx.root, ctx.role, `discarding leftover ${shortSha(sha)} after ${attempts} landings ended in unresolved merge conflicts with main, even after a hand-back`);
89
+ warnEventBestEffort(ctx.root, ctx.role, `discarding leftover ${shortSha(sha)} after ${attempts} merges ended in unresolved conflicts with main, even after sending it back to the role to resolve`);
89
90
  return { kind: "discarded", sha, summary, attempts };
90
91
  }
91
92
  // Main has moved too far under this change for the resolver to reconcile it: keep the pin
@@ -97,9 +98,9 @@ export async function recoverLeftover(ctx) {
97
98
  const entry = {
98
99
  role: ctx.role,
99
100
  sha,
100
- tick: ctx.tick,
101
- // Name what lands, not merely that a recovery happened: the recovered commit's own subject
102
- // rides the landing, so the failure digest's "Landed in the window" can correlate a
101
+ run: ctx.tick,
102
+ // Name what merges, not merely that a recovery happened: the recovered commit's own subject
103
+ // rides the merge, so the failure digest's "Merged in the window" can correlate a
103
104
  // recovered merge with the work it names (BUGS.md 2026-09-21). Unreadable messages fall
104
105
  // back to the bare provenance label.
105
106
  summary: meta.subject
@@ -108,18 +109,18 @@ export async function recoverLeftover(ctx) {
108
109
  body: meta.body,
109
110
  highFriction: meta.highFriction || undefined,
110
111
  // A retriable revision keeps its round from the commit's Revision trailer, so its next
111
- // rejection records the following round (not round 1 again) and a recovered landing still
112
+ // rejection records the following round (not round 1 again) and a recovered merge still
112
113
  // deletes the rejected ref (plans/revise-rejected.md). Absent on a fresh change.
113
114
  ...(meta.revisionRound !== undefined ? { revisionRound: meta.revisionRound } : {}),
114
115
  enqueuedAt: Date.now(),
115
116
  };
116
- enqueueLanding(ctx.root, entry);
117
- logEventBestEffort(ctx.root, { loop: ctx.role, type: "land_queued", commit: sha, summary: entry.summary });
117
+ enqueueMerge(ctx.root, entry);
118
+ logEventBestEffort(ctx.root, { source: ctx.role, type: "merge_queued", commit: sha, summary: entry.summary });
118
119
  return { kind: "enqueued", entry };
119
120
  }
120
121
  /** The review-gate metadata a pinned leftover commit carries in its own message: its
121
122
  * subject, high-friction flag, and author's body, or an empty object when the commit
122
- * predates the contract or the message cannot be read. Recovery lands through the same gate
123
+ * predates the contract or the message cannot be read. Recovery merges through the same gate
123
124
  * as a fresh tick, so it must present those fields the same way — otherwise a flagged change
124
125
  * is silently reviewed as routine (BUGS.md 2026-09-19), and the merged event names the
125
126
  * recovery but not the work (BUGS.md 2026-09-21). */
@@ -35,7 +35,7 @@ export function foldModelFallbackPhase(ctx, fallbackCtx, pi) {
35
35
  const willBeIn = next !== undefined && next.since > 0;
36
36
  if (!wasIn && willBeIn) {
37
37
  logEventBestEffort(ctx.root, {
38
- loop: ctx.role,
38
+ source: ctx.role,
39
39
  type: "model_fallback_started",
40
40
  provider: fallbackCtx.fallback.provider,
41
41
  model: fallbackCtx.fallback.model,
@@ -57,7 +57,7 @@ function emitFallbackEnded(ctx, prior, primary, now, wasIn) {
57
57
  if (!wasIn)
58
58
  return;
59
59
  logEventBestEffort(ctx.root, {
60
- loop: ctx.role,
60
+ source: ctx.role,
61
61
  type: "model_fallback_ended",
62
62
  provider: primary.provider,
63
63
  model: primary.model,
@@ -1,15 +1,19 @@
1
1
  import { runPi } from "../pi/pi.js";
2
2
  import { agentSessionResumable } from "../agent/agent-backend.js";
3
- import { HOLD_BASE_MS } from "../fleet/fleet-hold.js";
3
+ import { HOLD_BASE_MS } from "../team/team-hold.js";
4
4
  import { backendKindPhrase } from "../text/phrases.js";
5
- import { configForRole } from "../config/config-views.js";
5
+ import { configForRole, resolvedAgentConfig, seamHoldKey, seamPairName } from "../config/config-views.js";
6
+ import { ensureProviderLease } from "../concurrency/provider-lease.js";
6
7
  import { buildSummaryRequestPrompt } from "../prompt/prompt-followup.js";
7
8
  import { readClaudeAccount, subscriptionWindowRejected } from "../agent/claude/claude-account.js";
8
- import { dailyCost, fleetDailyCost } from "../budget/budget.js";
9
+ import { observeOpencodeRun, opencodeModelKey } from "../agent/opencode/opencode-catalog.js";
10
+ import { dailyCost, teamDailyCost, pairDailyCost } from "../budget/budget.js";
9
11
  import { loadLoopState } from "./loop-state.js";
10
12
  import { baseRoleOf, loopIds } from "../roles/loop-ids.js";
11
13
  import { buildStageFixPrompt } from "../prompt/gate-prompts.js";
14
+ import { logEventBestEffort } from "../events/events.js";
12
15
  import { nativeLogPath, piLogPath, roleNotesPath, sessionDir } from "../paths.js";
16
+ import { piModelsPath } from "../pi/pi-models.js";
13
17
  import { DIRECTOR_ROLE } from "../roles/roles.js";
14
18
  import { cappedRequestTimeouts } from "../request-timeouts.js";
15
19
  /** Upper bound on how long the transient retry waits out a provider's Retry-After hint
@@ -19,7 +23,7 @@ import { cappedRequestTimeouts } from "../request-timeouts.js";
19
23
  * working). */
20
24
  const RATE_LIMIT_RETRY_AFTER_CAP_S = 120;
21
25
  /** What a hint-less 429 waits before its one retry. With no Retry-After from the provider, the
22
- * fleet's own constants state the refill physics: fleet/fleet-hold.ts's base hold is one minute,
26
+ * team's own constants state the refill physics: team/team-hold.ts's base hold is one minute,
23
27
  * "the shortest pause that lets the bucket refill". Retrying sooner re-enters the same exhausted
24
28
  * per-minute bucket the first request just emptied and burns the tick's only retry on a
25
29
  * near-certain second 429. A present hint always wins instead; the cap above still bounds
@@ -30,24 +34,25 @@ function sleepFor(ms) {
30
34
  return new Promise((r) => setTimeout(r, ms));
31
35
  }
32
36
  /** The `--max-budget-usd` cap for one API-billed claude authoring run (plans/agent-backends.md
33
- * §7): the remaining fleet budget (`maxDailyCostUsd` − today's fleet spend), lowered to the
37
+ * §7): the remaining team budget (`maxDailyCostUsd` − today's team spend), lowered to the
34
38
  * base role's own remaining cap when `maxDailyCostUsdPerRole` names it. The cap map and its
35
39
  * spend are keyed by BASE role, exactly as the scheduler's role-cap gate reads them
36
40
  * (src/gates/role-cap-gates.ts: `baseRoleOf` groups `feature`/`feature-2` and sums the
37
41
  * group's spend), so instance loop ids share their base's cap instead of silently falling
38
- * through to the fleet budget. Returns undefined — no cap — for the director, a non-claude
42
+ * through to the team budget. Returns undefined — no cap — for the director, a non-claude
39
43
  * seam, an account that is not API-billed, a disabled cap (0), or no remaining budget at all
40
44
  * (a non-positive cap is dropped by the argv builder). Spend is read from the persisted loop
41
45
  * states: each role's daily window is what the budget gate and the dashboards sum, and this
42
- * runs before the authoring run records its own cost. */
43
- function authoringBudgetCap(root, role, cfg, agent) {
46
+ * runs before the authoring run records its own cost. A pair with a step cap of its own (a
47
+ * priced fallback step, `fallbackStepCaps`) is also held to that step's remaining budget. */
48
+ function authoringBudgetCap(root, role, cfg, agent, pair) {
44
49
  if (role === DIRECTOR_ROLE || agent !== "claude")
45
50
  return undefined;
46
51
  if (readClaudeAccount(root)?.billing !== "api")
47
52
  return undefined;
48
53
  const now = Date.now();
49
54
  const states = loopIds(cfg).map((r) => loadLoopState(root, r));
50
- let cap = cfg.maxDailyCostUsd - fleetDailyCost(states, now);
55
+ let cap = cfg.maxDailyCostUsd - teamDailyCost(states, now);
51
56
  const base = baseRoleOf(role);
52
57
  const perRole = cfg.maxDailyCostUsdPerRole?.[base];
53
58
  if (perRole !== undefined && perRole > 0) {
@@ -57,17 +62,22 @@ function authoringBudgetCap(root, role, cfg, agent) {
57
62
  : dailyCost(loadLoopState(root, role), now);
58
63
  cap = Math.min(cap, perRole - spent);
59
64
  }
65
+ // A fallback step with its own cap (applyFallbackModel's fallbackStepCaps, set only on the
66
+ // derived view) lowers the run to what is left of the step's budget for the day.
67
+ const stepCap = cfg.fallbackStepCaps?.[pair];
68
+ if (stepCap !== undefined)
69
+ cap = Math.min(cap, stepCap - pairDailyCost(states, pair, now));
60
70
  // Round the subtraction's floating-point residue off so the argv carries a clean number.
61
71
  cap = Math.round(cap * 1e6) / 1e6;
62
72
  return cap > 0 ? cap : undefined;
63
73
  }
64
74
  /** The pi-invocation plumbing of one role loop, extracted from LoopRunner (src/loop/loop.ts):
65
75
  * the shared per-loop wiring for every run (role config, session dir, raw log, abort
66
- * signal), the one bounded transient-failure retry all runs share, the landing slot's
76
+ * signal), the one bounded transient-failure retry all runs share, the merge slot's
67
77
  * run (which watches only the shutdown signal), and the tick's SUMMARY follow-up turn.
68
78
  * It owns HOW a loop talks to pi; the LoopRunner tick state machine owns WHAT runs happen
69
79
  * and folds every run's spend back through the host's foldUsage (authoring) or
70
- * foldLandingUsage (landing/review). */
80
+ * foldMergeUsage (merge/review). */
71
81
  export class LoopPi {
72
82
  host;
73
83
  constructor(host) {
@@ -77,15 +87,15 @@ export class LoopPi {
77
87
  * role-resolved config, this loop's session dir and raw log, and the per-tick abort signal.
78
88
  * One place for those derivations — a new PiRunOptions field touches only here instead of
79
89
  * drifting between the author run and the SUMMARY follow-up. Callers override what differs
80
- * (the follow-up's capped timeouts, the landing's shutdown-only signal). */
90
+ * (the follow-up's capped timeouts, the merge's shutdown-only signal). */
81
91
  loopPiOpts(wt, prompt, sessionName, kind, resume = false, config, notesPath) {
82
- // A caller-supplied config (the landing conflict resolver's strong-tier one,
92
+ // A caller-supplied config (the merge conflict resolver's strong-tier one,
83
93
  // plans/model-tiers.md part 4/8) replaces the role's own for that one run; the role's
84
94
  // config is the default for every other caller. Resolved once here so the native-log path
85
95
  // names the same agent the run actually uses.
86
96
  const resolved = config ?? configForRole(this.host.config(), this.host.role);
87
97
  const maxBudgetUsd = kind === "author"
88
- ? authoringBudgetCap(this.host.root, this.host.role, this.host.config(), resolved.agent ?? "pi")
98
+ ? authoringBudgetCap(this.host.root, this.host.role, this.host.config(), resolved.agent ?? "pi", seamPairName(resolved))
89
99
  : undefined;
90
100
  return {
91
101
  cwd: wt,
@@ -101,7 +111,7 @@ export class LoopPi {
101
111
  rawLogFile: piLogPath(this.host.root, this.host.role),
102
112
  nativeLogFile: nativeLogPath(this.host.root, this.host.role, resolved.agent ?? "pi"),
103
113
  signal: this.host.runSignal(),
104
- // Only runAuthoringPi and its SUMMARY/stage-fix follow-ups pass this; a landing,
114
+ // Only runAuthoringPi and its SUMMARY/stage-fix follow-ups pass this; a merge,
105
115
  // review, or conflict-resolution run has no notebook, and neither does the director.
106
116
  notesPath,
107
117
  // A tool call silent for the configured stall threshold names itself in the event feed
@@ -112,30 +122,42 @@ export class LoopPi {
112
122
  // The translator's unmapped-native-type and cost_unknown warnings reach the event feed
113
123
  // as their own warning events, once per run.
114
124
  onAgentWarning: (warning) => this.host.warn(`agent_unknown_event: ${warning}`),
125
+ // A run that read an image its model cannot see (plans/image-input.md "Detecting a
126
+ // blind read"): the runner killed the authoring run (action "fail") or a gate run
127
+ // merely recorded it ("logged") — each logged here with this loop's identity so the
128
+ // feed names who, on which tick, read which file on which blind model.
129
+ onBlindRead: (path, action) => logEventBestEffort(this.host.root, {
130
+ source: this.host.role,
131
+ type: "image_blind",
132
+ run: this.host.tickNumber(),
133
+ path,
134
+ model: resolved.model,
135
+ action,
136
+ }),
115
137
  };
116
138
  }
117
- /** Run the orchestrator's landing slot's pi (merge queue 3/5): the shared per-loop wiring
139
+ /** Run the orchestrator's merge slot's pi (merge queue 3/5): the shared per-loop wiring
118
140
  * of runRolePi — role config, session dir, raw log, transient-failure retry — with usage
119
141
  * folded into the authoring loop's counters, but watching ONLY the harness shutdown signal
120
- * (which may be undefined — PiRunOptions.signal is optional): a landing is outside any
142
+ * (which may be undefined — PiRunOptions.signal is optional): a merge is outside any
121
143
  * tick, so `tumwater abort --role` (which targets a tick's per-tick abort controller) must
122
144
  * never reach it, and the stale per-tick controller of a finished tick must not abort it
123
145
  * either. Session naming is the caller's (the reviewer composes its own from
124
- * ReviewContext.tick; landing-merge.ts keeps its conflict-resolver naming through
125
- * LanderContext.runPi).
146
+ * ReviewContext.tick; merge-to-main.ts keeps its conflict-resolver naming through
147
+ * MergerContext.runPi).
126
148
  */
127
- async runLandingPi(wt, prompt, sessionName, config) {
149
+ async runMergePi(wt, prompt, sessionName, config) {
128
150
  return this.runWithTransientRetry({
129
151
  ...this.loopPiOpts(wt, prompt, sessionName, "gate", false, config),
130
152
  signal: this.host.signal,
131
153
  }, { authoring: false });
132
154
  }
133
- /** The landing gate's reviewer runs (review.ts's review run and verdict follow-up): the
155
+ /** The merge gate's reviewer runs (review.ts's review run and verdict follow-up): the
134
156
  * same shared transient-retry wiring — a 429 in the gate waits its hint out and retries
135
157
  * once, exactly like an authoring run (BUGS.md 2026-10-01) — with one folding difference:
136
- * the FINAL run's usage is folded by the gate's caller (landing-core's foldUsage of
158
+ * the FINAL run's usage is folded by the gate's caller (merge-core's foldUsage of
137
159
  * gate.run / gate.followUpRun), so only a RETRIED first attempt is folded here. That fold
138
- * is not optional: it stamps the fleet-wide rate-limit hold's input before the wait, and
160
+ * is not optional: it stamps the team-wide rate-limit hold's input before the wait, and
139
161
  * it is the only record the failed attempt's spend would ever get.
140
162
  * Takes full PiRunOptions because the gate's two runs differ in everything but the loop
141
163
  * they charge (config, session dir and name, label, tool-call callbacks, signal).
@@ -144,7 +166,7 @@ export class LoopPi {
144
166
  return this.runWithTransientRetry(opts, { foldFinal: false, authoring: false });
145
167
  }
146
168
  /** The one bounded transient-failure retry shared by EVERY pi run this loop makes
147
- * (runRolePi, runLandingPi, and runGatePi): two transient failures of the world (not of
169
+ * (runRolePi, runMergePi, and runGatePi): two transient failures of the world (not of
148
170
  * the session) earn exactly one retry that continues the same session — the model server
149
171
  * timing out an idle predict stream, the provider severing that stream outright (undici's
150
172
  * bare "terminated", the stream-severed backend kind), the provider accepting a request
@@ -162,18 +184,49 @@ export class LoopPi {
162
184
  * Extracted verbatim from runRolePi so the rule lives in one place
163
185
  * (574a14c's loopPiOpts move was the wiring half of the same single-source-of-truth).
164
186
  */
187
+ /** Part 6c/7: demote a catalog-free opencode model the first time a run bills it. Observed
188
+ * here — the fold chokepoint every run of the loop passes through — because it is the one
189
+ * place that has the run's resolved model (opts.config) and its cost (the run) together, and
190
+ * it holds the loop's root and role for the cache and the event feed. pi and claude runs
191
+ * have no opencode catalog entry and no-op. Best-effort like observeClaudeAccount: an
192
+ * unwritable cache or event feed must not fail the run that observed it. */
193
+ observeFreeModelCost(run, config) {
194
+ if (config.agent !== "opencode")
195
+ return;
196
+ try {
197
+ observeOpencodeRun(this.host.root, { model: opencodeModelKey(config.model ?? ""), costUsd: run.costUsd }, { source: this.host.role });
198
+ }
199
+ catch {
200
+ // The next priced run demotes it; nothing this run depends on is lost.
201
+ }
202
+ }
165
203
  async runWithTransientRetry(opts,
166
204
  /** runGatePi only: skip folding the FINAL returned run (the caller folds it), while a
167
205
  * retried first attempt still folds — its spend and its rate-limit hold stamp must not
168
206
  * vanish just because the gate routes its own folding. Default folds everything, the
169
- * authoring and landing behavior. */
207
+ * authoring and merge behavior. */
170
208
  { foldFinal = true, authoring = true } = {}) {
171
209
  // The fold every attempt takes: an authoring run charges the loop's tick counters
172
- // (foldUsage); a landing, reviewer, or conflict-resolution run charges the same cost and
173
- // prompt-token totals but never the tick's pre-edit prefix (foldLandingUsage).
174
- const fold = (run) => authoring ? this.host.foldUsage(run) : this.host.foldLandingUsage(run);
210
+ // (foldUsage); a merge, reviewer, or conflict-resolution run charges the same cost and
211
+ // prompt-token totals but never the tick's pre-edit prefix (foldMergeUsage).
212
+ // The seam this run actually ran on, as the team hold keys it (seamHoldKey): the fold
213
+ // stamps the rate-limit and backend-failure observations with it, so a storm on a claude
214
+ // reviewer holds `claude` and a pi author on a different provider stays open. opts.config
215
+ // is always the run's own resolved seam (reviewRunConfig for the reviewer, the role's
216
+ // config or fallback pair for the author), so this names the backend that failed.
217
+ const provider = seamHoldKey(opts.config);
218
+ // The run's model pair name, the key of the per-step daily spend (LoopState.dayCostByPair).
219
+ const pair = seamPairName(opts.config);
220
+ const fold = (run) => authoring ? this.host.foldUsage(run, provider, pair) : this.host.foldMergeUsage(run, provider, pair);
221
+ // The run re-keys the calling tick's provider lease to the provider THIS run uses, if it is
222
+ // not already there (plans/provider-concurrency.md §5): a budget handback or a model-failure
223
+ // switch that moved this run onto another seam moves the tick's lease with it, so a
224
+ // claude-authored tick whose gate run lands on omlx is counted there. No-op when the tick
225
+ // carries no lease (a bare LoopPi unit test, `tumwater tick`).
226
+ await ensureProviderLease(opts.config);
175
227
  const pi = await runPi(opts);
176
228
  this.warnFallbackClone(pi);
229
+ this.observeFreeModelCost(pi, opts.config);
177
230
  if (!pi.aborted &&
178
231
  !pi.timedOut &&
179
232
  !pi.quietKilled &&
@@ -185,19 +238,19 @@ export class LoopPi {
185
238
  // A subscription claude usage-window rejection is not the 429 the retry's minute-scale
186
239
  // wait beats: the window resets hours out, so a within-run retry is guaranteed to fail
187
240
  // after burning up to RATE_LIMIT_RETRY_AFTER_CAP_S. The tick ends and the kept session
188
- // resumes once the fleet hold lifts (plans/agent-backends.md §8, part 6e).
241
+ // resumes once the team hold lifts (plans/agent-backends.md §8, part 6e).
189
242
  !subscriptionWindowRejected(pi)) {
190
243
  this.host.warn(pi.transientPiCrash
191
244
  ? `pi crashed on malformed JSON (${pi.errorMessage ?? "no detail"}) — resuming the session once`
192
245
  : pi.transientRateLimit
193
- ? `provider rate-limited the request (429${pi.retryAfterSeconds ? `, retry after ${pi.retryAfterSeconds}s` : `, no hint — waiting ${RATE_LIMIT_NO_HINT_RETRY_S}s`}) — retrying the pi run once`
246
+ ? `provider rate-limited the request (429${pi.retryAfterSeconds ? `, retry after ${pi.retryAfterSeconds}s` : `, no hint — waiting ${RATE_LIMIT_NO_HINT_RETRY_S}s`}) — retrying the agent run once`
194
247
  : pi.backendKind === "stream-severed"
195
- ? "provider severed the in-flight stream (terminated) — retrying the pi run once"
248
+ ? "provider severed the in-flight stream (terminated) — retrying the agent run once"
196
249
  : pi.backendKind === "timeout"
197
- ? "provider request timed out after accepting the connection — retrying the pi run once"
198
- : "model server timed out an idle predict stream (e.g. machine sleep) — retrying the pi run once");
250
+ ? "provider request timed out after accepting the connection — retrying the agent run once"
251
+ : "model server timed out an idle predict stream (e.g. machine sleep) — retrying the agent run once");
199
252
  // The failed attempt folds NOW, before the wait and the retry: a 429 it ended on is the
200
- // fleet-wide rate-limit hold's input (LoopRunner.lastRateLimit, stamped at fold time), and
253
+ // team-wide rate-limit hold's input (LoopRunner.lastRateLimit, stamped at fold time), and
201
254
  // folding after a retry that ran on for an hour would report the storm an hour late.
202
255
  fold(pi);
203
256
  // The pause is the rate-limit branch's, plus the timeout kind's: a server timeout or
@@ -224,6 +277,7 @@ export class LoopPi {
224
277
  // partial progress is not re-done. The next tick still starts fresh.
225
278
  const retry = await runPi({ ...opts, continueSession: true });
226
279
  this.warnFallbackClone(retry);
280
+ this.observeFreeModelCost(retry, opts.config);
227
281
  if (foldFinal)
228
282
  fold(retry);
229
283
  this.warnBackendKindFloor(retry);
@@ -237,8 +291,8 @@ export class LoopPi {
237
291
  /** The backend-kind floor (BUGS.md 2026-09-29): a failed run that ended on a
238
292
  * provider-backend failure (connection down, 5xx, model load) still warns — parity with
239
293
  * the 429 branch above, so any episode is visible from the feed alone instead of living
240
- * only in per-tick error events an operator must aggregate by hand. The fleet-wide hold
241
- * (src/fleet/fleet-hold.ts) and the storm alarms judge the spread; this line is the per-run
294
+ * only in per-tick error events an operator must aggregate by hand. The team-wide hold
295
+ * (src/team/team-hold.ts) and the storm alarms judge the spread; this line is the per-run
242
296
  * floor under them, not a retry: a dead backend is not something an immediate re-run beats.
243
297
  * Applied to whichever run is returned (BUGS.md 2026-10-09): the retry's returned run is a
244
298
  * second, independent run the retry predicate was never applied to, so a retry that dies
@@ -253,7 +307,7 @@ export class LoopPi {
253
307
  !run.quietKilled &&
254
308
  run.transientBackend &&
255
309
  !this.retryCoversBackendKind(run)) {
256
- this.host.warn(`provider backend failure (${backendKindPhrase(run.backendKind)}) — the transient retry does not cover this kind; the fleet-wide hold watches for a storm`);
310
+ this.host.warn(`provider backend failure (${backendKindPhrase(run.backendKind)}) — the transient retry does not cover this kind; the team-wide wait watches for a storm`);
257
311
  }
258
312
  }
259
313
  /** Whether the one bounded transient retry admits `run` on its backend-kind alone: the
@@ -271,13 +325,13 @@ export class LoopPi {
271
325
  warnFallbackClone(run) {
272
326
  if (!run.fallbackClone)
273
327
  return;
274
- this.host.warn(`pi could not resolve the model id exactly (${run.fallbackClone}) — the run uses the provider default's price and context window; add an exact entry to ~/.pi/agent/models.json`);
328
+ this.host.warn(`pi could not resolve the model id exactly (${run.fallbackClone}) — the run uses the provider default's price and context window; add an exact entry to ${piModelsPath()}`);
275
329
  }
276
330
  /** Run pi for this loop's authoring run in worktree `wt`: the shared per-loop wiring and
277
331
  * the transient-failure retry, with usage folded into the tick's counters. It is the ONE
278
332
  * entry point that carries the role notebook (PLANS.md "Role notebook": TUMWATER_NOTES_PATH,
279
- * read by the bundled role-notes extension): every other run (runRolePi's landing/conflict path,
280
- * runLandingPi, runGatePi) leaves it unset, and the director is excluded because its work is
333
+ * read by the `tumwater-notes` command): every other run (runRolePi's merge/conflict path,
334
+ * runMergePi, runGatePi) leaves it unset, and the director is excluded because its work is
281
335
  * the operator's prompt, not a recurring search.
282
336
  *
283
337
  * Every run starts a FRESH pi session: context never accumulates across ticks, so ticks start
@@ -290,22 +344,21 @@ export class LoopPi {
290
344
  }
291
345
  /** The notebook path an authoring run carries: the role's note file, or undefined for the
292
346
  * director, which has no notebook. The SUMMARY/stage-fix follow-ups share the authoring
293
- * tick's session and must carry the same notes wiring — claude re-passes `--mcp-config` on
294
- * every invocation because it keeps no flags across `--continue`, and pi reads
295
- * TUMWATER_NOTES_PATH from the environment. */
347
+ * tick's session and must carry the same notes wiring — every invocation reads the path
348
+ * from TUMWATER_NOTES_PATH in the environment, so the follow-up re-passes the same path. */
296
349
  authorNotesPath() {
297
350
  return this.host.role === DIRECTOR_ROLE
298
351
  ? undefined
299
352
  : roleNotesPath(this.host.root, this.host.role);
300
353
  }
301
- /** The shared authoring/conflict run wiring WITHOUT the role notebook. The landing merge's
354
+ /** The shared authoring/conflict run wiring WITHOUT the role notebook. The merge pipeline's
302
355
  * conflict resolver reaches pi through this (plans/merge-queue.md), and a conflict resolution
303
356
  * is not the role's recurring search: it must not carry the notes tool or rewrite the note.
304
357
  * Authoring ticks run through runAuthoringPi above, which is the only notebook-carrying path. */
305
358
  async runRolePi(wt, prompt, sessionName, resume = false, config) {
306
- // The refusal-note landing (loop.ts's merge) reaches pi here, in the ROLE's own worktree,
359
+ // The refusal-note merge (loop.ts's merge) reaches pi here, in the ROLE's own worktree,
307
360
  // so its runs demux as author — exactly the verdict the pre-part-1/5 cwd test gave them.
308
- // The shared-worktree conflict resolver goes through runLandingPi and demuxes as gate.
361
+ // The shared-worktree conflict resolver goes through runMergePi and demuxes as gate.
309
362
  return this.runWithTransientRetry(this.loopPiOpts(wt, prompt, sessionName, "author", resume, config));
310
363
  }
311
364
  /** Hard caps on the SUMMARY follow-up turn: it should take one short reply on a warm session,
@@ -331,7 +384,7 @@ export class LoopPi {
331
384
  * authoring run's budget: one short reply on a warm session. The run takes the loop's SHARED
332
385
  * transient-failure retry (BUGS.md 2026-10-01): a 429 on the follow-up turn waits its
333
386
  * Retry-After hint out and retries once on the same session, exactly like the authoring run,
334
- * the landing slot's run, and the gate's reviewer runs it mirrors. The caps bound BOTH
387
+ * the merge slot's run, and the gate's reviewer runs it mirrors. The caps bound BOTH
335
388
  * attempts (the retry inherits this capped config), so a flaky provider cannot stretch the
336
389
  * follow-up past its budget by more than one capped turn. The wiring folds every attempt
337
390
  * (the failed 429 before the wait), and the returned final run is folded here by the wiring
@@ -341,12 +394,13 @@ export class LoopPi {
341
394
  const dir = sessionDir(this.host.root, this.host.role);
342
395
  // The follow-up continues THIS tick's author session. The backend's own resumable rule
343
396
  // keeps it honest: pi would reach the newest file, claude its recorded id, so a conflict
344
- // resolver's gate run in the same dir cannot be mistaken for this tick's session.
345
- if (!agentSessionResumable(dir, { kind: "author", agent: cfg.agent ?? "pi" })) {
397
+ // resolver's gate run in the same dir cannot be mistaken for this tick's session. The
398
+ // claude config dir rides along so a claude team's resume looks under its own dir.
399
+ if (!agentSessionResumable(dir, { kind: "author", agent: cfg.agent ?? "pi" }, resolvedAgentConfig(cfg, "claude").configDir)) {
346
400
  return null;
347
401
  }
348
402
  return this.runWithTransientRetry({
349
- ...this.loopPiOpts(wt, prompt, `tumwater-${this.host.role}-${this.host.tickNumber()}-${suffix}`, "author", true, undefined, this.authorNotesPath()),
403
+ ...this.loopPiOpts(wt, prompt, `tumwater-${this.host.role}-${this.host.tickNumber()}-${suffix}`, "author", true, cfg, this.authorNotesPath()),
350
404
  config: {
351
405
  ...cfg,
352
406
  ...cappedRequestTimeouts(cfg, timeoutS, LoopPi.SUMMARY_REQUEST_QUIET_S),
@@ -357,8 +411,8 @@ export class LoopPi {
357
411
  * bounded turn, folded into the tick's usage like every other run. Null when there is no
358
412
  * session to continue (pi never wrote one) — the caller then derives a subject itself. The
359
413
  * run is returned even when it failed so the caller can honor a shutdown abort. */
360
- async requestSummary(wt, config) {
361
- return this.requestFollowUp(wt, buildSummaryRequestPrompt(), "summary", LoopPi.SUMMARY_REQUEST_TIMEOUT_S, config);
414
+ async requestSummary(wt, config, overLength) {
415
+ return this.requestFollowUp(wt, buildSummaryRequestPrompt(overLength), "summary", LoopPi.SUMMARY_REQUEST_TIMEOUT_S, config);
362
416
  }
363
417
  /** Ask the tick's own pi session (--continue) to fix the pre-queue self-check's findings:
364
418
  * one bounded turn, folded into the tick's usage like every other run. Null when there is no
@@ -1,6 +1,6 @@
1
1
  import { setRef } from "../git/git.js";
2
2
  import { resetWorktreeToMain } from "../git/worktree.js";
3
- import { landingRefName } from "../paths.js";
3
+ import { mergeRefName } from "../paths.js";
4
4
  import { shortSha } from "../text/format.js";
5
5
  /** Finish a tick whose pi run was killed mid-flight — shared by the author-run and review-
6
6
  * gate abort branches, which have identical semantics; only what the kill left behind
@@ -33,38 +33,38 @@ export async function finishAbortedTickPhase(ctx, userPrompt, wt, userAborted, h
33
33
  // prompt says the run was moved back to the primary model, not that the harness restarted.
34
34
  return handedBack ? { result: "aborted", resumeCause: "budget-resumed" } : { result: "aborted" };
35
35
  }
36
- /** Pin `sha` by this role's landing ref and free the worktree (plans/merge-queue.md invariant
36
+ /** Pin `sha` by this role's merge ref and free the worktree (plans/merge-queue.md invariant
37
37
  * 4): the pin must exist BEFORE resetWorktreeToMain moves the branch, or the commit is
38
38
  * orphaned. Returns false when the pin write failed — the caller then leaves the commit on its
39
39
  * branch (no reset) and defers to next-tick recovery, which adopts it into the pin scheme.
40
- * After a successful pin the review gate and the rebase run in the vet's leased pool slot —
41
- * never in this worktree (merge queue 2/5). */
40
+ * After a successful pin the review gate and the rebase run in the verification's leased pool
41
+ * slot, never in this worktree (merge queue 2/5). */
42
42
  export async function pinAndResetPhase(ctx, wt, sha) {
43
- const pinned = await setRef(ctx.root, landingRefName(ctx.role), sha);
43
+ const pinned = await setRef(ctx.root, mergeRefName(ctx.role), sha);
44
44
  if (!pinned) {
45
- ctx.warn(`failed to pin ${shortSha(sha)} by its landing ref — leaving the commit on the branch for next-tick recovery`);
45
+ ctx.warn(`failed to pin ${shortSha(sha)} by its merge ref — leaving the commit on the branch for the next run's recovery`);
46
46
  return false;
47
47
  }
48
48
  await resetWorktreeToMain(wt, ctx.mainBranch);
49
49
  return true;
50
50
  }
51
51
  /** End a tick whose leftover recovery found work to salvage (src/loop/leftover.ts) without an
52
- * authoring run — the leftover owns the role's one landing ref until its landing resolves. A
52
+ * authoring run — the leftover owns the role's one merge ref until its merge resolves. A
53
53
  * tick's dequeued user prompt goes back to its queue, since nothing ran it. A pin put on
54
- * the land queue ends the tick `queued` exactly like a fresh changed tick (the land-queue
55
- * interlock then holds the role until the slot lands it) and frees the worktree, the pin now
56
- * holding the commit. When the pin is left over from a landing that failed non-terminally
57
- * (`priorLandingFailure`), re-queuing it is a retry of a PERSISTENT failure the `queued` result
54
+ * the merge queue ends the tick `queued` exactly like a fresh changed tick (the merge-queue
55
+ * interlock then holds the role until the slot merges it) and frees the worktree, the pin now
56
+ * holding the commit. When the pin is left over from a merge that failed non-terminally
57
+ * (`priorMergeFailure`), re-queuing it is a retry of a PERSISTENT failure the `queued` result
58
58
  * cannot express, so it rides the outcome's recoveryFailure into the error streak and the
59
- * warning that names the stuck gate (BUGS.md 2026-09-21). A role whose landing is still queued
59
+ * warning that names the stuck gate (BUGS.md 2026-09-21). A role whose merge is still queued
60
60
  * ends `queued` on that entry — nothing enqueued twice, nothing reset. A leftover that could
61
61
  * not be pinned stays on the branch and the tick fails like a fresh tick's failed pin. Every
62
62
  * arm is `recoveredLeftover`: no model ran, so the tick is no evidence about the backend. */
63
- export async function finishRecoveryTickPhase(ctx, recovered, userPrompt, wt, priorLandingFailure) {
63
+ export async function finishRecoveryTickPhase(ctx, recovered, userPrompt, wt, priorMergeFailure) {
64
64
  ctx.pending.clear();
65
65
  ctx.pending.requeueUnfulfilled(userPrompt);
66
66
  if (recovered.kind === "unpinned") {
67
- ctx.state.lastError = `failed to pin leftover ${shortSha(recovered.sha)} by its landing ref; left for next-tick recovery`;
67
+ ctx.state.lastError = `failed to pin leftover ${shortSha(recovered.sha)} by its merge ref; left for the next run's recovery`;
68
68
  return { result: "error", summary: ctx.state.lastError, recoveredLeftover: true };
69
69
  }
70
70
  if (recovered.kind === "held") {
@@ -77,7 +77,7 @@ export async function finishRecoveryTickPhase(ctx, recovered, userPrompt, wt, pr
77
77
  }
78
78
  const { entry } = recovered;
79
79
  if (recovered.kind === "enqueued") {
80
- ctx.setRecoveryFailure(priorLandingFailure);
80
+ ctx.setRecoveryFailure(priorMergeFailure);
81
81
  await resetWorktreeToMain(wt, ctx.mainBranch);
82
82
  }
83
83
  return {