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,14 +1,32 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import { loadConfigSafe } from "../config/config.js";
4
+ import { CUSTOM_NAME_RE } from "../config/config-schema.js";
5
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
3
6
  import { loopEnabled } from "../roles/loop-ids.js";
4
- import { branchExists, currentBranch, deleteRef, isDirty, isMergedInto, refSha, targetBranch, } from "../git/git.js";
7
+ import { branchExists, countCommitsInRange, currentBranch, deleteRef, isDirty, isMergedInto, refSha, targetBranch, } from "../git/git.js";
5
8
  import { errorMessage } from "../text/text.js";
6
9
  import { loadLoopState } from "../loop/loop-state.js";
7
- import { branchName, landingRefName, worktreePath } from "../paths.js";
10
+ import { branchName, mergeRefName, legacyLandingRefName, worktreePath } from "../paths.js";
8
11
  import { isUsableWorktree, removeWorktree } from "../git/worktree.js";
9
12
  import { roleWorktreeDir, slotForDir, updateSlotsState } from "../git/slots-state.js";
10
- import { resumeRole } from "../fleet/fleet-state.js";
13
+ import { pausedRoles, resumeRole } from "../team/team-state.js";
11
14
  import { git } from "../git/git-run.js";
15
+ /** `tumwater retire --role <id>`: remove a disabled loop's persistent worktree and branch, plus the
16
+ * per-role merge ref and paused-state marker that outlive it. The safety rails and the `--force`
17
+ * override live here, so the CLI layer (operator/operator-commands.ts) stays a thin renderer. */
18
+ /** The role's merge pin, falling back to the legacy-named one a not-yet-migrated repo holds. */
19
+ async function mergeRefSha(root, role) {
20
+ return (await refSha(root, mergeRefName(role))) ?? (await refSha(root, legacyLandingRefName(role)));
21
+ }
22
+ /** The id gate for ids no config names (a lowered `instances`, a removed custom role): the id is
23
+ * well-formed (CUSTOM_NAME_RE keeps `..` and `/` out of the paths retire builds), is not the
24
+ * director, and still has its branch or merge ref. Read-only. */
25
+ export async function hasRetirableLeftovers(root, role) {
26
+ if (!CUSTOM_NAME_RE.test(role) || role === DIRECTOR_ROLE)
27
+ return false;
28
+ return (await branchExists(root, branchName(role))) || (await mergeRefSha(root, role)) !== null;
29
+ }
12
30
  /** Collect what exists today for `role`, with every probe tolerant of partial state — a retired
13
31
  * role's leftovers are exactly the messy half-state this command exists to clean up. */
14
32
  export async function collectRetire(root, role) {
@@ -25,51 +43,51 @@ export async function collectRetire(root, role) {
25
43
  throw new Error(`cannot read tumwater.json: ${errorMessage(config.error)}`);
26
44
  }
27
45
  const mainBranch = targetBranch(config.config.baseBranch, await currentBranch(root));
28
- // Count unlanded work from the branch ref at root, not from the worktree's HEAD: the worktree
29
- // may be gone or unusable (the messy half-state this command exists to clean up) while the
30
- // branch still holds commits — reading HEAD through the worktree would report 0 and let
31
- // retireRole's `branch -D` delete unlanded work without objection. When the branch is
32
- // absent, or a usable worktree sits on it, the rev-list range is empty or matches HEAD.
46
+ // Count unmerged work from the branch ref at root, not from the worktree's HEAD: the worktree may
47
+ // be gone or unusable (the messy half-state this command exists to clean up) while the branch
48
+ // still holds commits — reading HEAD through the worktree would report 0 and let retireRole's
49
+ // `branch -D` delete unmerged work without objection. When the branch is absent, or a usable
50
+ // worktree sits on it, the rev-list range is empty or matches HEAD.
33
51
  const branch = branchName(role);
34
52
  const branchThere = await branchExists(root, branch);
35
53
  const ahead = branchThere
36
- ? Number.parseInt(await git(root, "rev-list", "--count", `${mainBranch}..${branch}`), 10)
54
+ ? await countCommitsInRange(root, `${mainBranch}..${branch}`)
37
55
  : 0;
38
56
  const dirty = usable && wt !== null ? await isDirty(wt) : false;
39
- const landing = await refSha(root, landingRefName(role));
40
- // A landing ref pins a committed-but-unlanded sha (plans/merge-queue.md invariant 4): count
41
- // it as unlanded work too — the branch can sit at main while the pin holds the crash survivor.
42
- const pinnedUnlanded = landing !== null && !(await isMergedInto(root, landing, mainBranch)) ? 1 : 0;
57
+ const mergeRef = await mergeRefSha(root, role);
58
+ // A merge ref pins a committed-but-unmerged sha (plans/merge-queue.md invariant 4): count it as
59
+ // unmerged work too — the branch can sit at main while the pin holds the crash survivor.
60
+ const pinnedUnmerged = mergeRef !== null && !(await isMergedInto(root, mergeRef, mainBranch)) ? 1 : 0;
43
61
  return {
44
62
  role,
45
63
  worktreePresent: present,
46
64
  worktreeUsable: usable,
47
65
  branchPresent: branchThere,
48
- landingRefPresent: landing !== null,
49
- aheadOfMain: ahead + pinnedUnlanded,
66
+ mergeRefPresent: mergeRef !== null,
67
+ aheadOfMain: ahead + pinnedUnmerged,
50
68
  dirty,
51
69
  enabledInConfig: loopEnabled(config.config, role),
52
- midTick: loadLoopState(root, role).running === true,
70
+ midRun: loadLoopState(root, role).running === true,
53
71
  };
54
72
  }
