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
@@ -7,7 +7,7 @@ import { errCode } from "../errno.js";
7
7
  import { primaryWorktreeLockPath } from "../paths.js";
8
8
  /** How old a lock dir must be before it is stale on age alone — regardless of whether its
9
9
  * recorded pid still looks alive, so a reused pid cannot latch a dead holder as live. */
10
- const STALE_MS = 10 * 60 * 1000;
10
+ const STALE_MS = 10 * 60_000;
11
11
  /** A creator publishes its lock dir with the holder's pid already inside (atomic rename), so a
12
12
  * visible lock always carries a readable pid. This grace remains for a pid-less dir an older
13
13
  * build or an interrupted hand operation left behind: past it, the dir is an orphan and safe
@@ -93,6 +93,10 @@ function releaseOwnedLock(dir) {
93
93
  return;
94
94
  rmLockDir(dir);
95
95
  }
96
+ /** The ledger-facing release primitive: the same releaseOwnedLock every mutex exits with, but
97
+ * exported so src/concurrency/check-slots.ts can hand a suite's slot back without re-implementing
98
+ * the owned-pid guard (a holder whose lock was stolen must not delete its successor's dir). */
99
+ export const releaseOwnedLockDir = releaseOwnedLock;
96
100
  function acquireStep(dir, timeoutMs, waitedMs) {
97
101
  // An existing path is a held lock to wait on (and maybe break); publishLockDir is only for
98
102
  // an absent path, so a read-only parent that already holds a stale lock reports contention
@@ -149,6 +153,24 @@ function publishLockDir(dir) {
149
153
  throw err;
150
154
  }
151
155
  }
