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
@@ -1,8 +1,7 @@
1
- import { publishInfoFields } from "../team/orchestrator-info.js";
2
- import { baseRoleOf, runnerEnabled } from "../roles/runner-ids.js";
1
+ import { publishInfoFields } from "../team/team-orchestrator-info.js";
2
+ import { runnerEnabled } from "../roles/runner-ids.js";
3
3
  import { DIRECTOR_ROLE } from "../roles/roles.js";
4
4
  import { reviewHoldKey, pollDeferralInputs, pollRunnerReasons } from "./orchestrator-scheduling.js";
5
- import { seamConcurrencyKey } from "../config/config-views.js";
6
5
  import { launchDueRuns } from "./orchestrator-launch.js";
7
6
  import { branchHead, currentBranch } from "../git/git.js";
8
7
  import { queuedMergeFiles, queuedMergeRoles } from "../merge/merge-queue.js";
@@ -15,11 +14,21 @@ import { checkPermitSnapshot } from "../concurrency/check-permit.js";
15
14
  import { slotPoolSnapshot } from "../git/worktree-pool.js";
16
15
  import { redeployTarget, watchVerifiesMain } from "../baseline/main-baseline.js";
17
16
  import { p75RunDurationMs } from "../run/run-timing.js";
17
+ import { reapAttributableOrphans } from "../doctor/doctor-orphans.js";
18
18
  import { pollTeamGates } from "../gates/gate-polls.js";
19
19
  import { heldProviders, windowHeldProviders } from "../team/team-hold.js";
20
20
  import { workReserveCount } from "../scheduling/claims.js";
21
21
  export function newPollState() {
22
- return { restart: false, holdForRestart: false, diskHeld: false, warnedBranchDivergence: false, lastReserve: 0 };
22
+ return { restart: false, holdForRestart: false, diskHeld: false, warnedBranchDivergence: false, lastReserve: 0, lastOrphanReapAt: 0 };
23
+ }
24
+ /** How often the orchestrator's poll loop stands down the orphan reap — a once-per-interval
25
+ * throttle so a slow process-table scan never runs on the busy per-poll hot path. */
26
+ export const ORPHAN_REAP_INTERVAL_MS = 15 * 60_000;
27
+ /** Whether the orphan reap is due now, given the last poll it ran: true at and after
28
+ * `now >= last + intervalMs`. Purely a clock comparison so tests pin it from literal
29
+ * numbers. A fresh state's `last` of 0 is always due against a real `now`. */
30
+ export function dueForOrphanReap(last, now, intervalMs = ORPHAN_REAP_INTERVAL_MS) {
31
+ return now - last >= intervalMs;
23
32
  }
