tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -3,8 +3,9 @@ import { fail, say, sayJson } from "../cli/cli-output.js";
3
3
  import { failOverDurationCap, flagValue, parseCountFlag, parseDurationFlag, parseRoleFlag } from "../cli/cli-args.js";
4
4
  import { errorMessage, splitCommaList } from "../text/text.js";
5
5
  import { PAUSE_FOR_OVERFLOW_HINT, REASON_VALUE_ERROR, ROLE_VALUE_ERROR } from "../cli/cli-flag-specs.js";
6
- import { artifactPhrase, hasRetirableLeftovers, previewRetire, retireRole } from "./retire.js";
7
- import { previewRevert, revertFollowUp, revertMergedCommit, runRevertSha } from "./revert.js";
6
+ import { artifactPhrase, hasRetirableLeftovers, previewRetire, retireRole } from "./operator-retire.js";
7
+ import { previewRevert, revertFollowUp, revertMergedCommit, runRevertSha } from "./operator-revert.js";
8
+ import { distinctOrdered } from "../collections.js";
8
9
  import { runNotFoundMessage } from "../run/run-detail.js";
9
10
  import { readRunDetail } from "../run/run-detail-data.js";
10
11
  import { readRunRows } from "../history/history-data.js";
@@ -15,9 +16,9 @@ import { shortSha } from "../text/format.js";
15
16
  import { INSTANCE_ROLES } from "../roles/runner-ids.js";
16
17
  import { allRoleIds, unknownRoleMessage } from "../roles/roles.js";
17
18
  import { normalizePauseReason, pauseTeam, pauseRole, pausedRoles, resumeTeam, resumeRole, } from "../team/team-state.js";
18
- import { readOrchestratorInfo } from "../team/orchestrator-info.js";
19
+ import { readOrchestratorInfo } from "../team/team-orchestrator-info.js";
19
20
  import { pidAlive } from "../process/process.js";
20
- import { inspectReclaimCandidates } from "../team/reclaim.js";
21
+ import { inspectReclaimCandidates } from "../team/team-reclaim.js";
21
22
  import { markerApplyNote, NO_HARNESS_ERROR, PAUSE_FOR_MAX_MS, cancelScheduledStopMessage, cancelWakes, listWakes, renderWakeList, requestAbort, requestAbortAll, requestResetCounters, requestRestart, requestScheduledStop, requestWake, rolePauseMessage, roleResumeMessage, runManualReclaim, timedPauseBits, } from "./operator-intent.js";
22
23
  /** The CLI layer of the operator commands: the `reset-counters`, `wake`, `reclaim`, `abort`,
23
24
  * `pause`, `resume`, `stop`, and `retire` commands, split out of cli.ts so the command bodies
@@ -27,7 +28,7 @@ import { markerApplyNote, NO_HARNESS_ERROR, PAUSE_FOR_MAX_MS, cancelScheduledSto
27
28
  * the `prompt` command, which drives the durable per-role queues rather than the marker
28
29
  * protocol, lives beside them in src/inbox/prompt-commands.ts, and the `config` command,
29
30
  * which reads and writes tumwater.json directly rather than touching team state, in
30
- * src/cli/config-commands.ts. Every command here is deliberately usable without a live
31
+ * src/cli/cli-config-commands.ts. Every command here is deliberately usable without a live
31
32
  * harness except `abort` and `stop`, which have nothing to reach when no team is up. */
