tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,383 +1,48 @@
1
- // vocabulary: plain
2
1
  /** The poll loop's per-runner scheduling pass (src/orchestrator/orchestrator.ts): decide, for each
3
- * runner, whether this poll admits a tick and why — the pause/quiet/cost-cap/failure-hold
2
+ * runner, whether this poll admits a run and why — the pause/quiet/cost-cap/failure-hold
4
3
  * gates, the merge-queue interlock, need-based deferral, and once-mode settlement —
5
- * producing the reasons map orchestrator-launch.ts's launchDueTicks starts ticks from.
4
+ * producing the reasons map orchestrator-launch.ts's launchDueRuns starts runs from.
6
5
  * Split out of runOrchestrator's poll body so the WHY-due policy reads on its own screen;
7
- * the poll body keeps the poll's other passes (config reload, markers, gates, redeploy,
6
+ * orchestrator-poll.ts keeps the poll's other passes (config reload, markers, gates, redeploy,
8
7
  * merge drain) and the launch call itself. */
9
8
  import { increment } from "../collections.js";
10
- import { reviewRunConfig, seamHoldKey } from "../config/config-views.js";
11
- import { deferTickReason, isEligible } from "../scheduling/scheduling.js";
12
- import { BUGFIX_ROLE, DIRECTOR_ROLE, commitTier } from "../roles/roles.js";
13
- import { baseRoleOf, instanceIndex, loopEnabled, loopPaused } from "../roles/loop-ids.js";
14
- import { hasOutstandingWork, saveLoopState } from "../loop/loop-state.js";
15
- import { inboxSize } from "../inbox/inbox.js";
16
- import { briefStatusIncomplete } from "../brief/brief.js";
17
- import { queuedMergeRoles } from "../merge/merge-queue.js";
18
- import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
9
+ import { reviewRunConfig, roleSeamTier, seamHoldKey } from "../config/config-views.js";
10
+ import { openBugs } from "../backlog/backlog.js";
11
+ import { fallbackProbeDuePair } from "../budget/fallback-breaker.js";
12
+ import { modelPairName } from "../budget/budget.js";
13
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
14
+ import { baseRoleOf } from "../roles/runner-ids.js";
19
15
  import { eligibleEntries } from "../backlog/backlog-eligibility.js";