24
33
  /** The start gate every reserved run passes the moment its permit is granted
25
34
  * (runTimedRoleRun's `held`): closed while a restart is pending, and after one is decided so
@@ -29,20 +38,13 @@ function runStartHeld(state) {
29
38
  return state.holdForRestart || state.restart || state.diskHeld;
30
39
  }
31
40
  /** The one start-gate rule for merges: a restart/disk hold's `held` counts against every role,
32
- * while a hold on the reviewer's provider (`reviewHeld`) parks only the entries a pre-merge
33
- * review gates — a role on `review.afterMerge` keeps draining (it merges without a reviewer),
34
- * and no role at all (the conservative team-wide verdict a caller that cannot name a role
35
- * reads) still holds. Shared by pollMergeDrain's startHeld and the merge tests, so the
36
- * production wiring and its coverage cannot drift. */
37
- export function mergeStartHeld(held, reviewHeld, afterMergeRoles, role) {
38
- return held || (reviewHeld && (role === undefined || !afterMergeRoles.has(baseRoleOf(role))));
39
- }
40
- /** The base roles whose review runs after merging — the config's `review.afterMerge` list, mapped
41
- * through `baseRoleOf` so a listed base id stands for all its parallel instances. The one build of
42
- * the set pollMergeDrain's start gate and pollSchedule's launch gate both exempt from a
43
- * reviewer-hold, so the two passes cannot read the standing differently. */
44
- function afterMergeRoleIds(config) {
45
- return new Set((config.review.afterMerge ?? []).map(baseRoleOf));
41
+ * while a hold on the reviewer's provider (`reviewHeld`) parks only the entries a caller that
42
+ * names no role admits — the post-merge review drain. A role named (every merge entry) is never
43
+ * held by the reviewer: no role start or landing waits on the reviewer. Shared by
44
+ * pollMergeDrain's startHeld and the merge tests, so the production wiring and its coverage
45
+ * cannot drift. */
46
+ export function mergeStartHeld(held, reviewHeld, role) {
47
+ return held || (reviewHeld && role === undefined);
46
48
  }
47
49
  /** Live-reload, notify/retention/launchservices, CLI request markers, abort settling and the
48
50
  * branch-divergence watch. Writes state.warnedBranchDivergence. */
@@ -52,7 +54,7 @@ export async function pollRequestsAndWatch(ctx, state) {
52
54
  // src/config/config-live.ts owns the last-known-good retention and the edge-triggered
53
55
  // warnings/events around it.
54
56
  const liveConfig = ctx.liveReload.poll();
55
- // The notify command rides the same last-known-good reload (src/events/notify.ts): a live
57
+ // The notify command rides the same last-known-good reload (src/events/event-notify.ts): a live
56
58
  // `config set notify` edit takes effect on the next poll, no restart.
57
59
  ctx.notifier.update(liveConfig);
58
60
  // Live session retention (the last restart-only setting): the edge-triggered
@@ -60,6 +62,15 @@ export async function pollRequestsAndWatch(ctx, state) {
60
62
  // src/orchestrator/retention.ts. The live config (last-known-good while the file is broken or
61
63
  // missing) drives the check, like the budget gate.
62
64
  ctx.retentionPruner.poll(root, liveConfig.sessionRetentionDays);
65
+ // The orphan reap (src/doctor/doctor-orphans.ts): at most once per interval, kill the
66
+ // orphans the doctor row already reports that are old enough to be certain a live run no
67
+ // longer owns them — the automatic counterpart to the human click `doctor
68
+ // --kill-orphans`/dashboard/TUI makes. The time is stamped before the await, so a slow
69
+ // scan cannot overlap the next poll's reap (and this await anyway holds the poll step).
70
+ if (dueForOrphanReap(state.lastOrphanReapAt, Date.now())) {
71
+ state.lastOrphanReapAt = Date.now();
72
+ await reapAttributableOrphans(root);
73
+ }
63
74
  // A missing or stale opencode catalog is refetched at most once an hour while opencode is routed.
64
75
  ctx.opencodeCatalogRefresh.poll(liveConfig);
65
76
  // launchservicesd's Mach-port budget (src/process/launch-services.ts owns it):
@@ -119,9 +130,8 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
119
130
  state.diskHeld = gates.diskHeld;
120
131
  // Publish the permit pool's live state (plans/provider-concurrency.md §8): observers
121
132
  // (status, TUI, GUI) read one file instead of coupling to this Semaphore, and the team
122
- // line shows per-provider counts only while a cap map is configured. Written only when
123
- // it changes, like budget and disk; a map-less team publishes `byKey` empty, which
124
- // renders exactly as before. The check-permit pool rides along, so status can show checks
133
+ // line shows the team total. Written only when it changes, like budget and disk. The
134
+ // check-permit pool rides along, so status can show checks
125
135
  // running and waiting while merges stall. The workspace pool's live state rides along too:
126
136
  // one small slots.json read per poll, and publishInfoFields rewrites the info file only
127
137
  // when a value changed.
@@ -131,17 +141,15 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
131
141
  ["slotPool", slotPoolSnapshot(root)],
132
142
  ]);
133
143
  // The failure hold, keyed per provider (PLANS.md 2026-10-05): a role is held when ITS
134
- // run model's provider stands under a hold, and the roles a pre-merge review gates are held
135
- // when the reviewer's provider is (with the review gate on their approved change could not
136
- // merge without a reviewer — while a role on `review.afterMerge` merges without one, so it is
137
- // exempt, and the merge queue reads the same scoped reviewHeld verdict). Key presence in the
144
+ // run model's provider stands under a hold. A hold on the reviewer's provider parks only
145
+ // the post-merge review drain, never a role start, verify or landing. Key presence in the
138
146
  // map is relapse memory, not a hold: heldProviders() reads only standing holds (`until`
139
147
  // non-null), so a lifted hold never keeps blocking.
140
148
  const teamHeldProviders = heldProviders(gateStates.teamHold);
141
149
  const teamHeldNow = () => heldProviders(gateStates.teamHold);
142
150
  // The reviewer's seam is read from the config merges actually review under — the poll's
143
151
  // roleConfig, which a used-up claude window or the cap has already moved to a fallback —
144
- // so a reviewer that left claude no longer holds the roles a pre-merge review gates on the
152
+ // so a reviewer that left claude no longer parks the post-merge review drain on the
145
153
  // claude hold.
146
154
  const reviewSeamKey = reviewHoldKey(gates.roleConfig);
147
155
  return {
@@ -149,8 +157,7 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
149
157
  teamHeldProviders,
150
158
  teamHeldNow,
151
159
  reviewHeldNow: () => liveConfig.review.enabled && teamHeldNow().has(reviewSeamKey),
152
- reviewHeld: liveConfig.review.enabled && teamHeldProviders.has(reviewSeamKey),
153
- roleProviders: new Map(runners.map((r) => [r.role, r.runProvider(now)])),
160
+ roleProviders: new Map(runners.map((r) => [r.role, r.runProvider()])),
154
161
  };
155
162
  }