32
33
  /** The `--role <id>` value when given, resolved WITHOUT tumwater.json when it names a
33
34
  * built-in catalog role: the team itself tolerates a broken config (the live reload keeps
@@ -67,7 +68,7 @@ function namedRoleList(root, args) {
67
68
  const pieces = rolePieces(args);
68
69
  if (pieces === null)
69
70
  return null;
70
- return [...new Set(pieces.map((piece) => namedRole(root, ["--role", piece])))];
71
+ return distinctOrdered(pieces.map((piece) => namedRole(root, ["--role", piece])));
71
72
  }
72
73
  /** namedRole for the commands that write or lift the per-role pause marker, over a comma list
73
74
  * (`--role clean,dedupe`, like `history --role`): `feature-1` (`bugfix-1`) is the pause key of
@@ -80,7 +81,7 @@ function pauseTargetRoles(root, args) {
80
81
  if (pieces === null)
81
82
  return null;
82
83
  const roles = pieces.map((piece) => [...INSTANCE_ROLES].some((base) => piece === `${base}-1`) ? piece : namedRole(root, ["--role", piece]));
83
- return [...new Set(roles)];
84
+ return distinctOrdered(roles);
84
85
  }
85
86
  /** The `--role <id>` value a marker command requires: namedRole's resolution plus the
86
87
  * command-specific "requires --role" failure. retire needs a target (unlike
@@ -222,17 +223,9 @@ export async function cmdRestart(root, _args) {
222
223
  fail(result.error);
223
224
  say(result.message);
224
225
  }
225
- /** `tumwater pause [--role <id>[,<id>...]] [--for <duration>]`: with roles (a comma list pauses
226
- * each), stop THAT role from
227
- * starting new runs — in-flight ones finish, every other role (the director included)
228
- * keeps running; without one, stop every role from starting NEW runs while in-flight
229
- * ones finish and the director keeps running (its prompts outrank operator gates, like
230
- * under the budget cap). The markers
231
- * are persistent state, not one-shot requests: presence means paused until `resume` removes
232
- * it — so pausing before startup starts an already-paused team. Unlike abort, no live
233
- * harness is required; when none runs, say where the pause takes effect instead of failing.
234
- * Idempotent: a second pause reports the existing marker as-is. */
235
- export async function cmdPause(root, args = [], now = Date.now()) {
226
+ /** Parse and validate the `--for`/`--reason` flags, failing fast on a bad value before any
227
+ * marker is written: "how is `--reason` validated?" is answered here. */
228
+ function pauseFlags(args) {
236
229
  // `--for <duration>` (the timed pause): the ms-epoch deadline pauseTeam/pauseRole write
237
230
  // into the marker and every consumer honors — the command's own gate (runMarkerCommand's
238
231
  // rejectUnknownArgs) has already restricted the flag to this command and validated its
@@ -255,28 +248,45 @@ export async function cmdPause(root, args = [], now = Date.now()) {
255
248
  // capped-flag idiom the --since windows in cli.ts use).
256
249
  if (forMs !== undefined)
257
250
  failOverDurationCap("pause --for", forMs, PAUSE_FOR_MAX_MS, PAUSE_FOR_OVERFLOW_HINT);
251
+ return { forMs, reason };
252
+ }
253
+ /** The timed-pause deadline shape from `--for` and the single `now` instant the deadline and
254
+ * the confirmation both read, injected by tests so the wording they pin cannot drift with
255
+ * the wall clock — the same single-clock discipline timedPauseBits's own `now` parameter
256
+ * follows: "what deadline does `--for` resolve to?" is answered here. */
257
+ function pauseTiming(forMs, now) {
258
258
  // `now` is the one instant the deadline and the confirmation both read, injected by tests
259
259
  // so the wording they pin cannot drift with the wall clock — the same single-clock
260
260
  // discipline timedPauseBits's own `now` parameter follows.
261
261
  const timed = forMs === undefined ? undefined : { ms: forMs, untilMs: now + forMs };
262
- const untilMs = timed?.untilMs;
263
- const roles = pauseTargetRoles(root, args);
264
- if (roles) {
265
- // The plan keeps per-role pauses anonymous (a per-role reason would need a per-role map
266
- // for no observed need), so a reason aimed at a role pause fails fast here — accepting
267
- // the flag and silently dropping the note would tell the operator the reason stands
268
- // when no marker carries it. Before any marker is written, beside the other fail-fasts.
269
- if (reason !== undefined)
270
- fail("pause --reason states why the whole team is paused — a per-role pause carries no reason (drop --role, or run bare `tumwater pause --reason <text>`)");
271
- // Per-role branch: pauseRole in src/team/team-state.ts is the single writer of the role
272
- // marker (the TUI's Ctrl+P and the dashboard's /api/pause-role toggle call it too), so
273
- // the CLI, TUI, and GUI cannot drift on format or idempotence; a false return means the
274
- // role was already in the set, which rolePauseMessage words — unless a `--for` stands,
275
- // which overwrites the deadline and reports the fresh confirmation.
276
- for (const role of roles)
277
- say(rolePauseMessage(root, role, pauseRole(root, role, untilMs), timed, now));
278
- return;
279
- }
262
+ return { timed, untilMs: timed?.untilMs };
263
+ }
264
+ /** The per-role pause branch: rejects a reason aimed at a role pause (a per-role reason would
265
+ * need a per-role map for no observed need), then pauses each named role through
266
+ * `rolePauseMessage`/`pauseRole`. */
267
+ function pauseRoleGroup(root, roles, forMs, now, reason) {
268
+ const { timed, untilMs } = pauseTiming(forMs, now);
269
+ // The plan keeps per-role pauses anonymous (a per-role reason would need a per-role map
270
+ // for no observed need), so a reason aimed at a role pause fails fast here — accepting
271
+ // the flag and silently dropping the note would tell the operator the reason stands
272
+ // when no marker carries it. Before any marker is written, beside the other fail-fasts.
273
+ if (reason !== undefined)
274
+ fail("pause --reason states why the whole team is paused — a per-role pause carries no reason (drop --role, or run bare `tumwater pause --reason <text>`)");
275
+ // Per-role branch: pauseRole in src/team/team-state.ts is the single writer of the role
276
+ // marker (the TUI's Ctrl+P and the dashboard's /api/pause-role toggle call it too), so
277
+ // the CLI, TUI, and GUI cannot drift on format or idempotence; a false return means the
278
+ // role was already in the set, which rolePauseMessage words — unless a `--for` stands,
279
+ // which overwrites the deadline and reports the fresh confirmation.
280
+ for (const role of roles)
281
+ say(rolePauseMessage(root, role, pauseRole(root, role, untilMs), timed, now));
282
+ }
283
+ /** The team pause branch: writes the pause marker through pauseTeam (the single writer, the
284
+ * GUI's /api/pause toggle calls it too), reports "already paused" on a false return, and
285
+ * prints the confirmation template. The reason rides the fresh pause write (or a `--for`
286
+ * overwrite): the same last-write-wins rule as the deadline, so an "already paused" no-op
287
+ * keeps the standing note as-is. */
288
+ function pauseTeamGroup(root, forMs, now, reason) {
289
+ const { timed, untilMs } = pauseTiming(forMs, now);
280
290
  // pauseTeam in src/team/team-state.ts is the single writer of the pause marker — the GUI's
281
291
  // /api/pause toggle calls it too, so the CLI and the dashboard cannot drift on format
282
292
  // or idempotence; a false return means the marker was already there (a `--for` never
@@ -292,6 +302,24 @@ export async function cmdPause(root, args = [], now = Date.now()) {
292
302
  const why = pauseReasonSuffix(reason);
293
303
  say(`team paused${forPhrase}${why} — roles start no new runs${when} (current runs finish; the director keeps running your messages)${note}${tail}`);
294
304
  }
305
+ /** `tumwater pause [--role <id>[,<id>...]] [--for <duration>]`: with roles (a comma list pauses
306
+ * each), stop THAT role from
307
+ * starting new runs — in-flight ones finish, every other role (the director included)
308
+ * keeps running; without one, stop every role from starting NEW runs while in-flight
309
+ * ones finish and the director keeps running (its prompts outrank operator gates, like
310
+ * under the budget cap). The markers
311
+ * are persistent state, not one-shot requests: presence means paused until `resume` removes
312
+ * it — so pausing before startup starts an already-paused team. Unlike abort, no live
313
+ * harness is required; when none runs, say where the pause takes effect instead of failing.
314
+ * Idempotent: a second pause reports the existing marker as-is. */
315
+ export async function cmdPause(root, args = [], now = Date.now()) {
316
+ const { forMs, reason } = pauseFlags(args);
317
+ const roles = pauseTargetRoles(root, args);
318
+ if (roles)
319
+ pauseRoleGroup(root, roles, forMs, now, reason);
320
+ else
321
+ pauseTeamGroup(root, forMs, now, reason);
322
+ }
295
323
  /** Deliver the stop signal to the recorded orchestrator pid: "signalled" when SIGTERM was
296
324
  * delivered, "gone" when the process died between the caller's liveness check and the
297
325
  * signal (ESRCH) — stop's goal is already met in that case, so the caller reports it as
@@ -404,10 +432,21 @@ async function retireTargetRole(root, args) {
404
432
  return role;
405
433
  fail(unknownRoleMessage(role, ids));
406
434
  }
435
+ /** Render one line per artifact an inventory holds and one per skipped artifact — the same
436
+ * wording both the `--dry-run` preview and the committed run go through, so the two cannot
437
+ * drift on the removed / `nothing to remove` phrasing. `verb` distinguishes the preview's
438
+ * `would remove` from the committed `removed`. The `nothing to remove for role` no-ops line
439
+ * stays in the caller (only the committed run carries it). */
440
+ function sayRemoveInventory(removed, skipped, verb, role) {
441
+ for (const name of removed)
442
+ say(`${verb} ${artifactPhrase(name, role)}`);
443
+ for (const name of skipped)
444
+ say(`nothing to remove: ${artifactPhrase(name, role)}`);
445
+ }
407
446
  /** `tumwater retire --role <id> [--force] [--json]`: remove a disabled role's leftover worktree,
408
447
  * branch, merge ref, and paused-state marker. The collection, safety rails, and removal live in
409
- * src/operator/retire.ts; this is the render layer — one line per removed artifact, the skip lines
410
- * that make a second run idempotent, or the `--json` payload. */
448
+ * src/operator/operator-retire.ts; this is the render layer — one line per removed
449
+ * artifact, the skip lines that make a second run idempotent, or the `--json` payload. */
411
450
  export async function cmdRetire(root, args) {
412
451
  // The config exists only to validate the id against built-ins plus user-defined roles, with
413
452
  // the leftover-branch gate for ids no config names; a missing --role fails before it is needed.
@@ -425,10 +464,7 @@ export async function cmdRetire(root, args) {
425
464
  });
426
465
  return;
427
466
  }
428
- for (const name of preview.wouldRemove)
429
- say(`would remove ${artifactPhrase(name, role)}`);
430
- for (const name of preview.skipped)
431
- say(`nothing to remove: ${artifactPhrase(name, role)}`);
467
+ sayRemoveInventory(preview.wouldRemove, preview.skipped, "would remove", role);
432
468
  for (const reason of preview.objections)
433
469
  say(force ? `--force overrides: ${reason}` : `would refuse: ${reason}`);
434
470
  say("dry run — nothing removed");
@@ -439,10 +475,7 @@ export async function cmdRetire(root, args) {
439
475
  sayJson({ role, removed: result.removed, skipped: result.skipped });
440
476
  return;
441
477
  }
442
- for (const name of result.removed)
443
- say(`removed ${artifactPhrase(name, role)}`);
444
- for (const name of result.skipped)
445
- say(`nothing to remove: ${artifactPhrase(name, role)}`);
478
+ sayRemoveInventory(result.removed, result.skipped, "removed", role);
446
479
  if (result.removed.length === 0 && result.skipped.length === 0)
447
480
  say(`nothing to remove for role ${role}`);
448
481
  }
@@ -540,7 +573,7 @@ function revertRunSha(root, positionals, last) {
540
573
  /** `tumwater revert <sha>|<role> <n>|<role> --last`: undo one merged change on main,
541
574
  * committing the inverse as `tumwater(operator): Revert <short sha>` under the primary-worktree
542
575
  * lock. The lock, branch guard, and fail-closed ref/conflict handling live in
543
- * src/operator/revert.ts; this renderer names the new main head and names the role
576
+ * src/operator/operator-revert.ts; this renderer names the new main head and names the role
544
577
  * that made the reverted commit, when one did, as the one that may redo the work.
545
578
  * `--json` prints the preview or the revert as one JSON object instead (refusals still fail on
546
579
  * stderr). */
@@ -1,11 +1,11 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { cancelScheduledStop, isTeamPaused, scheduleStop } from "../team/team-state.js";
4
- import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
4
+ import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
5
5
  import { durationLabel } from "../cli/cli-args.js";
6
6
  import { formatDate, formatTime, humanUntil, DAY_MS } from "../text/datetime.js";
7
7
  import { submitRolePrompt } from "../inbox/inbox-submit.js";
8
- import { questionAsker } from "../backlog/question-answer.js";
8
+ import { questionAsker } from "../backlog/backlog-question-answer.js";
9
9
  import { knownRoleIdsCached, loadConfigCached } from "../config/config.js";
10
10
  import { baseRoleOf, runnerIds } from "../roles/runner-ids.js";
11
11
  import { DIRECTOR_ROLE } from "../roles/roles.js";
@@ -14,7 +14,7 @@ import { loadRunnerState } from "../runner/runner-state-load.js";
14
14
  import { clearBackoff } from "../scheduling/backoff.js";
15
15
  import { writeJsonAtomic } from "../files/json-files.js";
16
16
  import { ensureDir } from "../files/files.js";
17
- import { reclaimPass } from "../team/reclaim.js";
17
+ import { reclaimPass } from "../team/team-reclaim.js";
18
18
  import { formatGB } from "../text/format.js";
19
19
  import { plural } from "../text/phrases.js";
20
20
  import { numberedList } from "../text/markdown.js";
@@ -17,8 +17,7 @@ import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../eve
17
17
  import { errorMessage } from "../text/text.js";
18
18
  import { fairOrder } from "../scheduling/scheduling.js";
19
19
  import { modelPairName } from "../budget/budget.js";
20
- import { withProviderLease } from "../concurrency/provider-lease.js";
21
- import { seamConcurrencyKey } from "../config/config-views.js";
20
+ import { withRunPermit } from "../concurrency/run-permit.js";
22
21
  import { DIRECTOR_ROLE, roleTier } from "../roles/roles.js";
23
22
  import { runTimedRoleRun } from "../run/run-timing.js";
24
23
  /** How many recent work-bearing run durations the p75 sample keeps (drainInFlightWork's
@@ -27,13 +26,13 @@ import { runTimedRoleRun } from "../run/run-timing.js";
27
26
  export const ROLE_RUN_DURATION_SAMPLES = 50;
28
27
  /** The admission wait threshold below which a role run's semaphore wait logs no event — a
29
28
  * run that starts at once stays silent. Mirrors SLOT_WAIT_EVENT_MS in src/git/worktree-pool.ts:
30
- * the wait that signals maxConcurrentPerProvider (or maxConcurrent) caps the team. */
29
+ * the wait that signals maxConcurrent caps the team. */
31
30
  export const PROVIDER_WAIT_EVENT_MS = 30_000;
32
31
  /** Launch one due runner's run: reserve it, park it in the semaphore, and start it under
33
32
  * runTimedRoleRun with the breaker-evidence wiring. Returns the task promise already added
34
33
  * to its in-flight bucket (its `finally` self-removes and folds the duration sample). */
35
34
  export function launchDueRuns(ctx) {
36
- const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, windowMovedRoles, semaphore, piDefault, rolePermitHolders, roleInFlight, directorInFlight, roleRunDurationsMs, startHeld, } = ctx;
35
+ const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, semaphore, rolePermitHolders, roleInFlight, directorInFlight, roleRunDurationsMs, startHeld, } = ctx;
37
36
  let admittedProbe = false;
38
37
  for (const runner of fairOrder([...reasons.keys()])) {
39
38
  if (signal.aborted)
@@ -65,13 +64,8 @@ export function launchDueRuns(ctx) {
65
64
  // Mark the parked waiter while it waits: it holds no permit yet, so the dashboards
66
65
  // render `awaiting slot` (an inactive state) and the active rows keep tracking
67
66
  // maxConcurrent (BUGS.md 2026-09-24). Cleared the moment the permit is granted. The
68
- // director never queues, so it is never a parked waiter. Its provider key rides beside
69
- // `parkedSince` (provider-concurrency part 5/5): the key the admission lease will
70
- // acquire, computed from the same runConfig admission uses, so a waiter named by a
71
- // full key renders `awaiting <key> slot` on the dashboards.
72
- const parkKey = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
67
+ // director never queues, so it is never a parked waiter.
73
68
  runner.state.parkedSince = usesSlot ? Date.now() : undefined;
74
- runner.state.parkedOn = usesSlot && parkKey ? seamConcurrencyKey(parkKey, piDefault) : undefined;
75
69
  // The run's own duration (null when it never started, was cut off, or ended without
76
70
  // work): the drain-window sample is taken only for a work-bearing run that finished
77
71
  // on its own (runTimedRoleRun; BUGS.md 2026-09-30).
@@ -79,36 +73,21 @@ export function launchDueRuns(ctx) {
79
73
  // Whether runner.run() was ever called — false when the start gate (or a shutdown)
80
74
  // turned the run away at its permit.
81
75
  let started = false;
82
- // The run's keyed semaphore lease (plans/provider-concurrency.md §4): acquired on the
83
- // authoring seam's provider key and released in place of the bare team release, so the
84
- // cap is real for role runs from part 2/5 on. The director never queues, so its lease is
85
- // never created here.
76
+ // The run's team semaphore lease, acquired at admission and released in place of the bare
77
+ // team release. The director never queues, so its lease is never created here.
86
78
  let lease = null;
87
- // The director's forced provider lease (plans/provider-concurrency.md §4): never queues and
88
- // never counts toward maxConcurrent, but DOES count toward its provider key while its run
89
- // runs — so a full provider admits no further grants until the director releases.
90
- let directorLease = null;
91
79
  const task = (async () => {
92
80
  durationMs = await runTimedRoleRun(signal, usesSlot
93
81
  ? async () => {
94
- // The key is the authoring seam's concurrency key, computed here at admission
95
- // (runConfig applies any budget or model-failure fallback already in force).
96
- const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
97
- const key = cfg ? seamConcurrencyKey(cfg, piDefault) : undefined;
82
+ // The run is admitted by the one team total (maxConcurrent) alone — there is no
83
+ // per-provider key or cap any more. Which cap holds the run is always the team
84
+ // total.
98
85
  const admittedAt = Date.now();
99
- // Which cap would hold the run, read once at park time (not revised if the caps
100
- // change mid-wait): the run's own provider cap when full, else the team total.
101
- const pool = semaphore.snapshot();
102
- const keyed = key !== undefined ? pool.byKey[key] : undefined;
103
- const providerFull = keyed?.limit !== undefined && keyed.inUse >= keyed.limit;
104
- const heldBy = providerFull
105
- ? { cap: "provider", limit: keyed.limit }
106
- : { cap: "team", limit: pool.limit };
107
- lease = await semaphore.acquireLease(roleTier(runner.role), key);
108
- // A wait of PROVIDER_WAIT_EVENT_MS or more for the provider/team slot is
109
- // provider contention worth surfacing: name it so `tumwater doctor`'s provider
110
- // contention row can sum it and the operator can size maxConcurrentPerProvider
111
- // (or maxConcurrent) from real waits. Best-effort like the launch pass's other
86
+ lease = await semaphore.acquireLease(roleTier(runner.role));
87
+ // A wait of PROVIDER_WAIT_EVENT_MS or more for the team slot is
88
+ // contention worth surfacing: name it so `tumwater doctor`'s provider
89
+ // contention row can sum it and the operator can size maxConcurrent from real
90
+ // waits. Best-effort like the launch pass's other
112
91
  // events, so an unwritable log never blocks a start; a run admitted at once
113
92
  // stays silent.
114
93
  if (Date.now() - admittedAt >= (ctx.providerWaitMs ?? PROVIDER_WAIT_EVENT_MS)) {
@@ -117,56 +96,45 @@ export function launchDueRuns(ctx) {
117
96
  type: "provider_wait",
118
97
  role: runner.role,
119
98
  waitedMs: Date.now() - admittedAt,
120
- ...(key !== undefined ? { key } : {}),
121
- ...heldBy,
122
99
  });
123
100
  }
124
101
  // Permit granted: the run is now an active, permit-holding state until the
125
102
  // release below. A run the start gate turns away releases within the same
126
103
  // microtask chain, so no poll ever counts it as a permit holder.
127
104
  runner.state.parkedSince = undefined;
128
- runner.state.parkedOn = undefined;
129
105
  rolePermitHolders.add(runner);
130
106
  }
131
107
  : async () => {
132
- // The director wraps its run in a forced lease on its provider key, never
133
- // queuing behind roles: a user prompt starts at once even on a full provider,
134
- // and the key's in-use rises by one — overshooting by one — so no further grant on
135
- // that key happens until its release drains it back under the cap.
136
- const cfg = runner.runConfig(Date.now());
137
- directorLease = semaphore.forceLease(cfg ? seamConcurrencyKey(cfg, piDefault) : undefined);
108
+ // The director never queues behind roles: a user prompt starts at once, taking
109
+ // no semaphore permit (never counting toward maxConcurrent).
138
110
  }, usesSlot
139
111
  ? () => {
140
112
  rolePermitHolders.delete(runner);
141
113
  lease.release();
142
114
  }
143
- : () => {
144
- directorLease.release();
145
- }, async () => {
115
+ : () => { }, async () => {
146
116
  started = true;
147
117
  // A role run that starts while the budget holds uses the fallback pair its tier
148
- // resolved to (the gate's per-tier view was installed in its config), or — when a
149
- // model-fallback episode is active — on the tier's model-fallback pair, which can
150
- // differ from the budget pair. Its outcome is the evidence for the pair it ACTUALLY
151
- // ran on, so read it from runConfig (config + episode), never from configForRole
152
- // alone: a per-pair breaker fed a fallback run under the primary's name would
153
- // misprice a healthy pair and miss a dead one (PLANS.md "Model failure fallback,
154
- // part 1/2").
155
- const now = Date.now();
156
- const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(now);
118
+ // resolved to (the gate's per-tier view was installed in its config). Its outcome
119
+ // is the evidence for the pair it ACTUALLY ran on, so read it from the runner's
120
+ // resolved config (runConfig), which names the budget-fallback pair when a cap
121
+ // episode is active: a per-pair breaker fed a fallback run under the primary's
122
+ // name would misprice a healthy pair and miss a dead one (PLANS.md "Mixed claude +
123
+ // pi fleets, part 2/4").
124
+ const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig();
157
125
  // The breaker is keyed by the seam that ran (seamHoldKey), like the run pair and
158
126
  // the team-hold stamps (PLANS.md "Mixed claude + pi fleets, part 2/4"): a claude
159
127
  // fallback run demotes the `claude/<model>` breaker entry, never `?/…`.
160
- const ranPair = (budgetActive || windowMovedRoles.has(runner.role)) && cfg?.model !== undefined
128
+ const ranPair = budgetActive && cfg?.model !== undefined
161
129
  ? modelPairName({ provider: seamHoldKey(cfg), model: cfg.model })
162
130
  : null;
163
131
  const ranOn = ranPair !== null ? gateStates.budget.breakers[ranPair] : undefined;
164
- // The run's lease rides in the provider-lease store for the length of the run, so
165
- // every agent run it makes re-keys it to the provider that agent run actually uses
166
- // (plans/provider-concurrency.md §5) — a budget or model-failure switch that moves an
167
- // agent run to another provider mid-run moves the lease with it. The director's forced
168
- // lease re-keys the same way, and its rekey never waits.
169
- const outcome = await withProviderLease(runner.role === DIRECTOR_ROLE ? directorLease : lease, semaphore, () => runner.run(), { piDefault });
132
+ // The run's lease rides in the run-permit store for the length of the run, so an
133
+ // idle wait inside it (a failed review's in-place retry pause) can hand the team
134
+ // permit back and re-take it before the next attempt. The director holds no lease.
135
+ const outcome = usesSlot
136
+ ? await withRunPermit(lease, () => runner.run())
137
+ : await runner.run();
170
138
  if (ranPair !== null && ranOn !== undefined) {
171
139
  const at = breakerNow();
172
140
  const evidence = outcome.recoveredLeftover ? "skipped" : outcome.result;