55
73
  /** The artifacts retire manages, in removal order. */
56
- const ARTIFACTS = ["worktree", "branch", "landingRef", "pausedMarker"];
74
+ const ARTIFACTS = ["worktree", "branch", "mergeRef", "pausedMarker"];
57
75
  /** The safety rails: each returns a one-line objection, or null when the state is clean. */
58
76
  function objections(status) {
59
77
  const reasons = [];
60
78
  if (status.enabledInConfig)
61
79
  reasons.push(`role ${status.role} is still enabled in tumwater.json — disable it first`);
62
80
  if (status.aheadOfMain > 0)
63
- reasons.push(`branch ${branchName(status.role)} holds ${status.aheadOfMain} unlanded commit(s) — land or discard them first`);
81
+ reasons.push(`branch ${branchName(status.role)} holds ${status.aheadOfMain} unmerged commit(s) — merge or discard them first`);
64
82
  if (status.worktreePresent && !status.worktreeUsable)
65
83
  reasons.push("the worktree is unusable and cannot be checked for uncommitted changes — remove it manually or use --force");
66
84
  if (status.dirty)
67
85
  reasons.push("the worktree has uncommitted changes");
68
- if (status.midTick)
69
- reasons.push("a tick is in flight for this loop");
86
+ if (status.midRun)
87
+ reasons.push("a run is in progress for this role");
70
88
  return reasons;
71
89
  }
72
- /** Remove the role's worktree, branch, landing ref, and paused-state marker. Refuses — removing
90
+ /** Remove the role's worktree, branch, merge ref, and paused-state marker. Refuses — removing
73
91
  * nothing — when any safety rail objects, unless `force` overrides every objection. */