156
163
  /** Self-redeploy (src/redeploy/redeployer.ts): with main's head in hand, let the state
@@ -180,6 +187,8 @@ export async function pollRedeploy(ctx, state, { liveConfig, mainHead, now }) {
180
187
  directorInFlight: ctx.directorInFlight.size,
181
188
  roleRunP75Ms: p75RunDurationMs(ctx.roleRunDurationsMs),
182
189
  }, liveConfig.autoRestart, now);
190
+ // The tip beside the target: a tip no passing check covers yet is published as waitingForCheck.
191
+ await redeploy.noteTip(mainHead);
183
192
  publishInfoFields(ctx.root, ctx.info, [["build", redeploy.status(now)]]);
184
193
  if (action === "restart") {
185
194
  state.restart = true;
@@ -201,16 +210,12 @@ export async function pollRedeploy(ctx, state, { liveConfig, mainHead, now }) {
201
210
  * merge slot, and the dedupe against main). A restart or disk hold stops the whole drain, as it
202
211
  * starts no run; what is already in flight carries on, and a verify parked for its permit
203
212
  * meets the same start gate as a parked run when the permit comes. A hold on the reviewer's
204
- * provider parks only the entries a pre-merge review gates, per the role-scoped mergeStartHeld:
205
- * a `review.afterMerge` role's approved/queued changes keep draining. */
213
+ * provider parks nothing in the drain — no merge entry is held by the reviewer (its hold parks
214
+ * only the post-merge review drain, whose no-role start gate reads it). */
206
215
  export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates) {
207
216
  if (state.holdForRestart || state.diskHeld)
208
217
  return;
209
218
  const { root, mainBranch } = ctx;
210
- // The roles whose review runs after merging (plans post-merge-review role): while the
211
- // reviewer is held, their approved/queued changes keep draining, so the per-entry start gate
212
- // exempts them from the reviewer-hold leg below.
213
- const afterMergeRoles = afterMergeRoleIds(liveConfig);
214
219
  // A `main_red` pin the pipeline kept goes back on the queue once main's head has moved.
215
220
  for (const r of ctx.runners)
216
221
  await requeueMainRedPin(root, mainBranch, r.state, mainHead);
@@ -222,9 +227,8 @@ export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates
222
227
  runners: ctx.runners,
223
228
  liveConfig,
224
229
  roleConfig: gates.roleConfig,
225
- directorConfig: gates.directorConfig,
226
- piDefault: ctx.piDefault,
227
- startHeld: (role) => mergeStartHeld(runStartHeld(state), gates.reviewHeldNow(), afterMergeRoles, role),
230
+ directorConfig: liveConfig,
231
+ startHeld: (role) => mergeStartHeld(runStartHeld(state), gates.reviewHeldNow(), role),
228
232
  }, ctx.merges);
229
233
  }