156
+ /** The one-shot claim step of the lock protocol, exported for a caller that wants a single
157
+ * best-effort claim rather than withLock's retry loop: claim `dir` by atomic publish, returning
158
+ * true on success and false when `dir` already exists (a live holder) or contention — never
159
+ * throwing, unlike publishLockDir. The suite-slot ledger (check-slots.ts) calls it per index as
160
+ * it walks a ledger, so it must not throw on a held slot to inspect the others. A genuine
161
+ * filesystem failure (ENOSPC, a read-only parent) also returns false; the ledger degrades to
162
+ * "run unpermitted" on an unable-to-create ledger, which suits a best-effort scheduling cap. */
163
+ export function tryClaimLockDir(dir) {
164
+ try {
165
+ if (fs.existsSync(dir))
166
+ return false; // A held slot (live or stale) — the caller walks on.
167
+ publishLockDir(dir);
168
+ return true;
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ }
152
174
  /** Remove `.acquiring-*` temp dirs left beside a lock by a creator that crashed before its
153
175
  * rename — the one remnant atomic publication can leave. Only dirs older than STALE_MS are
154
176
  * touched, far beyond any scheduling stall, so a live creator's in-flight temp is never
@@ -0,0 +1,49 @@
1
+ /** The AsyncLocalStorage store that carries a run's lease across its work: a lease rides here
2
+ * for the length of a run, so an idle stretch inside it (a failed review's in-place retry
3
+ * pause, called via idleWithoutPermit) can hand the team permit back and re-take it, letting a
4
+ * parked run use the slot meanwhile. The check-permit.ts `holdingPermit` store is the pattern:
5
+ * nothing here is a separate permit, only the single lease the run path already holds. */
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ const runPermitStore = new AsyncLocalStorage();
8
+ /** Run `run` with `lease` in the run-permit store, so an idle wait inside it can pause and
9
+ * resume the same team permit. The same lease object is released by the caller after the store
10
+ * is unwrapped. */
11
+ export function withRunPermit(lease, run) {
12
+ return runPermitStore.run({ lease }, run);
13
+ }
14
+ /** Run `wait`, an idle stretch inside the calling lease's work, with the lease's team permit
15
+ * handed back (SemaphoreLease.pause), so a parked run can use it meanwhile, then take it back
16
+ * (resume) before returning. A failed review's in-place retry pause is the caller: with one
17
+ * team permit, a reviewer that kept failing held it through two 30 s pauses and starved every
18
+ * other role. Once `signal` has fired, nothing is taken back and the caller's release() settles
19
+ * a still-paused lease. No-op without a lease in context. */
20
+ export async function idleWithoutPermit(wait, signal) {
21
+ const ctx = runPermitStore.getStore();
22
+ if (!ctx)
23
+ return wait();
24
+ ctx.lease.pause();
25
+ try {
26
+ await wait();
27
+ }
28
+ finally {
29
+ if (!signal?.aborted)
30
+ await untilAborted(ctx.lease.resume(), signal);
31
+ }
32
+ }
33
+ /** `p`, or an earlier return once `signal` fires: a resume parked on a full team must not keep an
34
+ * aborted verification waiting for a permit it no longer needs. */
35
+ async function untilAborted(p, signal) {
36
+ if (!signal)
37
+ return p;
38
+ let onAbort = () => { };
39
+ const aborted = new Promise((resolve) => {
40
+ onAbort = resolve;
41
+ signal.addEventListener("abort", onAbort, { once: true });
42
+ });
43
+ try {
44
+ await Promise.race([p, aborted]);
45
+ }
46
+ finally {
47
+ signal.removeEventListener("abort", onAbort);
48
+ }
49
+ }
@@ -1,39 +1,19 @@
1
1
  /** In-process counting semaphore bounding concurrent work (the orchestrator uses it to
2
2
  * cap simultaneous pi runs). The cross-PROCESS mutex lives in lock.ts; this one only
3
- * coordinates within a single harness process.
4
- *
5
- * Since 2026-10-15 it also bounds a model provider (a "key") independently, so a full
6
- * provider never blocks a waiter on another (plans/provider-concurrency.md §3). The keyless
7
- * entry points — acquire/tryAcquire/release — are unchanged; only acquireLease, forceLease
8
- * and setKeyCaps touch the keyed path. */
9
- /** The entries of a `maxConcurrentPerProvider` map the semaphore enforces: positive integers
10
- * only, so a non-positive or non-integer cap is dropped rather than read as a limit. */
11
- export function enforcedKeyCaps(caps) {
12
- return Object.entries(caps ?? {}).filter(([, v]) => Number.isInteger(v) && v > 0);
13
- }
3
+ * coordinates within a single harness process. A run is admitted by the one team total
4
+ * (`maxConcurrent`) alone — there is no per-provider cap any more (the cap, its surfaces
5
+ * and its keyed machinery were subtracted in parts 5b/6 and 5i/6); a lease is a team
6
+ * permit that can pause and resume so an idle wait (a failed review's in-place retry)
7
+ * hands the slot back and re-takes it before the next attempt. */
14
8
  export class Semaphore {
15
9
  capacity;
16
- /** A parked WAITING (admission) waiter: its scheduling tier, optional provider key, and the
17
- * resolver that hands it the permit. Keyless waiters (the legacy path) carry no key and are
18
- * never held by a key cap. */
10
+ /** A parked WAITING (admission) waiter: its scheduling tier and the resolver that hands
11
+ * it the permit. */
19
12
  waiters = [];
20
- /** A parked re-key waiter: it already holds a team permit, so it waits only for its key's
21
- * headroom, ahead of every admission waiter (which would otherwise queue a team permit uselessly
22
- * behind another provider's runs). `granted` flips when drive() hands the waiter the target key's
23
- * slot, so the lease's release can tell a delivered grant (give it back) from a still-parked one
24
- * (cancel it) when the two interleave. */
25
- rekeyWaiters = [];
26
13
  /** Permits currently held, tracked separately from capacity (rather than as an "available"
27
14
  * count): setCapacity can shrink below the current in-use, and a free-permit count cannot
28
15
  * represent that state — it would over-admit new work after a shrink. */
29
16
  inUse = 0;
30
- /** The per-key permits currently held: a lease or forced lease charges its key here, and a
31
- * key at its cap withholds further grants on it. Grows and shrinks only through release and
32
- * rekey — never preempted. */
33
- keyInUse = new Map();
34
- /** The live per-key caps, from `maxConcurrentPerProvider` (plans/provider-concurrency.md
35
- * §1). An absent key is uncapped — bounded by `capacity` alone. */
36
- keyCaps = new Map();
37
17
  /** Permits withheld from tier ≥ 1 acquirers so a work role that has work to take can claim
38
18
  * one without waiting for a maintenance run to release (plans/parallel-work-instances.md
39
19
  * "Priority headroom", part 7/7). Tiers ≤ 0 (work, VERIFY_TIER, MERGE_TIER) ignore it. */
@@ -45,13 +25,12 @@ export class Semaphore {
45
25
  get limit() {
46
26
  return this.capacity;
47
27
  }
48
- /** How many acquirers are parked for a permit right now, admission and re-key waiters both.
49
- * With no keys there are never re-key waiters, so this is exactly today's count. */
28
+ /** How many acquirers are parked for a permit right now. */
50
29
  get waiting() {
51
- return this.waiters.length + this.rekeyWaiters.length;
30
+ return this.waiters.length;
52
31
  }
53
32
  /** Take a free permit without parking: true when one was granted (the same fast path
54
- * acquire takes), false when acquire would have to wait. Unkeyed, as before. */
33
+ * acquire takes), false when acquire would have to wait. */
55
34
  tryAcquire() {
56
35
  if (this.inUse >= this.capacity)
57
36
  return false;
@@ -62,7 +41,7 @@ export class Semaphore {
62
41
  * requests only: on arrival a waiter inserts ahead of every parked waiter with a strictly
63
42
  * greater tier (the orchestrator passes roleTier so work roles beat maintenance across
64
43
  * polls), keeping stable FIFO within a tier. In-flight holders are never preempted — they
65
- * run to completion and their release is what hands out the permit. Unkeyed, as before. */
44
+ * run to completion and their release is what hands out the permit. */
66
45
  async acquire(tier) {
67
46
  if (this.inUse < this.capacity - this.reserveFor(tier)) {
68
47
  this.inUse += 1;
@@ -72,25 +51,16 @@ export class Semaphore {
72
51
  this.enqueue({ tier, resolve });
73
52
  });
74
53
  }
75
- /** Admit by both the team total and an optional provider key at once, waiting until both
76
- * hold, and return a lease handle. `tier` orders the wait exactly as acquire does; `key`
77
- * bounds the permit by its provider cap: granted when `inUse < capacity − reserveFor(tier)`
78
- * and the key is under its cap (an absent or uncapped key always passes). FIFO holds within
79
- * a key, and a full key never delays a waiter on another. */
80
- async acquireLease(tier, key) {
81
- let currentKey = key;
54
+ /** Admit by the team total alone and return a lease handle. `tier` orders the wait exactly
55
+ * as acquire does. FIFO holds within a tier. */
56
+ async acquireLease(tier) {
82
57
  // True once release() has run: a released lease owns no slot and must not undo one again —
83
- // a second release (or a parked rekey's late grant) must be a no-op, never a negative
84
- // count. The orchestrator releases each lease exactly once, but the rekey cancel path
85
- // relies on this guard so a cancelled lease never re-decrements its target key.
58
+ // a second release must be a no-op, never a negative count. The orchestrator releases each
59
+ // lease exactly once, but a cancelled parked resume relies on this guard so a cancelled
60
+ // lease never re-decrements what it took.
86
61
  let closed = false;
87
- // The lease's parked re-key, while it waits on a full target key. At that point the lease
88
- // holds NO key slot (the old one was returned at the rekey's start), so release() must
89
- // CANCEL the pending grant rather than undo a slot this lease never took. Null when no
90
- // rekey is parked.
91
- let parkedRekey = null;
92
- // True while pause() has handed the team permit and the key's slot back: release() then owes
93
- // nothing, and resume() re-admits at this lease's tier on its current key.
62
+ // True while pause() has handed the team permit back: release() then owes nothing, and
63
+ // resume() re-admits at this lease's tier in queue order.
94
64
  let paused = false;
95
65
  // resume()'s admission waiter while it is queued. release() cancels it if drive has not
96
66
  // granted it yet, and gives the grant back if it has.
@@ -109,102 +79,46 @@ export class Semaphore {
109
79
  w.resolve();
110
80
  return;
111
81
  }
112
- // drive granted it this microtask: its permit and key slot are taken, so give them back.
82
+ // drive granted it this microtask: its permit is taken, so give it back.
113
83
  paused = false;
114
84
  }
115
85
  if (paused)
116
- return; // pause() already gave the permit and the key's slot back
117
- if (parkedRekey !== null) {
118
- // A re-key is parked waiting on its new key. Cancel it: pull it off the queue (so drive
119
- // can't grant it) and, if the grant already arrived this microtask, give the new key's slot
120
- // back. The lease holds no slot either way, so the team permit is the only thing to free.
121
- const w = parkedRekey;
122
- const idx = this.rekeyWaiters.indexOf(w);
123
- if (idx >= 0)
124
- this.rekeyWaiters.splice(idx, 1);
125
- if (w.granted) {
126
- this.bumpKeyInUse(w.key, -1);
127
- }
128
- w.resolve();
129
- parkedRekey = null;
130
- }
131
- else if (currentKey !== undefined) {
132
- this.bumpKeyInUse(currentKey, -1);
133
- }
86
+ return; // pause() already gave the permit back
134
87
  this.inUse -= 1;
135
88
  this.drive();
136
89
  };
137
- const rekey = async (to) => {
138
- if (to === currentKey || closed)
139
- return;
140
- if (paused) {
141
- // Nothing is held while paused: the move only changes the key resume() admits on.
142
- currentKey = to;
143
- lease.key = to;
144
- return;
145
- }
146
- // Give the old key back first, waking that key's waiters.
147
- if (currentKey !== undefined) {
148
- this.bumpKeyInUse(currentKey, -1);
149
- }
150
- currentKey = to;
151
- lease.key = to;
152
- this.drive();
153
- if (to !== undefined && this.keyAtCap(to)) {
154
- // The new key is full: wait only for its headroom, ahead of every admission waiter.
155
- const w = { key: to, resolve: () => { }, granted: false };
156
- parkedRekey = w;
157
- this.rekeyWaiters.push(w);
158
- await new Promise((resolve) => {
159
- w.resolve = resolve;
160
- });
161
- parkedRekey = null;
162
- if (w.granted)
163
- this.drive();
164
- return;
165
- }
166
- if (to !== undefined)
167
- this.bumpKeyInUse(to, 1);
168
- this.drive();
169
- };
170
90
  const pause = () => {
171
- if (closed || paused || parkedRekey !== null)
91
+ if (closed || paused)
172
92
  return;
173
93
  paused = true;
174
- if (currentKey !== undefined)
175
- this.bumpKeyInUse(currentKey, -1);
176
94
  this.inUse -= 1;
177
95
  this.drive();
178
96
  };
179
97
  const resume = async () => {
180
98
  if (closed || !paused || parkedResume !== null)
181
99
  return;
182
- if (this.canLeaseAtOnce(tier, currentKey)) {
100
+ if (this.canLeaseAtOnce(tier)) {
183
101
  this.inUse += 1;
184
- if (currentKey !== undefined)
185
- this.bumpKeyInUse(currentKey, 1);
186
102
  paused = false;
187
103
  return;
188
104
  }
189
105
  await new Promise((resolve) => {
190
- const waiter = { tier, key: currentKey, resolve };
106
+ const waiter = { tier, resolve };
191
107
  parkedResume = waiter;
192
108
  this.enqueue(waiter);
193
109
  });
194
110
  parkedResume = null;
195
- // drive granted it (the permit and key slot are taken), unless release() cancelled it.
111
+ // drive granted it (the permit is taken), unless release() cancelled it.
196
112
  if (!closed)
197
113
  paused = false;
198
114
  };
199
- const lease = { key: currentKey, release, rekey, pause, resume };
200
- if (this.inUse < this.capacity - this.reserveFor(tier) && (key === undefined || !this.keyAtCap(key))) {
115
+ const lease = { release, pause, resume };
116
+ if (this.inUse < this.capacity - this.reserveFor(tier)) {
201
117
  this.inUse += 1;
202
- if (key !== undefined)
203
- this.bumpKeyInUse(key, 1);
204
118
  return lease;
205
119
  }
206
120
  await new Promise((resolve) => {
207
- this.enqueue({ tier, key, resolve });
121
+ this.enqueue({ tier, resolve });
208
122
  });
209
123
  return lease;
210
124
  }
@@ -221,97 +135,29 @@ export class Semaphore {
221
135
  }
222
136
  this.waiters.splice(i, 0, waiter);
223
137
  }
224
- /** Synchronously report whether acquireLease(tier, key) would return at once — both the
225
- * team total and the key (when set) have headroom right now, and the tier's reserve is
226
- * satisfied. Mirrors the exact fast-path condition acquireLease's first branch uses, so a
227
- * caller can decide whether to show a "waiting for a slot" state before awaiting a lease
228
- * that would actually park (merge-drain.ts's resolver wrapper). */
229
- canLeaseAtOnce(tier, key) {
230
- return this.inUse < this.capacity - this.reserveFor(tier) && (key === undefined || !this.keyAtCap(key));
231
- }
232
- /** The director's bypass: take a key's slot at once, never waiting, and never counting toward
233
- * the team cap. It DOES count toward the key's in-use, so a forced lease can overshoot a
234
- * full key by one holder; no grant on that key happens until a release drains it back under
235
- * the cap. Its re-key also never waits. */
236
- forceLease(key) {
237
- let currentKey = key;
238
- if (currentKey !== undefined) {
239
- this.bumpKeyInUse(currentKey, 1);
240
- }
241
- // True while pause() has given the key's slot back (a forced lease holds no team permit).
242
- let paused = false;
243
- const release = () => {
244
- if (currentKey !== undefined && !paused) {
245
- this.bumpKeyInUse(currentKey, -1);
246
- }
247
- this.drive();
248
- };
249
- const rekey = async (to) => {
250
- if (to === currentKey)
251
- return;
252
- if (currentKey !== undefined && !paused) {
253
- this.bumpKeyInUse(currentKey, -1);
254
- }
255
- currentKey = to;
256
- lease.key = to;
257
- if (to !== undefined && !paused)
258
- this.bumpKeyInUse(to, 1);
259
- this.drive();
260
- };
261
- const pause = () => {
262
- if (paused)
263
- return;
264
- paused = true;
265
- if (currentKey !== undefined)
266
- this.bumpKeyInUse(currentKey, -1);
267
- this.drive();
268
- };
269
- // Like the forced lease itself, taking the slot back never waits.
270
- const resume = async () => {
271
- if (!paused)
272
- return;
273
- paused = false;
274
- if (currentKey !== undefined)
275
- this.bumpKeyInUse(currentKey, 1);
276
- };
277
- const lease = { key: currentKey, release, rekey, pause, resume };
278
- return lease;
279
- }
280
- /** Hand every parked waiter a permit it can take — both team and key headroom — in one
281
- * pass. The one home of the permit hand-off: release, setCapacity, setReserve and setKeyCaps
282
- * all drive it, and growth loops it to fill new headroom. It never admits past the team cap
283
- * or a key cap. Re-key waiters run first (they already hold a team permit, so they wait only
284
- * on their key). Admission waiters are scanned in queue (tier, then FIFO) order: a waiter
285
- * whose key is full is skipped and keeps its place, so a later waiter on a free key is never
286
- * stuck behind it — but the reserve stops the scan at the first tier ≥ 1 waiter it withholds
287
- * from, since the tier-ascending queue means everything behind is withheld too. */
138
+ /** Synchronously report whether acquireLease(tier) would return at once — the team total
139
+ * has headroom right now and the tier's reserve is satisfied. Mirrors the exact fast-path
140
+ * condition acquireLease's first branch uses, so a caller can decide whether to show a
141
+ * "waiting for a slot" state before awaiting a lease that would actually park
142
+ * (merge-drain.ts's resolver wrapper). */
143
+ canLeaseAtOnce(tier) {
144
+ return this.inUse < this.capacity - this.reserveFor(tier);
145
+ }
146
+ /** Hand every parked waiter a permit it can take in one pass. The one home of the permit
147
+ * hand-off: release, setCapacity, setReserve and a pause/resume all drive it, and growth
148
+ * loops it to fill new headroom. It never admits past the team cap. Admission waiters are
149
+ * scanned in queue (tier, then FIFO) order; the reserve stops the scan at the first tier ≥ 1
150
+ * waiter it withholds from, since the tier-ascending queue means everything behind is
151
+ * withheld too. */
288
152
  drive() {
289
153
  for (;;) {
290
- // Re-key waiters first: no team check — they already hold a team permit.
291
154
  let granted = false;
292
- for (let i = 0; i < this.rekeyWaiters.length; i++) {
293
- const w = this.rekeyWaiters[i];
294
- if (this.keyAtCap(w.key))
295
- continue;
296
- w.granted = true;
297
- this.rekeyWaiters.splice(i, 1);
298
- this.bumpKeyInUse(w.key, 1);
299
- w.resolve();
300
- granted = true;
301
- break;
302
- }
303
- if (granted)
304
- continue;
305
155
  for (let i = 0; i < this.waiters.length; i++) {
306
156
  const w = this.waiters[i];
307
157
  if (this.inUse >= this.capacity - this.reserveFor(w.tier))
308
158
  break; // reserve/full stops the scan
309
- if (w.key !== undefined && this.keyAtCap(w.key))
310
- continue; // skip a full key, keep its place
311
159
  this.waiters.splice(i, 1);
312
160
  this.inUse += 1;
313
- if (w.key !== undefined)
314
- this.bumpKeyInUse(w.key, 1);
315
161
  w.resolve();
316
162
  granted = true;
317
163
  break;
@@ -320,23 +166,11 @@ export class Semaphore {
320
166
  break;
321
167
  }
322
168
  }
323
- /** The permits withheld from `tier`: `reserve` for tier ≥ 1 (maintenance), 0 for every tier that
324
- * ships work or merges a change (work, VERIFY_TIER, MERGE_TIER). */
169
+ /** The permits withheld from `tier`: `reserve` for tier ≥ 1 (maintenance), 0 for every tier
170
+ * that ships work or merges a change (work, VERIFY_TIER, MERGE_TIER). */
325
171
  reserveFor(tier) {
326
172
  return tier >= 1 ? this.reserve : 0;
327
173
  }
328
- /** True when `key` is at its cap — a configured cap the in-use already meets or exceeds. An
329
- * uncapped key (no cap set) is never "full". */
330
- keyAtCap(key) {
331
- const cap = this.keyCaps.get(key);
332
- return cap !== undefined && (this.keyInUse.get(key) ?? 0) >= cap;
333
- }
334
- /** Add `delta` to `key`'s held permits, initializing an absent key at zero first — the
335
- * single home of the get-or-init bump that release, rekey, force and drive all repeated
336
- * (an absent key implicitly holds zero, so the first bump must not read `undefined`). */
337
- bumpKeyInUse(key, delta) {
338
- this.keyInUse.set(key, (this.keyInUse.get(key) ?? 0) + delta);
339
- }
340
174
  /** Release a held permit. The finishing work gives back its permit first, and a queued waiter
341
175
  * takes it over only if there is headroom (drive): after a shrink, inUse can sit at or above
342
176
  * the cap for a while (the in-flight work admitted before it), and no new grant may proceed
@@ -360,46 +194,8 @@ export class Semaphore {
360
194
  this.reserve = Math.max(0, n);
361
195
  this.drive();
362
196
  }
363
- /** Live-resize the per-key caps (a mid-run `maxConcurrentPerProvider` edit). Growing admits
364
- * parked waiters for that key up to the new headroom; shrinking never preempts an in-flight
365
- * holder — it only withholds future grants on that key until releases drain it under the new
366
- * cap, the same contract as setCapacity. Non-positive and non-integer entries are dropped. */
367
- setKeyCaps(caps) {
368
- this.keyCaps = new Map(enforcedKeyCaps(caps));
369
- this.drive();
370
- }
371
- /** The pool's live state for observability (plans/provider-concurrency.md §8): the team
372
- * in-use/cap, the reserve, and each referenced key's in-use, cap (undefined when uncapped)
373
- * and waiting count. */
197
+ /** The pool's live state for observability: the team in-use/cap and the reserve. */
374
198
  snapshot() {
375
- const byKey = {};
376
- const addKey = (key) => {
377
- if (key === undefined)
378
- return;
379
- if (byKey[key] === undefined) {
380
- const cap = this.keyCaps.get(key);
381
- const entry = {
382
- inUse: this.keyInUse.get(key) ?? 0,
383
- waiting: 0,
384
- };
385
- if (cap !== undefined)
386
- entry.limit = cap;
387
- byKey[key] = entry;
388
- }
389
- };
390
- for (const k of this.keyCaps.keys())
391
- addKey(k);
392
- for (const k of this.keyInUse.keys())
393
- addKey(k);
394
- for (const w of this.waiters) {
395
- addKey(w.key);
396
- if (w.key !== undefined)
397
- byKey[w.key].waiting += 1;
398
- }
399
- for (const w of this.rekeyWaiters) {
400
- addKey(w.key);
401
- byKey[w.key].waiting += 1;
402
- }
403
- return { inUse: this.inUse, limit: this.capacity, reserve: this.reserve, byKey };
199
+ return { inUse: this.inUse, limit: this.capacity, reserve: this.reserve };
404
200
  }
405
201
  }
@@ -8,10 +8,10 @@
8
8
  * This is config vocabulary, not HTTP vocabulary — it names which tumwater.json keys are
9
9
  * browser-editable — so it lives beside the other config-key lists (config-schema.ts's
10
10
  * TOP_LEVEL_KEYS, config-write.ts's per-key validators) rather than in the endpoint module
11
- * that first consumed it. The four run-concurrency knobs (maxConcurrent,
12
- * maxConcurrentPerProvider, maxConcurrentChecks, worktreeSlots) joined the set because
11
+ * that first consumed it. The run-concurrency knobs (maxConcurrent,
12
+ * maxConcurrentChecks, worktreeSlots) joined the set because
13
13
  * tumwater doctor's own contention checks name them for the operator to raise
14
- * (checkWorktreePool, checkProviderContention, checkCheckContention); the per-role maps and
14
+ * (checkWorktreePool, checkCheckContention); the per-role maps and
15
15
  * customRoles still stay CLI/director territory. The browser side mirrors the set for its labels
16
16
  * (ui/gui/gui-client-settings.ts's SETTINGS_KEYS), a manual copy; the GET /api/config test
17
17
  * pins the served key set to this list, and the settings test pins the browser copy's keys
@@ -23,13 +23,10 @@ export const EDITABLE_CONFIG_KEYS = [
23
23
  "fallback",
24
24
  "maxDailyCostUsd",
25
25
  // The run-concurrency knobs tumwater doctor's contention checks name for the operator to
26
- // raise (checkWorktreePool, checkProviderContention) — grouped so the Settings card that
27
- // shows the advice can apply it.
26
+ // raise (checkCheckContention) — grouped so the Settings card that shows the advice can apply it.
28
27
  "maxConcurrent",
29
- "maxConcurrentPerProvider",
30
28
  "maxConcurrentChecks",
31
29
  "worktreeSlots",
32
- "claudeWindowReserve",
33
30
  "quietHours",
34
31
  "notify",
35
32
  // The per-backend section (plans/agent-backends.md, part 4a/7): the whole `agents` object is