74
92
  export async function retireRole(root, role, { force }) {
75
93
  const status = await collectRetire(root, role);
@@ -117,15 +135,32 @@ export async function retireRole(root, role, { force }) {
117
135
  }
118
136
  else
119
137
  mark("branch", false);
120
- if (status.landingRefPresent) {
121
- await deleteRef(root, landingRefName(status.role));
122
- mark("landingRef", true);
138
+ if (status.mergeRefPresent) {
139
+ await deleteRef(root, mergeRefName(status.role));
140
+ await deleteRef(root, legacyLandingRefName(status.role)); // a team stopped before its first start here
141
+ mark("mergeRef", true);
123
142
  }
124
143
  else
125
- mark("landingRef", false);
144
+ mark("mergeRef", false);
126
145
  mark("pausedMarker", resumeRole(root, status.role));
127
146
  return { status, removed, skipped };
128
147
  }
148
+ /** Preview `retireRole` without touching the worktree, refs, slot state, or paused marker. */
149
+ export async function previewRetire(root, role) {
150
+ const status = await collectRetire(root, role);
151
+ const there = {
152
+ worktree: status.worktreePresent,
153
+ branch: status.branchPresent,
154
+ mergeRef: status.mergeRefPresent,
155
+ pausedMarker: pausedRoles(root).includes(role),
156
+ };
157
+ return {
158
+ status,
159
+ wouldRemove: ARTIFACTS.filter((a) => there[a]),
160
+ skipped: ARTIFACTS.filter((a) => !there[a]),
161
+ objections: objections(status),
162
+ };
163
+ }
129
164
  /** The one-line human description of an artifact name, for the CLI renderer. */
130
165
  export function artifactPhrase(name, role) {
131
166
  switch (name) {
@@ -135,8 +170,8 @@ export function artifactPhrase(name, role) {
135
170
  return `the worktree`;
136
171
  case "branch":
137
172
  return `the branch (${branchName(role)})`;
138
- case "landingRef":
139
- return `the landing ref (${landingRefName(role)})`;
173
+ case "mergeRef":
174
+ return `the merge ref (${mergeRefName(role)})`;
140
175
  case "pausedMarker":
141
176
  return `the paused-state marker`;
142
177
  default:
@@ -0,0 +1,71 @@
1
+ // vocabulary: plain
2
+ import { loadConfigSafe } from "../config/config.js";
3
+ import { withStateLockAsync } from "../concurrency/lock.js";
4
+ import { currentBranch, headOf, isMergedInto, targetBranch } from "../git/git.js";
5
+ import { COMMIT_IDENT, gitTry } from "../git/git-run.js";
6
+ import { stampedSubject } from "../git/commit-message.js";
7
+ import { primaryWorktreeLockPath } from "../paths.js";
8
+ import { shortSha } from "../text/format.js";
9
+ /** `tumwater revert <sha>`: undo one merged change on main, committing the inverse as
10
+ * `tumwater(operator): Revert <short sha>` on the primary checkout. Mirrors
11
+ * `commitOperatorBacklogEdit`'s lock-and-branch discipline (src/backlog/backlog-commit.ts): the
12
+ * inverse commit lands under the primary-worktree lock so it cannot interleave with a
13
+ * fast-forward, and it refuses — writing nothing — when the primary is not on the base branch.
14
+ * Fail-closed: an unknown ref, a ref not on the base branch, or a revert that conflicts writes
15
+ * no commit and leaves the checkout as it was. */
16
+ /** Revert one already-merged commit (`ref` resolved to a commit) and commit the inverse on the
17
+ * primary checkout under the primary-worktree lock. Never throws; every refusal and every git
18
+ * failure comes back as `{ ok: false, error }` after restoring the checkout. `head` is captured
19
+ * before the revert so a future caller can compare; the resolved full sha is what the commit
20
+ * message shortens. */
21
+ export async function revertMergedCommit(root, ref) {
22
+ try {
23
+ // Yield first: a synchronous caller (the TUI key handler) may write again right after this
24
+ // call, and that write takes the same lock synchronously — a lock already held across our
25
+ // awaits would block its event loop until the lock times out.
26
+ await new Promise((resolve) => setImmediate(resolve));
27
+ return await withStateLockAsync(primaryWorktreeLockPath(root), async () => {
28
+ const primary = await currentBranch(root);
29
+ const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
30
+ if (primary === null || base !== primary)
31
+ return { ok: false, error: `the primary checkout is not on ${base} — run from a checkout of it` };
32
+ const full = await gitTry(root, "rev-parse", "--verify", ref + "^{commit}");
33
+ if (full === null)
34
+ return { ok: false, error: `no such commit: ${ref}` };
35
+ // `head` is captured before the revert so a future caller can compare before/after.
36
+ const head = await headOf(root, "HEAD");
37
+ if (!(await isMergedInto(root, full, primary)))
38
+ return { ok: false, error: `${shortSha(full)} is not on ${primary}` };
39
+ // Fail-closed on any staged or unstaged change to a tracked path: we refuse to revert a
40
+ // checkout that has one (untracked files are tolerated — the lock dir we just created is
41
+ // one). This keeps the inverse commit free of unrelated content and, just as important,
42
+ // makes `git revert --abort` below safe to run after a conflict — with nothing dirty in
43
+ // the index or tree there is nothing for its hard reset to discard, so the operand's
44
+ // pre-existing work can never be lost.
45
+ if ((await gitTry(root, "status", "--porcelain", "--untracked-files=no")) !== "")
46
+ return { ok: false, error: "the worktree has staged or unstaged changes; commit or stash them first, nothing was committed" };
47
+ if ((await gitTry(root, "revert", "--no-commit", full)) === null) {
48
+ // The tree was clean when the revert started, so --abort restores exactly that state.
49
+ await gitTry(root, "revert", "--abort");
50
+ return { ok: false, error: `reverting ${shortSha(full)} conflicted; nothing was committed and the checkout is unchanged` };
51
+ }
52
+ const commitMsg = stampedSubject("operator", "Revert " + shortSha(full)); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
53
+ // Mirror commitOperatorBacklogEdit: `--only` with the revert's own staged paths commits
54
+ // just the inverse, so it cannot pick up any unrelated change of another path staged
55
+ // after the clean check above. `--no-renames` keeps a rename's two halves as separate
56
+ // path lines: the default name-only collapses a detected rename to its destination, which
57
+ // would commit only half and leave the other half staged with a dirty tree.
58
+ const pathsSpec = ((await gitTry(root, "diff", "--cached", "--name-only", "--no-renames")) ?? "")
59
+ .split("\n")
60
+ .filter((p) => p.length > 0);
61
+ if ((await gitTry(root, ...COMMIT_IDENT, "commit", "--only", "-m", commitMsg, "--", ...pathsSpec)) === null) {
62
+ await gitTry(root, "revert", "--abort");
63
+ return { ok: false, error: `the revert of ${shortSha(full)} did not commit; nothing was committed` };
64
+ }
65
+ return { ok: true, headSha: await headOf(root, "HEAD"), beforeSha: head };
66
+ });
67
+ }
68
+ catch {
69
+ return { ok: false, error: "the revert did not complete (git failed); nothing was committed" };
70
+ }
71
+ }
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** The orchestrator's launch pass: the half of the per-poll runner sweep that admits due
2
3
  * roles to their ticks. The decision half (isEligible, the pause/backoff/deferral gates,
3
4
  * once-mode settling) stays in runOrchestrator (src/orchestrator/orchestrator.ts) — it reads
@@ -12,21 +13,28 @@
12
13
  * in and mutated in place, exactly as the inline version did: the drain's `finally` and the
13
14
  * redeployer read it between polls. */
14
15
  import { abandonFallbackProbeAt, recordFallbackTickAt, startFallbackProbeAt, } from "../budget/fallback-breaker.js";
15
- import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
16
+ import { seamHoldKey } from "../config/config-views.js";
17
+ import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
16
18
  import { errorMessage } from "../text/text.js";
17
19
  import { fairOrder } from "../scheduling/scheduling.js";
18
20
  import { modelPairName } from "../budget/budget.js";
21
+ import { withProviderLease } from "../concurrency/provider-lease.js";
22
+ import { seamConcurrencyKey } from "../config/config-views.js";
19
23
  import { DIRECTOR_ROLE, roleTier } from "../roles/roles.js";
20
24
  import { runTimedRoleTick } from "../tick/tick-timing.js";
21
25
  /** How many recent work-bearing tick durations the p75 sample keeps (drainInFlightWork's
22
- * HANDOFF_LANDING_WINDOW_MS input): the drain window tracks recent tick pace, not all-time
26
+ * HANDOFF_MERGE_WINDOW_MS input): the drain window tracks recent tick pace, not all-time
23
27
  * pace, so the sample is a ring buffer. */
24
28
  export const ROLE_TICK_DURATION_SAMPLES = 50;
29
+ /** The admission wait threshold below which a role tick's semaphore wait logs no event — a
30
+ * tick that starts at once stays silent. Mirrors SLOT_WAIT_EVENT_MS in src/git/worktree-pool.ts:
31
+ * the wait that signals maxConcurrentPerProvider (or maxConcurrent) caps the team. */
32
+ export const PROVIDER_WAIT_EVENT_MS = 30_000;
25
33
  /** Launch one due runner's tick: reserve it, park it in the semaphore, and start it under
26
34
  * runTimedRoleTick with the breaker-evidence wiring. Returns the task promise already added
27
35
  * to its in-flight bucket (its `finally` self-removes and folds the duration sample). */
28
36
  export function launchDueTicks(ctx) {
29
- const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, semaphore, rolePermitHolders, roleInFlight, directorInFlight, roleTickDurationsMs, startHeld, } = ctx;
37
+ const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, semaphore, piDefault, rolePermitHolders, roleInFlight, directorInFlight, roleTickDurationsMs, startHeld, } = ctx;
30
38
  let admittedProbe = false;
31
39
  for (const runner of fairOrder([...reasons.keys()])) {
32
40
  if (signal.aborted)
@@ -47,7 +55,7 @@ export function launchDueTicks(ctx) {
47
55
  }
48
56
  const reason = reasons.get(runner);
49
57
  if (reason && reason !== "scheduled" && reason !== "startup") {
50
- logEventBestEffort(root, { loop: runner.role, type: "wake", reason });
58
+ logEventBestEffort(root, { source: runner.role, type: "wake", reason });
51
59
  }
52
60
  runner.state.running = true; // Reserve before the semaphore wait so we don't double-schedule.
53
61
  // The director never queues behind role loops: a user prompt starts immediately,
@@ -58,8 +66,13 @@ export function launchDueTicks(ctx) {
58
66
  // Mark the parked waiter while it waits: it holds no permit yet, so the dashboards
59
67
  // render `awaiting slot` (an inactive state) and the active rows keep tracking
60
68
  // maxConcurrent (BUGS.md 2026-09-24). Cleared the moment the permit is granted. The
61
- // director never queues, so it is never a parked waiter.
69
+ // director never queues, so it is never a parked waiter. Its provider key rides beside
70
+ // `parkedSince` (provider-concurrency part 5/5): the key the admission lease will
71
+ // acquire, computed from the same runConfig admission uses, so a waiter named by a
72
+ // full key renders `awaiting <key> slot` on the dashboards.
73
+ const parkKey = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
62
74
  runner.state.parkedSince = usesSlot ? Date.now() : undefined;
75
+ runner.state.parkedOn = usesSlot && parkKey ? seamConcurrencyKey(parkKey, piDefault) : undefined;
63
76
  // The tick's own run time (null when it never ran, was cut off, or ended without
64
77
  // work): the drain-window sample is taken only for a work-bearing tick that finished
65
78
  // on its own (runTimedRoleTick; BUGS.md 2026-09-30).
@@ -67,22 +80,61 @@ export function launchDueTicks(ctx) {
67
80
  // Whether runner.tick() was ever called — false when the start gate (or a shutdown)
68
81
  // turned the tick away at its permit.
69
82
  let started = false;
83
+ // The tick's keyed semaphore lease (plans/provider-concurrency.md §4): acquired on the
84
+ // authoring seam's provider key and released in place of the bare team release, so the
85
+ // cap is real for role ticks from part 2/5 on. The director never queues, so its lease is
86
+ // never created here.
87
+ let lease = null;
88
+ // The director's forced provider lease (plans/provider-concurrency.md §4): never queues and
89
+ // never counts toward maxConcurrent, but DOES count toward its provider key while its tick
90
+ // runs — so a full provider admits no further grants until the director releases.
91
+ let directorLease = null;
70
92
  const task = (async () => {
71
93
  durationMs = await runTimedRoleTick(signal, usesSlot
72
94
  ? async () => {
73
- await semaphore.acquire(roleTier(runner.role));
95
+ // The key is the authoring seam's concurrency key, computed here at admission
96
+ // (runConfig applies any budget or model-failure fallback already in force).
97
+ const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
98
+ const key = cfg ? seamConcurrencyKey(cfg, piDefault) : undefined;
99
+ const admittedAt = Date.now();
100
+ lease = await semaphore.acquireLease(roleTier(runner.role), key);
101
+ // A wait of PROVIDER_WAIT_EVENT_MS or more for the provider/team slot is
102
+ // provider contention worth surfacing: name it so `tumwater doctor`'s provider
103
+ // contention row can sum it and the operator can size maxConcurrentPerProvider
104
+ // (or maxConcurrent) from real waits. Best-effort like the launch pass's other
105
+ // events, so an unwritable log never blocks a start; a tick admitted at once
106
+ // stays silent.
107
+ if (Date.now() - admittedAt >= (ctx.providerWaitMs ?? PROVIDER_WAIT_EVENT_MS)) {
108
+ logEventBestEffort(ctx.root, {
109
+ source: TUMWATER_SOURCE,
110
+ type: "provider_wait",
111
+ role: runner.role,
112
+ waitedMs: Date.now() - admittedAt,
113
+ ...(key !== undefined ? { key } : {}),
114
+ });
115
+ }
74
116
  // Permit granted: the tick is now an active, permit-holding state until the
75
117
  // release below. A tick the start gate turns away releases within the same
76
118
  // microtask chain, so no poll ever counts it as a permit holder.
77
119
  runner.state.parkedSince = undefined;
120
+ runner.state.parkedOn = undefined;
78
121
  rolePermitHolders.add(runner);
79
122
  }
80
- : async () => { }, usesSlot
123
+ : async () => {
124
+ // The director wraps its tick in a forced lease on its run's provider key, never
125
+ // queuing behind role loops: a user prompt starts at once even on a full provider,
126
+ // and the key's in-use rises by one — overshooting by one — so no further grant on
127
+ // that key happens until its release drains it back under the cap.
128
+ const cfg = runner.runConfig(Date.now());
129
+ directorLease = semaphore.forceLease(cfg ? seamConcurrencyKey(cfg, piDefault) : undefined);
130
+ }, usesSlot
81
131
  ? () => {
82
132
  rolePermitHolders.delete(runner);
83
- semaphore.release();
133
+ lease.release();
84
134
  }
85
- : () => { }, async () => {
135
+ : () => {
136
+ directorLease.release();
137
+ }, async () => {
86
138
  started = true;
87
139
  // A role tick that starts while the budget holds runs on the fallback pair its tier
88
140
  // resolved to (the gate's per-tier view was installed in its config), or — when a
@@ -94,11 +146,19 @@ export function launchDueTicks(ctx) {
94
146
  // part 1/2").
95
147
  const now = Date.now();
96
148
  const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(now);
149
+ // The breaker is keyed by the seam that ran (seamHoldKey), like the tick pair and
150
+ // the team-hold stamps (PLANS.md "Mixed claude + pi fleets, part 2/4"): a claude
151
+ // fallback tick demotes the `claude/<model>` breaker entry, never `?/…`.
97
152
  const ranPair = budgetActive && cfg?.model !== undefined
98
- ? modelPairName({ provider: cfg.provider, model: cfg.model })
153
+ ? modelPairName({ provider: seamHoldKey(cfg), model: cfg.model })
99
154
  : null;
100
155
  const ranOn = ranPair !== null ? gateStates.budget.breakers[ranPair] : undefined;
101
- const outcome = await runner.tick();
156
+ // The tick's lease rides in the provider-lease store for the length of the tick, so
157
+ // every run it makes re-keys it to the provider that run actually uses
158
+ // (plans/provider-concurrency.md §5) — a budget or model-failure switch that moves a
159
+ // mid-tick run to another provider moves the lease with it. The director's forced
160
+ // lease re-keys the same way, and its rekey never waits.
161
+ const outcome = await withProviderLease(runner.role === DIRECTOR_ROLE ? directorLease : lease, semaphore, () => runner.tick(), { piDefault });
102
162
  if (ranPair !== null && ranOn !== undefined) {
103
163
  const at = breakerNow();
104
164
  const evidence = outcome.recoveredLeftover ? "skipped" : outcome.result;
@@ -123,7 +183,7 @@ export function launchDueTicks(ctx) {
123
183
  // A reservation whose tick never started hands itself back, so the role re-schedules
124
184
  // once the hold lifts (or on the new build) instead of sitting `running` forever with
125
185
  // nothing in flight. Memory only, on purpose: nothing started, so no state write and
126
- // no tick_start/tick_end — the persisted state still reads exactly as the last real
186
+ // no run_start/run_end — the persisted state still reads exactly as the last real
127
187
  // tick left it, and the next generation schedules the role from that.
128
188
  if (!started)
129
189
  runner.state.running = false;
@@ -138,14 +198,14 @@ export function launchDueTicks(ctx) {
138
198
  // should resolve. A throw outside that fold (a finalize-time file write, a runner bug)
139
199
  // rejects it instead; the drain awaits the bucket with allSettled, but chaining cleanup
140
200
  // off a bare `void task.finally(...)` would leave the derived promise's rejection
141
- // unhandled and take the whole fleet down. Surface it as a warning (warnEventBestEffort,
201
+ // unhandled and take the whole team down. Surface it as a warning (warnEventBestEffort,
142
202
  // which keeps a failed warning write from becoming that unhandled rejection) and hand the
143
203
  // reservation back so the role can schedule again, exactly as the unstarted path above
144
204
  // does; the drain's allSettled is otherwise silent about the failure.
145
205
  void task
146
206
  .catch((err) => {
147
207
  runner.state.running = false;
148
- warnEventBestEffort(root, runner.role, `tick task rejected: ${errorMessage(err)}`);
208
+ warnEventBestEffort(root, runner.role, `run task rejected: ${errorMessage(err)}`);
149
209
  })
150
210
  .finally(() => {
151
211
  bucket.delete(task);