230
234
  /** The scheduling pass: the deferral inputs feed pollRunnerReasons, whose reasons map
@@ -232,10 +236,6 @@ export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates
232
236
  export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, gates) {
233
237
  const { root, runners, gateStates } = ctx;
234
238
  const inputs = pollDeferralInputs(root, runners, liveConfig, gateStates, gates, ctx.breakerNow());
235
- // The roles whose review runs after merging: while the reviewer is held their runs stay
236
- // launchable (the pre-merge gate skips their review), so the permit-time start gate exempts
237
- // them from the reviewer-hold leg below, exactly as the scheduling pass and merge drain do.
238
- const afterMergeRoles = afterMergeRoleIds(liveConfig);
239
239
  const reasons = await pollRunnerReasons({
240
240
  root,
241
241
  runners,
@@ -252,14 +252,11 @@ export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, ga
252
252
  bootstrapHeld: gates.bootstrapHeld,
253
253
  budgetPausedRoles: gates.budgetPausedRoles,
254
254
  probeRoles: inputs.probeRoles,
255
- reviewStrongPaused: inputs.reviewStrongPaused,
256
255
  roleQuietHeld: gates.roleQuietHeld,
257
256
  holdForRestart: state.holdForRestart,
258
257
  diskHeld: state.diskHeld,
259
258
  heldProviders: gates.teamHeldProviders,
260
259
  windowHeldProviders: windowHeldProviders(gateStates.teamHold),
261
- reviewHeld: gates.reviewHeld,
262
- afterMergeRoles,
263
260
  roleProviders: gates.roleProviders,
264
261
  openBugsNow: inputs.openBugsNow,
265
262
  workBacklogOpen: inputs.workBacklogOpen,
@@ -280,52 +277,30 @@ export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, ga
280
277
  probePair: inputs.probePair,
281
278
  probeRoles: inputs.probeRoles,
282
279
  budgetActive: gates.budgetActive,
283
- windowMovedRoles: gates.windowMovedRoles,
284
280
  semaphore: ctx.semaphore,
285
- piDefault: ctx.piDefault,
286
281
  rolePermitHolders: ctx.rolePermitHolders,
287
282
  roleInFlight: ctx.roleInFlight,
288
283
  directorInFlight: ctx.directorInFlight,
289
284
  roleRunDurationsMs: ctx.roleRunDurationsMs,
290
285
  startHeld: (role) => runStartHeld(state) ||
291
286
  (role !== DIRECTOR_ROLE
292
- ? (!afterMergeRoles.has(baseRoleOf(role)) && gates.reviewHeldNow()) ||
293
- gates.teamHeldNow().has(gates.roleProviders.get(role))
287
+ ? gates.teamHeldNow().has(gates.roleProviders.get(role))
294
288
  : windowHeldProviders(gateStates.teamHold).has(gates.roleProviders.get(role))),
295
289
  });
296
290
  }
297
291
  /** Priority headroom, then once mode's exit; returns whether the loop should stop. Writes
298
292
  * state.lastReserve and, in once mode, fires internalStop. */