20
- import { CLAIM_IDLE_MAX_MS, assignNext, claimReleaseReason, heldKeys, listedKeys, } from "../scheduling/claims.js";
21
- /** Record why the poll skipped a due loop on its state (LoopState.skipReason), saving only when
22
- * the value changes: a skipped loop gets no tick to save its own state, and a loop that stays
23
- * skipped must not rewrite its file every poll. */
24
- function noteSkip(root, runner, reason) {
25
- if (runner.state.skipReason === reason)
26
- return;
27
- runner.state.skipReason = reason;
28
- saveLoopState(root, runner.state);
29
- }
16
+ import { planBacklogFull } from "../scheduling/claims.js";
17
+ import { admitRunner } from "./scheduling-admit.js";
18
+ import { skipRunner } from "./scheduling-skip.js";
19
+ import { buildSchedulingScope, noteSkip, } from "./scheduling-scope.js";
30
20
  /** Decide which runners are due this poll. Mutates `deferredDue` (and once-mode settlement)
31
- * as its only side effects; returns the reason per admitted runner. */
21
+ * as its only side effects; returns the reason per admitted runner. The once-per-poll
22
+ * precompute lives on the scope scheduling-scope.ts's buildSchedulingScope builds (the
23
+ * merge-queue snapshot, director pin, per-base claim groups, gate predicates and admission
24
+ * state); this per-runner loop reads them all through it. */
32
25
  export async function pollRunnerReasons(ctx) {
33
- const { root, runners, once, now, mainHead, userPaused, quietNow, pausedRoles: pausedRolesSet, capPaused, maintenanceQuota, bootstrapHeld, bootstrapActive, budgetPausedRoles, probeRoles, reviewStrongPaused, roleQuietHeld, heldProviders, windowHeldProviders, reviewHeld, roleProviders, openBugsNow, workBacklogOpen, planBacklogFull, holdForRestart, diskHeld, deferredDue, workMergedSince, } = ctx;
34
- const reasons = new Map();
35
- // Computed ONCE per poll (the plan tight-loop rule, src/brief/brief.ts): the flag means the
36
- // brief's status section is still the placeholder, read once from the resolved brief so the
37
- // whole pass agrees on it — the pass already holds root for its other per-poll reads.
38
- const descriptionIncomplete = briefStatusIncomplete(root);
39
- // Merge-queue interlock data, listed ONCE per poll (was once per runner per poll — each
40
- // queuedMergeFiles() re-lists the merge-queue directory and re-stats every queued entry): the
41
- // check below only asks whether the runner's role has a queued entry. The queue changes
42
- // only when a merge completes or a tick enqueues — both asynchronous events this pass
43
- // observes on its next poll — so one snapshot is as fresh as per-runner reads, and a
44
- // merge that completes mid-pass just keeps its role blocked one extra poll (conservative).
45
- const queuedRoles = queuedMergeRoles(root);
46
- // Parallel work instances (plans/parallel-work-instances.md "Claims", part 4/7): a base role
47
- // with more than one runner assigns each due instance a distinct eligible entry, and releases
48
- // the claims whose entry left, turned ineligible, was disabled or went stale. Computed once
49
- // per poll and only for a multi-runner base, so a single-runner team is unchanged. The
50
- // release runs before assignment so a freed key counts as free below.
51
- const claimGroups = new Map();
52
- const baseCounts = new Map();
53
- for (const runner of runners) {
54
- const base = baseRoleOf(runner.role);
55
- increment(baseCounts, base);
56
- }
57
- for (const [base, count] of baseCounts) {
58
- if (count <= 1)
59
- continue;
60
- const groupRunners = runners.filter((r) => baseRoleOf(r.role) === base);
61
- const eligible = eligibleEntries(root, base);
62
- const eligibleKeys = new Set(eligible.map((e) => e.key));
63
- const listed = listedKeys(root, base);
64
- // A group whose runner is mid-entry with no recorded claim (it took the entry while it was
65
- // the only runner, before a live reload raised `instances`) cannot be told WHICH entry that
66
- // is from state alone. Assigning a free entry to a sibling could then hand it the same
67
- // work: hold the whole group's assignment until that runner settles (merges, drops or goes
68
- // idle), at which point its own staging may have recorded a claim. A busy runner that DOES
69
- // hold a claim is fine — `held` excludes its key and siblings take other entries.
70
- let hold = false;
71
- for (const r of groupRunners) {
72
- const busy = r.state.running ||
73
- queuedRoles.has(r.role) ||
74
- hasOutstandingWork(r.state);
75
- if (r.state.claim === undefined && busy)
76
- hold = true;
77
- const released = claimReleaseReason(r, {
78
- listedKeys: listed,
79
- eligibleKeys,
80
- now,
81
- hasQueuedMerge: queuedRoles.has(r.role),
82
- // Per instance, not per base role: a lowered `instances` leaves the surplus runner in
83
- // place with `loopEnabled` false, and its idle claim must be released so a sibling can
84
- // take the entry instead of the key staying held until the 24h stale sweep.
85
- enabled: loopEnabled(r.config, r.role),
86
- // Only this instance paused (not its whole role; instance 1 under `<role>-1`): a sibling
87
- // can take the entry. With the base role paused no sibling runs, so the claim stays.
88
- paused: loopPaused(pausedRolesSet, r.role) && !pausedRolesSet.has(base),
89
- });
90
- if (released === null)
91
- continue;
92
- const title = r.state.claim?.title ?? "";
93
- r.state.claim = undefined;
94
- // Persist the release: an idle extra instance gets no tick to save its state, so the dead
95
- // claim would stay on disk for status/doctor to show and for a restart to release again.
96
- saveLoopState(root, r.state);
97
- const reason = released;
98
- logEventBestEffort(root, { source: r.role, type: "claim", action: "released", title, reason });
99
- if (reason === "stale") {
100
- warnEventBestEffort(root, r.role, `released "${title}", which a stopped instance was working on (idle ${Math.round(CLAIM_IDLE_MAX_MS / 3_600_000)}h)`);
101
- }
102
- }
103
- claimGroups.set(base, {
104
- eligible,
105
- eligibleKeys,
106
- held: heldKeys(groupRunners, eligibleKeys, listed),
107
- hold,
108
- });
109
- }
110
- // The team gates' block predicate (the director exempt — an explicit human prompt
111
- // outranks any autonomous gate, quiet hours included): the once-mode settle below and
112
- // the start gate must agree on exactly when a team gate holds, so the compound lives
113
- // here once. Quiet hours fold in beside the operator pause and the budget gate — the
114
- // schedule is not probe-worthy the way a paused budget is, so no probeDue exception.
115
- // The per-role windows (quietHoursPerRole) fold in beside the team one as a fourth
116
- // disjunct — a role held by either window (or both, held once) starts no new ticks;
117
- // the director stays exempt by the same role check that covers the other disjuncts.
118
- // The budget hold, per role (part 5c/8): a tier that resolved to pause holds its own roles,
119
- // and — with review on — an unresolvable strong tier holds every role (nothing could merge).
120
- // The probe of a demoted pair pierces the hold for EXACTLY the probed pair's tier's roles:
121
- // the earlier draft's pair-blind pierce lifted the pause team-wide on any due probe.
122
- // Quiet hours fold in beside the operator pause — the schedule is not probe-worthy the way a
123
- // paused budget is.
124
- const budgetHolds = (role) => (reviewStrongPaused || budgetPausedRoles.has(role)) && !probeRoles.has(role);
125
- const operatorPauseBlocks = (role) => (userPaused || quietNow || roleQuietHeld.has(role) || budgetHolds(role)) &&
126
- role !== DIRECTOR_ROLE;
127
- // Fresh operator demand: a queued prompt or a wake newer than the loop's last tick end. The
128
- // same override the need-based deferral honors (an explicit "try again now" is not idle
129
- // maintenance); shared by the bootstrap and maintenance-allowance holds so the two cannot
130
- // drift on what counts as demand.
131
- const hasDemand = (runner) => inboxSize(root, runner.role) > 0 ||
132
- (runner.state.wokenAt !== undefined && runner.state.wokenAt > (runner.state.lastRunEndedAt ?? 0));
133
- // The new-project bootstrap hold (part 2/2): held unless the loop has a fresh operator wake
134
- // or a queued prompt.
135
- const bootstrapHeldNow = (runner) => bootstrapHeld.has(runner.role) && !hasDemand(runner);
136
- // The maintenance allowance's team-level hold (part 1b/4): `used` has reached `allowance`,
137
- // so no maintenance loop starts unless it carries demand (`tumwater wake <maintenance-role>`
138
- // runs exactly one tick). The per-poll admission headroom is spent separately, after each
139
- // loop's due check, where the pass knows what actually ticks; the verdict alone cannot.
140
- const maintenanceQuotaOverLine = (runner) => commitTier(baseRoleOf(runner.role)) === "maintenance" &&
141
- maintenanceQuota.used >= maintenanceQuota.allowance &&
142
- !hasDemand(runner);
143
- // The one-poll admission budget: each due maintenance loop this pass admits spends one of
144
- // the remaining `allowance - used` slots, reset every pass, so two due maintenance loops
145
- // cannot start in the same poll and overshoot the allowance.
146
- let maintenanceQuotaAdmitted = 0;
147
- // The stand-down's eligible lists per base role, read at most once per poll (see the check
148
- // below).
149
- const eligibleMemo = new Map();
150
- for (const runner of runners) {
151
- // Once mode: a paused role runs no tick this round and must be reported as skipped,
152
- // so it settles here — before the gates that would otherwise skip it silently (a
153
- // once round has to end even when a pause marker is left over; a later resume within
154
- // the same round cannot un-settle it, which is the at-most-one-tick contract).
155
- if (once.active &&
156
- !once.isSettled(runner) &&
157
- !runner.state.running &&
158
- (loopPaused(pausedRolesSet, runner.role) ||
159
- capPaused.has(runner.role) ||
160
- maintenanceQuotaOverLine(runner) ||
161
- bootstrapHeldNow(runner) ||
162
- operatorPauseBlocks(runner.role))) {
163
- once.settle(runner.role, "paused");
164
- }
165
- // Once mode's at-most-one-tick contract (src/scheduling/once-round.ts): a role that already ran
166
- // its tick — or was settled with a skip reason — runs nothing further this round, even
167
- // when its backoff has expired or the clock override would admit it again. Without this
168
- // gate the one-tick guarantee held only for deferrable built-ins (their deferral
169
- // settles them); a custom loop or work role with a short backoff re-qualified every
170
- // poll and the round never ended.
171
- if (once.active && !runner.state.running && once.isSettled(runner))
172
- continue;
173
- if (holdForRestart || diskHeld)
174
- continue; // a restart or a low disk is pending: nothing new starts, on any loop
175
- // The per-role pause gates BEFORE the team check and exempts nothing — the director
176
- // included (the operator named that one loop deliberately).
177
- if (loopPaused(pausedRolesSet, runner.role))
26
+ const scope = await buildSchedulingScope(ctx);
27
+ const { root, writing, reasons } = scope;
28
+ for (const runner of scope.ordered) {
29
+ // The WHY-due policy: every gate that decides this runner is due or skipped, and the
30
+ // once/deferral bookkeeping that travels with them, lives in scheduling-skip.ts's
31
+ // skipRunner. What follows is only the claim-admission tail (part 3/3, scheduling-admit.ts).
32
+ const verdict = await skipRunner(scope, runner);
33
+ if (verdict.skip)
178
34
  continue;
179
- // The role's own daily cost cap (src/gates/role-cap-gates.ts): no start-gate change — a
180
- // parked waiter finishes (in-flight ticks finish, NEW ticks are gated at scheduling,
181
- // exactly like every per-role pause), and there is no fallback-probe exception: the
182
- // stop is about spend, and probing it adds noise, not signal. The lift is a live
183
- // config edit or local midnight — no marker exists for `resume --role` to touch.
184
- if (capPaused.has(runner.role))
185
- continue;
186
- // The maintenance allowance (plans/work-ratio.md, part 1b/4): no new maintenance tick while
187
- // the rolling 24 h window is over its line. A fresh operator wake or a queued prompt admits
188
- // one anyway (maintenanceQuotaOverLine), the same demand override the bootstrap hold honors.
189
- if (maintenanceQuotaOverLine(runner))
190
- continue;
191
- // The new-project bootstrap (part 2/2): no new maintenance ticks until the plan target is
192
- // met; a fresh wake or queued prompt admits one anyway (bootstrapHeldNow).
193
- if (bootstrapHeldNow(runner))
194
- continue;
195
- if (operatorPauseBlocks(runner.role))
196
- continue; // no new role ticks while either gate holds
197
- // The director honors only the subscription-window hold: the per-run 429 retry that backs
198
- // its other exemptions does not answer a window rejection, so its prompt would fail and
199
- // re-queue every poll until the repeated-failure pause stops it.
200
- if (runner.role === DIRECTOR_ROLE && windowHeldProviders.has(roleProviders.get(runner.role)))
201
- continue;
202
- if (runner.role !== DIRECTOR_ROLE &&
203
- (reviewHeld || heldProviders.has(roleProviders.get(runner.role))))
204
- continue; // nor while ITS provider's failure storm holds — or the reviewer's (nothing could merge)
205
- // The loop's OWN queue decides inbox due-ness: the director counts its historical
206
- // inbox, every other loop its per-role queue (tumwater prompt --role <id>) — so a
207
- // queued prompt makes its loop due by itself, no wake marker needed (isEligible).
208
- const { run, reason } = isEligible(runner, now, mainHead, inboxSize(root, runner.role), {
209
- once: once.active,
210
- bootstrapActive,
211
- descriptionIncomplete,
212
- });
213
- if (!run) {
214
- // Not due this poll: any deferral episode has ended (or never started). No event —
215
- // the tick's own events cover it.
216
- if (deferredDue.get(runner.role))
217
- deferredDue.set(runner.role, false);
218
- // Once mode: idle and not due — settleSkipped records the skip reason (the caller's
219
- // summary reports it).
220
- if (once.active && !runner.state.running)
221
- once.settleSkipped(runner);
35
+ const reason = verdict.reason;
36
+ // Post-gate admission (part 3/3, scheduling-admit.ts): the stand-down turn-down and the
37
+ // parallel-work-instance claim assignment, after every skip gate has admitted this runner.
38
+ // Returns "skipped" having recorded its skip reason and settled its once answer; "admitted"
39
+ // runs the binder: the final noteSkip, the reasons entry and the writing increment.
40
+ if (admitRunner(scope, runner, reason) === "admitted") {
222
41
  noteSkip(root, runner, undefined);
223
- continue;
42
+ reasons.set(runner, reason);
43
+ if (scope.claimGroups.has(baseRoleOf(runner.role)) && !runner.state.running)
44
+ increment(writing, baseRoleOf(runner.role));
224
45
  }
225
- // Merge queue 3/5 interlock (invariant 3): a role with a QUEUED or IN-FLIGHT merge never
226
- // starts a tick — the entry stays in the queue until its merge completes, so one check
227
- // covers both. Uniform over every role, director included: its prompt is not finished until it
228
- // merges. Placed before the need-based deferral block on purpose — no episode bookkeeping is
229
- // needed (the tick that queued the merge ran, which closed any deferral episode; its `queued`
230
- // can leave a prior no_change in lastResult — loop-state.ts records only completed results
231
- // — but the merge's outcome replaces it before the interlock lets the role back into this
232
- // pass), and skipping here saves that branch's workMergedSince git-range query for the skipped
233
- // role.
234
- if (queuedRoles.has(runner.role)) {
235
- noteSkip(root, runner, undefined);
236
- continue;
237
- }
238
- // Need-based deferral: a due maintenance tick (scheduled or main-moved wake) whose last
239
- // tick did nothing stays deferred while the feature/bugfix backlog is open or no new
240
- // work has merged to react to — nextRunAt is left untouched, so it re-checks every poll
241
- // until the backlog drains and qualifying work merges. Resume wakes precede this check in
242
- // isEligible, the director's "inbox" reason skips it, work roles are not in
243
- // DEFERRABLE_ROLES (bugfix is the exception while its backlog is empty — deferTick), and
244
- // a fresh operator wake overrides it (deferTick's woken check — an explicit "try again
245
- // now" is a demand, not idle maintenance). Sitting before reasons.set also keeps a
246
- // deferred role out of the wake-event pass below.
247
- if (reason === "scheduled" || reason === "main moved") {
248
- const s = runner.state;
249
- // The git range is only consulted when the other conditions already hold — a
250
- // never-ticked role or a tick with pending business runs without paying for it, and an
251
- // open backlog defers regardless of what merged. bugfix in search mode is the
252
- // exception: its deferral keys on the work-merged verdict alone, so the backlog-open
253
- // shortcut must not stand in for the query.
254
- const searchBugfix = baseRoleOf(runner.role) === BUGFIX_ROLE && !openBugsNow;
255
- const merged = (!workBacklogOpen || searchBugfix) && s.lastMainHead !== ""
256
- ? await workMergedSince.since(s.lastMainHead, mainHead)
257
- : true;
258
- const deferral = deferTickReason(s, runner.role, merged, workBacklogOpen, openBugsNow, now, planBacklogFull);
259
- const deferredNow = deferral !== null;
260
- if (deferredNow !== (deferredDue.get(runner.role) ?? false)) {
261
- if (deferral)
262
- logEventBestEffort(root, { source: runner.role, type: "run_deferred", reason: deferral });
263
- deferredDue.set(runner.role, deferredNow);
264
- }
265
- if (deferredNow) {
266
- // Once mode: a deferred maintenance tick decided not to run — its once-round
267
- // answer, so it settles instead of holding the round open for DEFER_MAX_MS.
268
- if (once.active)
269
- once.settle(runner.role, "deferred");
270
- noteSkip(root, runner, deferral === "backlog"
271
- ? "deferred-backlog"
272
- : deferral === "plans-full"
273
- ? "deferred-plans-full"
274
- : "deferred-no-work");
275
- continue;
276
- }
277
- }
278
- else if (deferredDue.get(runner.role)) {
279
- deferredDue.set(runner.role, false); // an inbox/resume run ends the episode
280
- }
281
- // The allowance's in-flight admission (plans/work-ratio.md, part 1b/4): `used` counts the
282
- // maintenance loops already in flight, so a due maintenance tick admitted here spends one
283
- // of the remaining `allowance - used` slots. This must sit after the due check — the pure
284
- // verdict cannot know which loops will be eligible this poll, so the reservation lives in
285
- // the pass that does; without it two due maintenance loops start in the same poll. Demand
286
- // spends no slot: it is an explicit operator request, not idle maintenance.
287
- if (commitTier(baseRoleOf(runner.role)) === "maintenance" && !hasDemand(runner)) {
288
- if (maintenanceQuotaAdmitted >= maintenanceQuota.allowance - maintenanceQuota.used) {
289
- noteSkip(root, runner, undefined);
290
- continue;
291
- }
292
- maintenanceQuotaAdmitted++;
293
- }
294
- // Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
295
- // every gate has admitted this runner. A runner that already holds a claim proceeds — its
296
- // revision, resume or retry is the same entry. An unclaimed runner with outstanding work
297
- // (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
298
- // holding it too would deadlock the whole group, since only its tick can settle the hold.
299
- // Otherwise, when the tick is not an inbox/resume run, it takes the first free entry; an
300
- // extra instance with none free is skipped without touching its backoff, and the primary
301
- // runs unassigned only while some eligible entry exists (all of them held by siblings).
302
- // Stand-down: the feature charter says "If PLANS.md is empty or everything is done, there is
303
- // nothing to do" and the bugfix charter says "when every open entry carries [a Refused note],
304
- // there is nothing to do", so a feature runner (primary included) with no eligible plan at all,
305
- // or a bugfix runner whose open bugs are all held (no open bugs at all is search mode, which
306
- // still runs), skips its scheduled and main-moved ticks instead of paying a model run that
307
- // can only end in
308
- // nothing-to-do. Startup, inbox and resume ticks, a fresh operator wake, and a runner with a
309
- // claim or outstanding work still run. nextRunAt and backoff stay put, so the first poll
310
- // after a plan turns eligible ticks it.
311
- const standDownBase = baseRoleOf(runner.role);
312
- if ((standDownBase === "feature" || (standDownBase === BUGFIX_ROLE && openBugsNow)) &&
313
- (reason === "scheduled" || reason === "main moved") &&
314
- !(runner.state.wokenAt !== undefined && runner.state.wokenAt > (runner.state.lastRunEndedAt ?? 0)) &&
315
- runner.state.claim === undefined &&
316
- !hasOutstandingWork(runner.state)) {
317
- let eligible = eligibleMemo.get(standDownBase);
318
- if (eligible === undefined) {
319
- eligible = claimGroups.get(standDownBase)?.eligible ?? eligibleEntries(root, standDownBase);
320
- eligibleMemo.set(standDownBase, eligible);
321
- }
322
- if (eligible.length === 0) {
323
- noteSkip(root, runner, "no-free-entry");
324
- runner.state.heldElsewhere = undefined;
325
- if (once.active)
326
- once.settle(runner.role, "no free entry");
327
- continue;
328
- }
329
- }
330
- const claimGroup = claimGroups.get(baseRoleOf(runner.role));
331
- if (claimGroup) {
332
- if (runner.state.claim) {
333
- // keeps its claim
334
- }
335
- else if (hasOutstandingWork(runner.state)) {
336
- // finishes its own unrecorded entry
337
- }
338
- else if (reason !== "inbox" && reason !== "resume") {
339
- // The group holds an unclaimed mid-entry sibling: assign nothing this poll, or a
340
- // sibling could be handed that same entry (the reported `feature-2`/`bugfix-2`
341
- // duplication). It resolves the moment the sibling settles.
342
- if (claimGroup.hold) {
343
- noteSkip(root, runner, "claim-hold");
344
- continue;
345
- }
346
- const free = claimGroup.eligible.filter((e) => !claimGroup.held.has(e.key));
347
- const pick = assignNext(free);
348
- // An unassigned runner is told which entries its siblings hold (cleared when none).
349
- const heldTitles = claimGroup.eligible.filter((e) => claimGroup.held.has(e.key)).map((e) => e.title);
350
- runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
351
- if (pick) {
352
- runner.state.claim = {
353
- file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
354
- key: pick.key,
355
- title: pick.title,
356
- at: now,
357
- source: "assigned",
358
- };
359
- claimGroup.held.add(pick.key);
360
- logEventBestEffort(root, {
361
- source: runner.role,
362
- type: "claim",
363
- action: "assigned",
364
- key: pick.key,
365
- title: pick.title,
366
- });
367
- }
368
- else if (instanceIndex(runner.role) >= 2) {
369
- noteSkip(root, runner, "no-free-entry");
370
- if (once.active)
371
- once.settle(runner.role, "no free entry");
372
- continue; // an idle extra instance: no tick, no backoff climb
373
- }
374
- }
375
- }
376
- else {
377
- runner.state.heldElsewhere = undefined; // no claim group (e.g. instances lowered to 1)
378
- }
379
- noteSkip(root, runner, undefined);
380
- reasons.set(runner, reason);
381
46
  }
382
47
  return reasons;
383
48
  }
@@ -387,3 +52,37 @@ export async function pollRunnerReasons(ctx) {
387
52
  export function reviewHoldKey(roleConfig) {
388
53
  return seamHoldKey(reviewRunConfig(roleConfig));
389
54
  }
55
+ /** Backlog-aware deferral: while PLANS.md `## Planned` or BUGS.md `## Open` on main
56
+ * holds an entry feature or bugfix may take (eligibleEntries), idle maintenance runs
57
+ * stay deferred — queued feature/bugfix work outranks them regardless of what merged.
58
+ * Stat-cached reads (backlog.ts): one stat per file per poll while the files are unchanged.
59
+ * bugfix is not a maintenance role, but while BUGS.md `## Open` is empty it defers like
60
+ * one (deferRun) — openBugsNow is that emptiness, read once here and reused for both
61
+ * predicates. Also the demoted fallback's half-open window (part 5c/8): the pair whose probe
62
+ * is due, and the roles whose model tier WOULD run on that pair absent the demotion (the
63
+ * price-based resolution — the serving resolution has already dropped the pair, which is
64
+ * exactly why it is being probed). Only those roles pierce the budget hold, and the launch
65
+ * pass admits exactly one of them as the probe — a probe blind to the pair's tier would lift
66
+ * the pause for every role, and a probe with no eligible runner must not hijack the poll's
67
+ * other launches. The review-on strong pause (budgetGate): with the review gate on, an
68
+ * unresolvable strong tier means nothing could merge, so EVERY role is held — the per-tier
69
+ * pause set alone would let small/default roles author changes no reviewer could judge. The
70
+ * probe of the strong pair pierces this hold for exactly the probed tier's roles too. */
71
+ export function pollDeferralInputs(root, runners, liveConfig, gateStates, gates, breakerNowMs) {
72
+ const openBugsNow = openBugs(root).length > 0;
73
+ const workBacklogOpen = eligibleEntries(root, "feature").length > 0 || eligibleEntries(root, "bugfix").length > 0;
74
+ const plansFull = planBacklogFull(root, liveConfig);
75
+ const probePair = fallbackProbeDuePair(gateStates.budget.breakers, breakerNowMs);
76
+ const probeRoles = new Set();
77
+ if (probePair !== null) {
78
+ for (const r of runners) {
79
+ if (r.role === DIRECTOR_ROLE)
80
+ continue;
81
+ const p = gates.priceResolved[roleSeamTier(liveConfig, r.role)].pair;
82
+ if (p !== null && modelPairName(p) === probePair)
83
+ probeRoles.add(r.role);
84
+ }
85
+ }
86
+ const reviewStrongPaused = gates.budgetActive && liveConfig.review.enabled && gates.servingResolved.strong.pair === null;
87
+ return { openBugsNow, workBacklogOpen, plansFull, probePair, probeRoles, reviewStrongPaused };
88
+ }