299
- export function pollReserveAndExit(ctx, state, { liveConfig, now }) {
293
+ export function pollReserveAndExit(ctx, state, { liveConfig }) {
300
294
  const { root, runners, semaphore } = ctx;
301
295
  // Priority headroom (plans/parallel-work-instances.md part 7/7): withhold a few permits
302
296
  // from maintenance while a work role has work to take, so a due work run never waits out
303
297
  // a whole maintenance run. Sized after the launch pass, so a runner launched this poll
304
298
  // already reads as running and reserves nothing for itself.
305
- const poolSnapshot = semaphore.snapshot();
306
- // The provider-headroom predicate receives the structural ClaimRunner; the config that
307
- // names the provider lives on the real RoleRunners, so look the runner up by role.
308
- const runnerByRole = new Map(runners.map((r) => [r.role, r]));
309
299
  const reserve = workReserveCount(root, runners, {
310
300
  maxConcurrent: liveConfig.maxConcurrent,
311
301
  enabled: new Set(runners.filter((r) => runnerEnabled(r.config, r.role)).map((r) => r.role)),
312
302
  queuedMergeRoles: queuedMergeRoles(root),
313
303
  instancesOf: (base) => liveConfig.roles[base]?.instances ?? 1,
314
- // Per-provider headroom (plans/provider-concurrency.md §6): an idle work role reserves a
315
- // permit only when its own provider could use it — capped and full, it would only idle
316
- // the slot another provider's maintenance run could take. Read from the pool's live
317
- // snapshot; an uncapped key (or the team-total-only case) always has headroom.
318
- providerHasHeadroom: (r) => {
319
- const runner = runnerByRole.get(r.role);
320
- const cfg = runner && runner.role !== DIRECTOR_ROLE ? runner.runConfig(now) : null;
321
- const key = cfg ? seamConcurrencyKey(cfg, ctx.piDefault) : undefined;
322
- if (key === undefined)
323
- return true;
324
- const k = poolSnapshot.byKey[key];
325
- if (k?.limit === undefined)
326
- return true;
327
- return k.inUse < k.limit;
328
- },
329
304
  });
330
305
  semaphore.setReserve(reserve);
331
306
  if ((state.lastReserve === 0) !== (reserve === 0)) {
@@ -48,8 +48,8 @@ export async function pollRunnerReasons(ctx) {
48
48
  }
49
49
  /** The team-hold key of the reviewer's seam, read from the config merges actually review under
50
50
  * (the poll's roleConfig, which a used-up claude window or the cap has moved to a fallback), so
51
- * a reviewer that left claude no longer holds the roles a pre-merge review gates on the claude
52
- * hold. */
51
+ * a reviewer that left claude no longer parks the no-role post-merge review drain on the claude
52
+ * hold — no named entry (start, verify or landing) waits on the reviewer. */
53
53
  export function reviewHoldKey(roleConfig) {
54
54
  return seamHoldKey(reviewRunConfig(roleConfig));
55
55
  }
@@ -65,11 +65,8 @@ export function reviewHoldKey(roleConfig) {
65
65
  * exactly why it is being probed). Only those roles pierce the budget hold, and the launch
66
66
  * pass admits exactly one of them as the probe — a probe blind to the pair's tier would lift
67
67
  * the pause for every role, and a probe with no eligible runner must not hijack the poll's
68
- * other launches. The review-on strong pause (budgetGate): with the review gate on, an
69
- * unresolvable strong tier means nothing could merge, so every role a pre-merge review gates is
70
- * held (a `review.afterMerge` role is exempt — its review runs after merging) — the per-tier
71
- * pause set alone would let small/default roles author changes no reviewer could judge. The
72
- * probe of the strong pair pierces this hold for exactly the probed tier's roles too. */
68
+ * other launches. The probe of a demoted pair pierces the hold for
69
+ * exactly the probed tier's roles too. */
73
70
  export function pollDeferralInputs(root, runners, liveConfig, gateStates, gates, breakerNowMs) {
74
71
  const openBugsNow = openBugs(root).length > 0;
75
72
  const workBacklogOpen = eligibleEntries(root, "feature").length > 0 || eligibleEntries(root, "bugfix").length > 0;
@@ -85,6 +82,5 @@ export function pollDeferralInputs(root, runners, liveConfig, gateStates, gates,
85
82
  probeRoles.add(r.role);
86
83
  }
87
84
  }
88
- const reviewStrongPaused = gates.budgetActive && liveConfig.review.enabled && gates.servingResolved.strong.pair === null;
89
- return { openBugsNow, workBacklogOpen, plansFull, probePair, probeRoles, reviewStrongPaused };
85
+ return { openBugsNow, workBacklogOpen, plansFull, probePair, probeRoles };
90
86
  }
@@ -1,4 +1,4 @@
1
- import { readOrchestratorInfo, writeOrchestratorInfo } from "../team/orchestrator-info.js";
1
+ import { readOrchestratorInfo, writeOrchestratorInfo } from "../team/team-orchestrator-info.js";
2
2
  import { runnerIds } from "../roles/runner-ids.js";
3
3
  import { newLiveConfigReload } from "../config/config-live.js";
4
4
  import { FALLBACK_BREAKER_POLICY, } from "../budget/fallback-breaker.js";
@@ -10,20 +10,18 @@ import { mergeTasks, newMergePipeline } from "../merge/merge-pipeline.js";
10
10
  import { bestEffortEventLogger, logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
11
11
  import { removeQuiet } from "../files/files.js";
12
12
  import { OnceRound } from "../scheduling/once-round.js";
13
- import { newNotifier } from "../events/notify.js";
13
+ import { newNotifier } from "../events/event-notify.js";
14
14
  import { piModelsPath } from "../pi/pi-models.js";
15
- import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
16
15
  import { Semaphore } from "../concurrency/semaphore.js";
17
16
  import { orchestratorStatePath } from "../paths.js";
18
17
  import { createMainWatch } from "../baseline/main-watch.js";
19
18
  import { queueCulpritHeal } from "../baseline/main-red.js";
20
19
  import { RetentionPruner } from "./retention.js";
21
- import { newOpencodeCatalogRefresh } from "../gates/startup-gate.js";
20
+ import { newOpencodeCatalogRefresh } from "../gates/startup-gates.js";
22
21
  import { WorkMergedCache } from "../scheduling/work-merged-cache.js";
23
22
  import { drainInFlightWork, HANDOFF_MERGE_WINDOW_MS, sleepInterruptible, } from "../run/run-timing.js";
24
23
  import { newTeamGateStates } from "../gates/gate-polls.js";
25
- import { loggedWindowKeys } from "../team/team-polls.js";
26
- import { ReclaimController } from "../team/reclaim.js";
24
+ import { ReclaimController } from "../team/team-reclaim.js";
27
25
  import { reapPreviousTeamOrphans } from "../doctor/doctor-orphans.js";
28
26
  import { runStartupMigrations } from "./startup-migration-steps.js";
29
27
  import { archiveAtTeamStart } from "../backlog/backlog-archive.js";
@@ -34,9 +32,6 @@ export async function runOrchestrator(opts) {
34
32
  const { root, config, mainBranch, signal: externalSignal } = opts;
35
33
  const pollMs = opts.pollMs ?? POLL_MS;
36
34
  const modelsPath = opts.modelsPath ?? piModelsPath();
37
- // pi's saved default model — the same stat-cached read status-roles.ts makes — used as the
38
- // fallback provider key for a pi seam that names no model (plans/provider-concurrency.md §2).
39
- const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), root);
40
35
  const breakerPolicy = opts.fallbackBreakerPolicy ?? FALLBACK_BREAKER_POLICY;
41
36
  const breakerNow = opts.breakerNow ?? Date.now;
42
37
  // Parallel work instances (part 5b/7): the runner list is every runner id the config enables
@@ -77,10 +72,6 @@ export async function runOrchestrator(opts) {
77
72
  // resolver — merge-drain.ts) all hold a permit; see its MergePipelineContext for why a
78
73
  // merge is not exempt.
79
74
  const semaphore = new Semaphore(Math.max(1, config.maxConcurrent));
80
- // Seed the pool's per-provider caps from the startup config (plans/provider-concurrency.md
81
- // §7): a role run's lease is bounded by its provider key from the first launch onward, not
82
- // just after a live edit. A config without the key passes an empty map, the no-change case.
83
- semaphore.setKeyCaps(config.maxConcurrentPerProvider ?? {});
84
75
  const infoFile = orchestratorStatePath(root);
85
76
  const info = { pid: process.pid, startedAt: Date.now(), roles: enabled };
86
77
  if (redeploy)
@@ -174,9 +165,6 @@ export async function runOrchestrator(opts) {
174
165
  // the start gate reads gateStates.teamHold at permit time, so its closures always see
175
166
  // the latest poll's hold verdict rather than the poll that created them.
176
167
  const gateStates = newTeamGateStates(config);
177
- // A restart must not re-announce a window that is still open.
178
- gateStates.claudeWindowWarned = loggedWindowKeys(root, "subscription_window_warning", Date.now());
179
- gateStates.claudeWindowReserved = loggedWindowKeys(root, "subscription_window_reserved", Date.now());
180
168
  // The live config the last successful reload produced (last-known-good while the file is
181
169
  // broken or missing). The reload bookkeeping itself lives in src/config/config-live.ts.
182
170
  const liveReload = newLiveConfigReload({ root, config, mainBranch, runnerSignal: roleSignal, runners, semaphore, roleFilter: opts.roleFilter });
@@ -194,10 +182,10 @@ export async function runOrchestrator(opts) {
194
182
  // logs exactly once — on the transition in, never while merely not-due and never on exit.
195
183
  // In-memory only: a restart mid-episode can re-log at most one event.
196
184
  const deferredDue = new Map();
197
- // The operator notify hook (src/events/notify.ts owns the whole concern): one configured shell
198
- // command run on notable events (budget_paused, role_streak_paused, merge_failed,
199
- // restart_blocked). Subscribed here so it sees every event this process logs from start;
200
- // disposed beside the orchestrator_stop event below.
185
+ // The operator notify hook (src/events/event-notify.ts owns the whole concern): one
186
+ // configured shell command run on notable events (budget_paused, role_streak_paused,
187
+ // merge_failed, restart_blocked). Subscribed here so it sees every event this
188
+ // process logs from start; disposed beside the orchestrator_stop event below.
201
189
  const notifier = newNotifier(root);
202
190
  // Best-effort event logging in the poll loop (bestEffortEventLogger, events.ts): this loop
203
191
  // and its finally run with no catch, so an unwritable feed (ENOSPC, EACCES, the path replaced
@@ -221,7 +209,6 @@ export async function runOrchestrator(opts) {
221
209
  once,
222
210
  info,
223
211
  modelsPath,
224
- piDefault,
225
212
  redeploy,
226
213
  launchServicesWatch,
227
214
  reclaim,
@@ -19,52 +19,73 @@ import { noteSkip } from "./scheduling-scope.js";
19
19
  * runner's claim/heldElsewhere state, the claim group's held set, and once-mode settlement on
20
20
  * the skip paths. */
21
21
  export function admitRunner(scope, runner, reason) {
22
- // Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
23
- // every gate has admitted this runner. A runner that already holds a claim proceeds — its
24
- // revision, resume or retry is the same entry. An unclaimed runner with outstanding work
25
- // (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
26
- // holding it too would deadlock the whole group, since only its run can settle the hold.
27
- // Otherwise, when the run is not an inbox/resume run, it takes the first free entry; an
28
- // extra instance with none free is skipped without touching its backoff, and the primary
29
- // stands down too while siblings hold every eligible entry, unless an operator wake is fresh
30
- // (then it runs unassigned).
31
- // Stand-down: the feature charter says "If PLANS.md is empty or everything is done, there is
32
- // nothing to do" and the bugfix charter says "when every open entry carries [a Refused note],
33
- // there is nothing to do", so a feature runner (primary included) with no eligible plan at all,
34
- // or a bugfix runner whose open bugs are all held (no open bugs at all is search mode, which
35
- // still runs), skips its scheduled and main-moved runs instead of paying a model run that
36
- // can only end in
37
- // nothing-to-do. Startup, inbox and resume runs, a fresh operator wake, and a runner with a
38
- // claim or outstanding work still run. nextRunAt and backoff stay put, so the first poll
39
- // after a plan turns eligible starts a run for it.
40
- const standDownBase = baseRoleOf(runner.role);
41
- if ((standDownBase === "feature" || (standDownBase === BUGFIX_ROLE && scope.openBugsNow)) &&
22
+ if (standDownForNoWork(scope, runner, reason))
23
+ return "skipped";
24
+ return claimGroupAdmission(scope, runner, reason);
25
+ }
26
+ /** The candidate half of the stand-down test: true when a feature runner, or a bugfix runner
27
+ * whose open bugs are non-empty (no open bugs at all is search mode, which still runs), is due
28
+ * a scheduled or main-moved run, is not freshly woken, and holds no claim or outstanding work —
29
+ * so startup, inbox and resume runs, a fresh operator wake, and a claimed or mid-entry runner
30
+ * all pass through. The caller stands the runner down only when this holds and no entry is
31
+ * eligible. */
32
+ function noWorkScheduledRun(scope, runner, reason) {
33
+ const base = baseRoleOf(runner.role);
34
+ return ((base === "feature" || (base === BUGFIX_ROLE && scope.openBugsNow)) &&
42
35
  (reason === "scheduled" || reason === "main moved") &&
43
36
  !freshlyWoken(runner.state) &&
44
37
  runner.state.claim === undefined &&
45
- !hasOutstandingWork(runner.state)) {
46
- let eligible = scope.eligibleMemo.get(standDownBase);
47
- if (eligible === undefined) {
48
- eligible =
49
- scope.claimGroups.get(standDownBase)?.eligible ??
50
- eligibleEntries(scope.root, standDownBase);
51
- scope.eligibleMemo.set(standDownBase, eligible);
52
- }
53
- if (eligible.length === 0) {
54
- noteSkip(scope.root, runner, "no-free-entry");
55
- runner.state.heldElsewhere = undefined;
56
- if (scope.once.active)
57
- scope.once.settle(runner.role, "no free entry");
58
- return "skipped";
59
- }
38
+ !hasOutstandingWork(runner.state));
39
+ }
40
+ /** The eligible-entries list for a base role, computed once per poll and memoized on the
41
+ * scope so the stand-down and claim-group paths share it. */
42
+ function eligibleForBase(scope, base) {
43
+ let eligible = scope.eligibleMemo.get(base);
44
+ if (eligible === undefined) {
45
+ eligible =
46
+ scope.claimGroups.get(base)?.eligible ?? eligibleEntries(scope.root, base);
47
+ scope.eligibleMemo.set(base, eligible);
60
48
  }
49
+ return eligible;
50
+ }
51
+ /** Stand down a runner with no eligible entry at all on its scheduled and main-moved runs: the
52
+ * feature charter says "If PLANS.md is empty or everything is done, there is nothing to do" and
53
+ * the bugfix charter says "when every open entry carries [a Refused note], there is nothing to
54
+ * do", so a feature runner (primary included) with no eligible plan at all, or a bugfix runner
55
+ * whose open bugs are all held (no open bugs at all is search mode, which still runs), skips
56
+ * its scheduled and main-moved runs instead of paying a model run that can only end in
57
+ * nothing-to-do. nextRunAt and backoff stay put, so the first poll after a plan turns eligible
58
+ * starts a run for it. */
59
+ function standDownForNoWork(scope, runner, reason) {
60
+ if (!noWorkScheduledRun(scope, runner, reason))
61
+ return false;
62
+ if (eligibleForBase(scope, baseRoleOf(runner.role)).length > 0)
63
+ return false;
64
+ noteSkip(scope.root, runner, "no-free-entry");
65
+ runner.state.heldElsewhere = undefined;
66
+ if (scope.once.active)
67
+ scope.once.settle(runner.role, "no free entry");
68
+ return true;
69
+ }
70
+ /** Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
71
+ * every gate has admitted this runner. A runner that already holds a claim proceeds — its
72
+ * revision, resume or retry is the same entry. An unclaimed runner with outstanding work
73
+ * (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
74
+ * holding it too would deadlock the whole group, since only its run can settle the hold.
75
+ * Otherwise, when the run is not an inbox/resume run, it takes the first free entry; an
76
+ * extra instance with none free is skipped without touching its backoff, and the primary
77
+ * stands down too while siblings hold every eligible entry, unless an operator wake is fresh
78
+ * (then it runs unassigned). */
79
+ function claimGroupAdmission(scope, runner, reason) {
61
80
  const claimGroup = scope.claimGroups.get(baseRoleOf(runner.role));
62
81
  if (claimGroup) {
63
82
  if (runner.state.claim) {
64
83
  // keeps its claim
84
+ return "admitted";
65
85
  }
66
86
  else if (hasOutstandingWork(runner.state)) {
67
87
  // finishes its own unrecorded entry
88
+ return "admitted";
68
89
  }
69
90
  else if (reason !== "inbox" && reason !== "resume") {
70
91
  // The group holds an unclaimed mid-entry sibling: assign nothing this poll, or a
@@ -74,39 +95,7 @@ export function admitRunner(scope, runner, reason) {
74
95
  noteSkip(scope.root, runner, "claim-hold");
75
96
  return "skipped";
76
97
  }
77
- const free = claimGroup.eligible.filter((e) => !claimGroup.held.has(e.key));
78
- const pick = assignNext(free);
79
- // An unassigned runner is told which entries its siblings hold (cleared when none).
80
- const heldTitles = claimGroup.eligible
81
- .filter((e) => claimGroup.held.has(e.key))
82
- .map((e) => e.title);
83
- runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
84
- if (pick) {
85
- runner.state.claim = {
86
- file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
87
- key: pick.key,
88
- title: pick.title,
89
- at: scope.now,
90
- source: "assigned",
91
- };
92
- claimGroup.held.add(pick.key);
93
- logEventBestEffort(scope.root, {
94
- source: runner.role,
95
- type: "claim",
96
- action: "assigned",
97
- key: pick.key,
98
- title: pick.title,
99
- });
100
- }
101
- else if (instanceIndex(runner.role) >= 2 ||
102
- (heldTitles.length > 0 && !freshlyWoken(runner.state))) {
103
- // An idle extra instance, or a primary whose eligible entries siblings all hold
104
- // (unless an operator wake is fresh): no run, no backoff climb.
105
- noteSkip(scope.root, runner, "no-free-entry");
106
- if (scope.once.active)
107
- scope.once.settle(runner.role, "no free entry");
108
- return "skipped";
109
- }
98
+ return assignClaim(scope, runner, claimGroup.eligible, claimGroup.held);
110
99
  }
111
100
  }
112
101
  else {
@@ -114,3 +103,47 @@ export function admitRunner(scope, runner, reason) {
114
103
  }
115
104
  return "admitted";
116
105
  }
106
+ /** Assign the first free eligible entry to an unclaimed admitted runner: record the claim on
107
+ * the runner, add it to the group's held set, and log the assignment. With no free entry, an
108
+ * idle extra instance, or a primary whose eligible entries siblings all hold (unless an
109
+ * operator wake is fresh), stands down rather than run unassigned. */
110
+ function assignClaim(scope, runner, eligible, held) {
111
+ const free = eligible.filter((e) => !held.has(e.key));
112
+ const pick = assignNext(free);
113
+ // An unassigned runner is told which entries its siblings hold (cleared when none).
114
+ const heldTitles = eligible.filter((e) => held.has(e.key)).map((e) => e.title);
115
+ runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
116
+ if (pick) {
117
+ recordAssignedClaim(scope, runner, pick, held);
118
+ return "admitted";
119
+ }
120
+ else if (instanceIndex(runner.role) >= 2 ||
121
+ (heldTitles.length > 0 && !freshlyWoken(runner.state))) {
122
+ // An idle extra instance, or a primary whose eligible entries siblings all hold
123
+ // (unless an operator wake is fresh): no run, no backoff climb.
124
+ noteSkip(scope.root, runner, "no-free-entry");
125
+ if (scope.once.active)
126
+ scope.once.settle(runner.role, "no free entry");
127
+ return "skipped";
128
+ }
129
+ return "admitted";
130
+ }
131
+ /** Build the runner's claim record, add the picked entry to the group's held set, and log the
132
+ * assignment to the events feed. */
133
+ function recordAssignedClaim(scope, runner, pick, held) {
134
+ runner.state.claim = {
135
+ file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
136
+ key: pick.key,
137
+ title: pick.title,
138
+ at: scope.now,
139
+ source: "assigned",
140
+ };
141
+ held.add(pick.key);
142
+ logEventBestEffort(scope.root, {
143
+ source: runner.role,
144
+ type: "claim",
145
+ action: "assigned",
146
+ key: pick.key,
147
+ title: pick.title,
148
+ });
149
+ }