tumwater 0.4.2 → 0.4.3

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 (305) 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} +25 -12
  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 +1 -8
  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 +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -1,18 +1,16 @@
1
1
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
2
- import { changeBaseRev, headOf, patchId } from "../git/git.js";
2
+ import { changeBaseRev, headOf } from "../git/git.js";
3
3
  import { aheadOfMainFiles } from "../git/git-diff.js";
4
4
  import { resetWorktreeToMain } from "../git/worktree.js";
5
5
  import { recordReview } from "../run/run-apply.js";
6
- import { reviewedAfterMerge } from "../config/config-views.js";
7
- import { errorMessage, parseNonNegativeInt } from "../text/text.js";
6
+ import { sleepInterruptible } from "../run/run-timing.js";
7
+ import { parseNonNegativeInt } from "../text/text.js";
8
8
  import { firstReason } from "../text/phrases.js";
9
9
  import { isExemptDiff } from "./exemptions.js";
10
- import { idleWithoutProviderLease } from "../concurrency/provider-lease.js";
11
- import { setMergeStage } from "../merge/merge-slot.js";
10
+ import { idleWithoutPermit } from "../concurrency/run-permit.js";
12
11
  import { gateBuildPrecheck } from "./review-precheck.js";
13
12
  import { diffRuleReasons } from "./diff-rules.js";
14
- export { REVIEW_FAILURE_LIMIT } from "./review-run.js";
15
- import { restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
13
+ export { REVIEW_FAILURE_LIMIT, restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
16
14
  /** In-place retries of a failed reviewer run within one verify, after the first run. */
17
15
  const REVIEW_RETRIES = 2;
18
16
  /** The pause before each retry (ReviewContext.retryDelayMs overrides it in tests). */
@@ -23,89 +21,40 @@ const REVIEW_RETRY_DELAY_ENV = "TUMWATER_REVIEW_RETRY_DELAY_MS";
23
21
  /** The pause before each retry: the context override wins; else the tunable env knob read through
24
22
  * the shared plain-decimal rule (parseNonNegativeInt — a 0 ms delay, the test-suite's setting, is
25
23
  * valid). A malformed spelling falls back to REVIEW_RETRY_DELAY_MS rather than silently becoming
26
- * NaN: `sleepUnlessAborted(NaN)` resolves at ~0 ms and the retries would fire back-to-back instead
24
+ * NaN: `sleepInterruptible(NaN)` resolves at ~0 ms and the retries would fire back-to-back instead
27
25
  * of 30 s apart. */
28
26
  export function reviewRetryDelayMs(ctxRetryDelayMs) {
29
27
  const raw = process.env[REVIEW_RETRY_DELAY_ENV];
30
28
  return ctxRetryDelayMs ?? (raw === undefined ? REVIEW_RETRY_DELAY_MS : (parseNonNegativeInt(raw) ?? REVIEW_RETRY_DELAY_MS));
31
29
  }
32
- /** A second pool slot for the gate check, or null when the context cannot lease one, none is free
33
- * right now, or the lease failed (the check then runs first, in the change's own slot). */
34
- async function leaseCheckSlot(ctx, head) {
35
- if (!ctx.leaseCheckSlot)
36
- return null;
37
- try {
38
- return await ctx.leaseCheckSlot(head);
39
- }
40
- catch (err) {
41
- warnEventBestEffort(ctx.root, ctx.role, `no second workspace for the gate check (${errorMessage(err)}); running it before the review`);
42
- return null;
43
- }
44
- }
45
- /** Start the gate check in `lease`'s checkout of `head`. A rejection is recorded, not applied:
46
- * resetting the change's branch is left to reviewAttempt, after the reviewer has stopped. */
47
- function startOverlappedCheck(ctx, state, head, lease) {
48
- const overlap = { abort: new AbortController(), done: false };
49
- const reject = async (reasons) => {
50
- overlap.rejectedReasons = reasons;
51
- return { decision: "rejected", detail: firstReason(reasons) };
52
- };
53
- overlap.gate = gateBuildPrecheck({ ...ctx, wt: lease.dir }, state, head, reject)
54
- .then((pre) => {
55
- overlap.done = true;
56
- if (pre.resolved)
57
- overlap.abort.abort();
58
- else
59
- setMergeStage(ctx.root, ctx.role, "reviewing");
60
- return pre;
61
- }, (err) => {
62
- overlap.done = true;
63
- overlap.abort.abort();
64
- throw err;
65
- })
66
- .finally(() => lease.release());
67
- overlap.gate.catch(() => { }); // surfaced where reviewAttempt awaits it
68
- return overlap;
69
- }
70
- /** Run the adversarial review gate over everything ahead of main in `wt` and update `state`
30
+ /** Run the deterministic review gate over everything ahead of main in `wt` and update `state`
71
31
  * accordingly. Shared by the run path (after commitAll) and leftover recovery
72
- * (leftover.ts's recoverLeftover) — every path
32
+ * (runner-leftover.ts's recoverLeftover) — every path
73
33
  * that can move a commit into main routes through here, so no crash or abort path smuggles
74
34
  * unreviewed work in:
75
35
  * - gate disabled, or an already-approved HEAD, or an exempt (doc-only) diff → merge as-is;
76
- * - a new HEAD carrying an approved patch (approvedPatchIds) → approved after the
77
- * build pre-check, with no reviewer run;
78
- * - approve → record lastApprovedHead and its patch-id, and discard the reviewer's stray
79
- * working-tree edits (its only output channel is the verdict);
36
+ * - approve → a passing build pre-check records lastApprovedHead and approves, with no
37
+ * reviewer run; every merged change is judged once, after it lands, by the post-landing
38
+ * review (part 3g/3, reviewMergedCommit, run by the merge pipeline's post-review drain);
80
39
  * - reject → reset the branch to main, record reasons in state.lastReview (injected into the
81
40
  * role's next run prompt), log review_rejected;
82
- * - fail with an unparseable reply (the reviewer ran to completion but emitted no VERDICT) →
83
- * one capped follow-up turn on the reviewer's own session asks for the missing line
84
- * (mirroring requestSummary — BUGS.md 2026-09-29); only a follow-up that also yields
85
- * nothing counts the strike. Leave the commit on the branch for the next run's re-review,
86
- * counting consecutive failures per HEAD; past REVIEW_FAILURE_LIMIT discard the leftover
87
- * with a warning.
88
- * - fail because the run itself failed (transport/spawn/timeout, pi.ok false) → leave the
89
- * commit and do NOT advance the count: the reviewer never judged the diff, so the failure
90
- * is evidence about the backend, never about the commit (BUGS.md 2026-09-20).
91
41
  * - the declared check fails, and fails again on its one re-run → reject with the check's
92
- * reasons, no pi run, when main's own tip is green (or has no verdict); when main is red too,
93
- * fail without advancing the count and leave the commit — the failure is main's. Never throws.
42
+ * reasons when main's own tip is green (or has no verdict); when main is red too, fail
43
+ * without advancing the count and leave the commit — the failure is main's. Never throws.
94
44
  * `highFriction` marks a change whose authoring run burned more than the configured turn/time
95
- * thresholds (plans/refusal-and-thrash.md): the flag rides along in the review prompt so the
96
- * reviewer applies extra scrutiny to whether the work should exist at all. */
97
- async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retry) {
45
+ * thresholds (plans/refusal-and-thrash.md): kept for the caller's convenience while the
46
+ * reviewer's remaining seams stay in review-run.ts/review-followup.ts. */
47
+ async function reviewAttempt(ctx, state, _summary, _commitBody, _highFriction, retry) {
98
48
  const { root, role, wt, mainBranch, config } = ctx;
99
49
  if (!config.review.enabled) {
100
50
  state.reviewResume = undefined;
101
51
  return { decision: "exempt" };
102
52
  }
103
53
  const head = await headOf(wt, "HEAD");
104
- // The one reject path: every rejection of this HEAD — deterministic build-check failure or
105
- // model verdict — shares this bookkeeping. Record lastReview (injected into the role's next
54
+ // The one reject path: every rejection of this HEAD — the deterministic build-check
55
+ // outcome — shares this bookkeeping. Record lastReview (injected into the role's next
106
56
  // run prompt), reset the failure count (a verdict about this HEAD is a successful review
107
57
  // either way), discard the branch by resetting it to main, and log review_rejected.
108
- // Callers attach `run` when a pi run was consumed.
109
58
  const reject = async (reasons, durationMs) => {
110
59
  recordReview(state, "reject", reasons, head);
111
60
  state.reviewResume = undefined;
@@ -120,22 +69,6 @@ async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retr
120
69
  });
121
70
  return { decision: "rejected", detail: firstReason(reasons) };
122
71
  };
123
- // Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
124
- const keep = async (r) => {
125
- await restoreReviewedHead(wt, head, root, role);
126
- return r;
127
- };
128
- // Settle the attempt on an overlapped check that did not pass: the sequential pre-check's own
129
- // outcome, with the reviewer's run (when one started) dropped and no verdict recorded. A
130
- // rejection is applied now, after the reviewer stopped touching the tree; every other outcome
131
- // keeps the change at the head the gate reviewed.
132
- const settleFailedCheck = async (o, pre, pi) => {
133
- state.reviewResume = undefined;
134
- const settled = o.rejectedReasons
135
- ? await reject(o.rejectedReasons)
136
- : await keep(pre.resolved ?? { decision: "failed", detail: "the gate check did not complete" });
137
- return pi ? withRuns(settled, pi, null, null) : settled;
138
- };
139
72
  // Already reviewed this exact HEAD (e.g. a merge_blocked retry): do not burn another run.
140
73
  if (state.lastApprovedHead === head) {
141
74
  state.reviewResume = undefined;
@@ -164,67 +97,34 @@ async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retr
164
97
  return { decision: "exempt" };
165
98
  }
166
99
  // Deterministic build pre-check — after BOTH early returns above (an md-only diff cannot
167
- // break the build) and before any reviewer run or the phase/event that would show
168
- // "reviewing": a deterministic rejection never shows as reviewing on the dashboards. Both
169
- // gate callers (the run path and leftover.ts's recoverLeftover) get it for free. Its run,
170
- // flake re-run, and attribution live in review-precheck.ts (the check run itself lives in
171
- // runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved result
172
- // here is returned verbatim and a pass hands the verified head to the merge path below.
173
- // The pre-check sets the merge cell's stage (a no-op outside a queued merge) —
174
- // "build-check" while it runs, "reviewing" when the reviewer run starts below.
175
- // A retried attempt keeps the first attempt's passing pre-check (a green, or a red naming
176
- // only main's known failures): the tree is the same head.
177
- // A review judges a diff, not a sha: a head carrying exactly the patch this role's last
178
- // approval judged (the approved head cleanly rebased onto a moved main) reuses that verdict
179
- // instead of paying a second reviewer run. Only the model review is reused — the pre-check
180
- // has just run on this tree (a failure rejected it there), and its verifiedHead rides on the
181
- // result as for any approval.
182
- const reviewedPatchId = (await patchId(wt, mainBranch, head)) ?? undefined;
183
- const patchApproved = reviewedPatchId !== undefined && state.approvedPatchIds?.includes(reviewedPatchId) === true;
184
- // The check runs alongside the reviewer when a second pool slot is free right now (Fast merge
185
- // part 12): it starts here, in its own checkout of this head, while the reviewer run below
186
- // starts in the change's own slot. An already-approved patch has no reviewer to overlap, and
187
- // with no free slot (or no way to lease one) the check runs first, as it always did.
188
- const afterMerge = reviewedAfterMerge(config, role);
189
- let overlap;
100
+ // break the build): a deterministic rejection never shows as reviewing on the dashboards.
101
+ // Both gate callers (the run path and runner-leftover.ts's recoverLeftover) get it for free.
102
+ // Its run, flake re-run, and attribution live in review-precheck.ts (the check run itself
103
+ // lives in runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved
104
+ // result here is returned verbatim and a pass hands the verified head to the merge path below.
105
+ // The pre-check always runs in the change's own slot. A retried attempt keeps the first
106
+ // attempt's passing pre-check (a green, or a red naming only main's known failures): the
107
+ // tree is the same head.
190
108
  if (!retry.pre) {
191
- const lease = patchApproved || afterMerge ? null : await leaseCheckSlot(ctx, head);
192
- if (lease)
193
- overlap = startOverlappedCheck(ctx, state, head, lease);
194
- else {
195
- const pre = await gateBuildPrecheck(ctx, state, head, reject);
196
- if (pre.resolved) {
197
- state.reviewResume = undefined;
198
- return pre.resolved;
199
- }
200
- retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
109
+ const pre = await gateBuildPrecheck(ctx, state, head, reject);
110
+ if (pre.resolved) {
111
+ state.reviewResume = undefined;
112
+ return pre.resolved;
201
113
  }
114
+ retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
202
115
  }
203
- const knownMainFailures = retry.pre?.knownMainFailures;
204
- // A role listed in review.afterMerge merges on its gate alone: the deterministic rejects and the
205
- // pre-check above still ran, and the merged commit is reviewed after it lands
206
- // (reviewMergedCommit, run by the merge pipeline's post-review drain).
207
- if (afterMerge && retry.pre) {
208
- state.reviewResume = undefined;
209
- state.lastApprovedHead = head;
210
- return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
211
- }
212
- if (patchApproved && retry.pre) {
213
- state.reviewResume = undefined;
214
- state.lastApprovedHead = head;
215
- return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
216
- }
217
- return runReviewer(ctx, state, {
218
- head, overlap, knownMainFailures, retry, summary, commitBody, highFriction,
219
- reviewedPatchId, reject, keep, settleFailedCheck,
220
- });
116
+ // A passing pre-check approves: the gate runs no model reviewer, and every merge is judged
117
+ // once, after it lands, by the post-landing review (part 3g/3, reviewMergedCommit).
118
+ state.reviewResume = undefined;
119
+ state.lastApprovedHead = head;
120
+ return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
221
121
  }
222
122
  /** Review the change ahead of main, retrying a failed reviewer run in place (twice, 30 s apart)
223
123
  * before the attempt ends `failed`: a run that timed out, was killed, died in transport or at
224
124
  * the provider, or stated no VERDICT after its recovery turn. A resumable session is resumed,
225
125
  * otherwise the retry starts fresh (reviewAttempt). A configuration error and an abort are not
226
126
  * retried. While the reviewer's provider is held the retry waits for the hold to lift and
227
- * costs nothing. Each pause runs without the caller's team permit (idleWithoutProviderLease):
127
+ * costs nothing. Each pause runs without the caller's team permit (idleWithoutPermit):
228
128
  * a verification hands its lease back while it waits and takes it back before the next attempt.
229
129
  * See reviewAttempt for the rest of the gate's contract. */
230
130
  export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFriction) {
@@ -241,10 +141,10 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
241
141
  warnEventBestEffort(ctx.root, ctx.role, `the reviewer run failed (${result.detail ?? "no detail"}) — ${held ? "waiting for the provider hold to lift, then " : ""}retrying the review in place`);
242
142
  // The pause holds no team permit: a verification's lease is handed back while it waits and
243
143
  // re-taken before the next attempt, so a reviewer that keeps failing does not starve the roles.
244
- await idleWithoutProviderLease(async () => {
245
- await sleepUnlessAborted(delayMs, ctx.signal);
144
+ await idleWithoutPermit(async () => {
145
+ await sleepInterruptible(delayMs, ctx.signal);
246
146
  while (ctx.reviewHeld?.() && !ctx.signal?.aborted)
247
- await sleepUnlessAborted(Math.min(delayMs, 5000), ctx.signal);
147
+ await sleepInterruptible(Math.min(delayMs, 5000), ctx.signal);
248
148
  }, ctx.signal);
249
149
  if (ctx.signal?.aborted)
250
150
  return { decision: "failed", aborted: true };
@@ -253,16 +153,3 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
253
153
  retry.attempt += 1;
254
154
  }
255
155
  }
256
- function sleepUnlessAborted(ms, signal) {
257
- return new Promise((resolve) => {
258
- if (signal?.aborted)
259
- return resolve();
260
- const done = () => {
261
- clearTimeout(timer);
262
- signal?.removeEventListener("abort", done);
263
- resolve();
264
- };
265
- const timer = setTimeout(done, ms);
266
- signal?.addEventListener("abort", done, { once: true });
267
- });
268
- }
@@ -32,47 +32,57 @@ export function secretEnvValues(env) {
32
32
  }
33
33
  return out;
34
34
  }
35
- /** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
36
- * `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
37
- * `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
38
- * missing or torn file yields nothing; never throws. */
39
- export function piCredentialValues(modelsPath = piModelsPath()) {
35
+ /** Push `value` into `out` as a credential named `name`, unless it is not a string, is a `$NAME`
36
+ * interpolation or `!command`, or fails `looksLikeSecret`. */
37
+ function addPiSecret(out, name, value) {
38
+ if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
39
+ return;
40
+ if (looksLikeSecret(value))
41
+ out.push({ name, value });
42
+ }
43
+ function modelsJsonCredentials(modelsPath) {
40
44
  const out = [];
41
- const add = (name, value) => {
42
- if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
43
- return;
44
- if (looksLikeSecret(value))
45
- out.push({ name, value });
46
- };
47
45
  const models = readJsonFile(modelsPath);
48
46
  if (models && isJsonObject(models.providers)) {
49
47
  for (const [p, cfg] of Object.entries(models.providers)) {
50
48
  if (!isJsonObject(cfg))
51
49
  continue;
52
- add(`models.json ${p}.apiKey`, cfg.apiKey);
50
+ addPiSecret(out, `models.json ${p}.apiKey`, cfg.apiKey);
53
51
  if (isJsonObject(cfg.headers)) {
54
- for (const [h, v] of Object.entries(cfg.headers))
55
- add(`models.json ${p}.headers.${h}`, v);
52
+ for (const [h, v] of Object.entries(cfg.headers)) {
53
+ addPiSecret(out, `models.json ${p}.headers.${h}`, v);
54
+ }
56
55
  }
57
56
  }
58
57
  }
58
+ return out;
59
+ }
60
+ function authJsonCredentials(modelsPath) {
61
+ const out = [];
59
62
  const auth = readJsonFile(path.join(path.dirname(modelsPath), "auth.json"));
60
63
  if (auth) {
61
64
  for (const [p, cred] of Object.entries(auth)) {
62
65
  if (!isJsonObject(cred))
63
66
  continue;
64
67
  for (const f of ["key", "access", "refresh"])
65
- add(`auth.json ${p}.${f}`, cred[f]);
68
+ addPiSecret(out, `auth.json ${p}.${f}`, cred[f]);
66
69
  if (isJsonObject(cred.env)) {
67
70
  for (const [n, v] of Object.entries(cred.env)) {
68
71
  if (SECRET_NAME.test(n))
69
- add(`auth.json ${p}.env.${n}`, v);
72
+ addPiSecret(out, `auth.json ${p}.env.${n}`, v);
70
73
  }
71
74
  }
72
75
  }
73
76
  }
74
77
  return out;
75
78
  }
79
+ /** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
80
+ * `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
81
+ * `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
82
+ * missing or torn file yields nothing; never throws. */
83
+ export function piCredentialValues(modelsPath = piModelsPath()) {
84
+ return [...modelsJsonCredentials(modelsPath), ...authJsonCredentials(modelsPath)];
85
+ }
76
86
  /** Every secret the gate knows: secret-named environment values (named `$NAME`) then pi's stored
77
87
  * credentials (named `pi <file> <path>`), the first entry winning when two share a value. */
78
88
  export function knownSecrets(env = process.env, modelsPath = piModelsPath()) {
@@ -4,7 +4,7 @@
4
4
  * in roles.ts. Pure catalog data — the shared instruction fragments role prompts embed live in
5
5
  * role-guidance.ts (import-only downward, so prompt assembly depends on both without a cycle).
6
6
  * roles.ts re-exports ROLES and Role, so every existing importer keeps its import path. */
7
- import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
7
+ import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_DEFECT_MARKER, PLAN_DEFECT_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
8
8
  import { roleCommitGrep } from "./legacy-role-ids.js";
9
9
  import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
10
10
  /** The trailer the readme role's commits carry and its `git log --grep` reads back; git parses
@@ -50,8 +50,10 @@ export const ROLES = [
50
50
  6. Mark the plan done in PLANS.md — ${backlogMoveGuidance("PLANS.md", "## Planned", "## Done", "PLANS-DONE.md", "done")}
51
51
  7. Verify last, after the PLANS.md edit, so one run of the project's check covers everything
52
52
  you changed (see "Leave the project working" below).
53
- A plan that resists implementation is a finding: refuse it with the objection recorded rather
54
- than forcing it.`,
53
+ A plan whose approach or criteria are wrong is a finding the plan role owns, not a refusal:
54
+ append ${PLAN_DEFECT_NOTE} under its heading with the objection recorded, skip it, and implement
55
+ the next available plan that fits — do not force it. Reserve **Refused …** for a plan that
56
+ should not be done at all, which is the user's call to decide.`,
55
57
  },
56
58
  {
57
59
  id: "bugfix",
@@ -113,6 +115,12 @@ within that budget, there is nothing to do.`,
113
115
  the quoted objections and the code they name and rewrite the plan so an implementation
114
116
  following it would answer each — correcting the approach or the anchors, tightening the
115
117
  acceptance criteria, or splitting it per PLAN_SIZING — then remove the note.
118
+ - A plan carrying a ${PLAN_DEFECT_MARKER} note (the feature implementer found the plan's
119
+ approach or criteria wrong) also outranks adding another plan: read the objection after the
120
+ marker and the code it names and rewrite the plan so an implementation following it would
121
+ answer it — correcting the approach or the anchors, tightening the acceptance criteria,
122
+ splitting it per PLAN_SIZING, or refusing it with the objection recorded when it should not
123
+ be done at all — then remove the note. That replan is this run's task.
116
124
  - A plan carrying a **Needs review …** note that quotes the reviewer's surviving objections
117
125
  (a final-round rejection — \`rejected after <N> review rounds\`) also outranks adding another
118
126
  plan: read the quoted objections and the code they name, then rewrite the plan so an
@@ -125,11 +125,6 @@ export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${V
125
125
  * feature role appends (gate-notes.ts) also begins with this prefix, so it reads as a Needs-review
126
126
  * note and is classified by entryHold accordingly. */
127
127
  export const NEEDS_REVIEW_PREFIX = `**Needs review `;
128
- /** The tag text before a held count, read by the backlog (backlog-eligibility's heldCount) so
129
- * the note form and the parser cannot drift. No writer emits `(held N)` anymore — the feature
130
- * note is flat — but heldCount still parses the tag from a Needs-review note line already on
131
- * disk until part 2f/6 removes the reader. */
132
- export const HELD_TAG_PREFIX = `(held `;
133
128
  /** The markdown note the feature role appends to a plan it cannot finish in one run, so the plan
134
129
  * role can split it. Mirrors the **Refused …** note convention: a note the next fresh run reads,
135
130
  * with no code parsing it. Defined once so the feature and plan texts and the director's routing
@@ -137,7 +132,7 @@ export const HELD_TAG_PREFIX = `(held `;
137
132
  * flat, so a new hold never escalates to the operator's desk. */
138
133
  export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
139
134
  /** The literal reason the automatic convergence hold writes into a held plan's note
140
- * (holdEntryAfterExhaustion, src/runner/revision.ts, builds the note as
135
+ * (holdEntryAfterExhaustion, src/runner/runner-revision.ts, builds the note as
141
136
  * `**Needs review … by <role>: could not converge — <surviving objections>**`, INV-BH4).
142
137
  * Exported so the writer and the reader share one literal: it sits between holdEntryAfterExhaustion
143
138
  * (which writes the note) and the plan role's charter (role-catalog.ts `find`) which reads it, so
@@ -146,12 +141,21 @@ export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature:
146
141
  * each hold to its own action without matching on a second literal. Add no second spelling of
147
142
  * this text. */
148
143
  export const CONVERGENCE_HOLD_MARKER = "could not converge";
149
- /** The marker a lifted hold leaves behind, so a split or rewrite carried the count instead of
150
- * letting it reset: `**Previously held N**` on the note's line. No writer emits this anymore (the
151
- * plan role's split no longer carries a count) and heldCount no longer reads it, so it is inert;
152
- * withoutHeldForOperatorNotes still drops a surviving `**Previously held N−1**` line beside a
153
- * released at-threshold note until part 2f/6 retires the count machinery. */
154
- export const PRIOR_HELD_PREFIX = `**Previously held `;
144
+ /** The literal marker the feature role writes inside a Needs-review note when a planned entry's
145
+ * approach or acceptance criteria resists implementation, so the plan role re-specifies the plan
146
+ * instead of the refusal landing on the human user's desk. Mirrors the convergence hold: the note
147
+ * reads `**Needs review <YYYY-MM-DD> by feature: plan defect — <objection>**`, carrying this
148
+ * text only in the note body after the prefix, so entryHold classes it as a plain needs-review
149
+ * hold and the feature role (which reads a Needs-review note as a park) skips it. Exported so the
150
+ * feature charter (which tells the feature role to write it) and the plan charter (which reads it
151
+ * to replan) share one literal like the convergence marker does. Add no second spelling of this
152
+ * text. */
153
+ export const PLAN_DEFECT_MARKER = "plan defect";
154
+ /** The template for the replan hand-back note the feature role appends to a planned entry whose
155
+ * approach or criteria are wrong, routing it to the plan role instead of a Refused note. The
156
+ * objection the feature role recorded goes after the marker's dash, so the plan charter can name
157
+ * it (`the objection after the marker`) without parsing the note body itself. */
158
+ export const PLAN_DEFECT_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: ${PLAN_DEFECT_MARKER} — <objection>**`;
155
159
  /** The literal marker the plan role writes as its own line under the PLANS.md `## Planned` entry
156
160
  * that declares this project's deterministic check, and the feature role reads as that entry's
157
161
  * identity. It is the one decision point between the two no-check notes (gate-notes.ts
@@ -133,7 +133,7 @@ function modelLine(p) {
133
133
  /** A fallback pair as one readable token, agent-qualified exactly like modelLine — a non-pi
134
134
  * backend (its `agent`, or a provider that names one, `claude`) is named as the selector's
135
135
  * namespace; a pi pair keeps `provider/model`; neither half present falls back to the
136
- * backend-default label. One home for the Model fallback and Budget fallback lines, so a
136
+ * backend-default label. One home for the Budget fallback line, so a
137
137
  * claude fallback reads `claude/claude-sonnet-5-5`, not a bare id (PLANS.md "Mixed claude +
138
138
  * pi fleets, part 3/4" gap 7). */
139
139
  function fallbackPairLabel(fb, backendDefault) {
@@ -254,13 +254,6 @@ export function renderRoleMarkdown(p, now = Date.now()) {
254
254
  // fallback's own documented state). `unknown` when it cannot be known (a non-pi seam, an
255
255
  // unresolvable pair, a model with no declared window).
256
256
  lines.push(`- Context window: ${p.contextWindow === null ? "unknown" : `${compactTokens(p.contextWindow)} tokens`}`);
257
- // Per-provider concurrency cap (plans/provider-concurrency.md §8): name the key whose cap
258
- // bounds the role's concurrent runs, plus the cap itself. Present only while
259
- // `maxConcurrentPerProvider` maps this seam's key — a cap-less default team has no
260
- // resolvable key entry, so its role view renders no new line and stays byte-identical.
261
- if (p.providerKey !== undefined && p.providerCap !== undefined) {
262
- lines.push(`- Provider: ${p.providerKey} (cap ${p.providerCap})`);
263
- }
264
257
  // Image input (plans/image-input.md "Surfaces", part 5/5): whether this role's own model
265
258
  // reads images, and — when it cannot — where a run that reads an image reroutes. Not just
266
259
  // `yes`/`no`: a text-only role that falls back names the target, so `tumwater role <id>`
@@ -280,12 +273,6 @@ export function renderRoleMarkdown(p, now = Date.now()) {
280
273
  if (p.reviewedAfterMerge) {
281
274
  lines.push(`- Review: after merge (a rejected change stays on main with the reviewer's recorded reasons)`);
282
275
  }
283
- // Model failure fallback, part 2/2: name the off-model pair, when the episode began, and
284
- // the provider-class failure that tripped it, so `tumwater role <id>` explains the state.
285
- if (p.modelFallback) {
286
- const pair = fallbackPairLabel(p.modelFallback, backendDefault(p));
287
- lines.push(`- Model fallback: ${pair} — on fallback since ${formatTimestamp(p.modelFallback.since)} (primary failing: ${p.modelFallback.reason})`);
288
- }
289
276
  if (p.fallback) {
290
277
  const pair = fallbackPairLabel(p.fallback, backendDefault(p));
291
278
  lines.push(`- Over-cap model: ${pair} (${p.fallbackFree ? "free" : "priced"})`);
@@ -19,11 +19,11 @@ import { projectCapHit } from "../budget/budget.js";
19
19
  import { yieldMultiplierFor } from "../scheduling/backoff.js";
20
20
  import { quietHoursStatus } from "../scheduling/quiet-hours.js";
21
21
  import { assembleRunPrompt } from "../run/run-prompt.js";
22
- import { configForRole, resolvedModelFields, reviewedAfterMerge, reviewRunConfig, roleSeamTier, seamConcurrencyKey } from "../config/config-views.js";
23
- import { fallbackPair, modelFallbackView } from "../config/config-fallbacks.js";
22
+ import { configForRole, resolvedModelFields, reviewedAfterMerge, reviewRunConfig, roleSeamTier } from "../config/config-views.js";
23
+ import { fallbackPair } from "../config/config-fallbacks.js";
24
24
  import { fallbackModelPair, piModelsPath, seamContextWindowForRole } from "../pi/pi-models.js";
25
25
  import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
26
- import { formatModelSelector } from "../config/model-selector.js";
26
+ import { formatModelSelector } from "../config/config-model-selector.js";
27
27
  import { roleImageInfo } from "../agent/image-input.js";
28
28
  import { readPiListedModels } from "../pi/pi-listed-models.js";
29
29
  import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
@@ -32,11 +32,11 @@ import { readAccount, windowEntries } from "../agent/subscription-account.js";
32
32
  import { CONFIG_BASENAME, configPath, roleNotesPath } from "../paths.js";
33
33
  import { coverageRows, readQaCoverage } from "../run/qa-coverage.js";
34
34
  import { filingRows, readFeedbackFilings } from "../run/feedback-filings.js";
35
- import { readTextOrNull, statOrNull } from "../files/files.js";
35
+ import { readRegularTextOrNull, statOrNull } from "../files/files.js";
36
36
  import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
37
37
  import { finiteNumber, objectOrEmpty } from "../files/json-object.js";
38
38
  import { canonicalRoleId } from "./legacy-role-ids.js";
39
- import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
39
+ import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
40
40
  /** The roles.<base>.<field> value the FILE actually holds, or undefined when the file never
41
41
  * set that field. Needed only for the seeded per-role fields: overlayDefaults fills every
42
42
  * built-in role's minRunIntervalSeconds from config.ts's catalog, so the merged config cannot
@@ -103,14 +103,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
103
103
  effective.provider === undefined &&
104
104
  effective.model === undefined;
105
105
  const piDefault = runsPiWithoutAModel ? readPiDefaultModel(piSettingsPath(modelsPath), root) : null;
106
- // The role's provider key and its cap (plans/provider-concurrency.md §8): the same pure
107
- // key the orchestrator's admission resolves for this role, answered from the same
108
- // piDefault seam the orchestrator carries, so `tumwater role <id>` names the exact key
109
- // whose cap bounds the role. The cap is the map's entry for that key, absent when no
110
- // `maxConcurrentPerProvider` map is set or the key has no entry (then the role is bounded
111
- // only by the team total, and the cap is omitted).
112
- const providerKey = seamConcurrencyKey(effective, piDefault);
113
- const providerCap = providerKey !== undefined ? cfg.maxConcurrentPerProvider?.[providerKey] : undefined;
114
106
  // The fallback story the operator sees: the declared pair, but the price-resolved pair when
115
107
  // one exists (a map with no `default` entry borrows another tier's free pair) and the
116
108
  // freeness verdict from the same read. `fallbackFree` false keeps the declared pair visible
@@ -135,9 +127,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
135
127
  const accounts = (b) => (b === "claude" ? claudeFallbackAccount : null);
136
128
  const freeFallback = declaredFallback ? fallbackModelPair(cfg, modelsPath, accounts, opencodeCatalog) : null;
137
129
  const fallback = freeFallback ?? declaredFallback;
138
- // One state read serves both the model-fallback episode and the next-run preview below.
130
+ // One state read serves the next-run preview below.
139
131
  const state = loadRunnerState(root, role);
140
- const episode = state.modelFallback;
141
132
  // The role's recent run outcomes, read from the event log (readEvents) so the command
142
133
  // answers with the team stopped and never touches live state. readEvents returns the log's
143
134
  // newest events in file (chronological) order, so filtering to this role's run_end events
@@ -233,7 +224,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
233
224
  let noteSavedAt = null;
234
225
  if (role !== DIRECTOR_ROLE) {
235
226
  const notesPath = roleNotesPath(root, role);
236
- const text = readTextOrNull(notesPath);
227
+ const text = readRegularTextOrNull(notesPath);
237
228
  note = text !== null && text.trim() !== "" ? text : null;
238
229
  if (note !== null)
239
230
  noteSavedAt = statOrNull(notesPath)?.mtimeMs ?? null;
@@ -270,11 +261,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
270
261
  }
271
262
  : {}),
272
263
  backend: effective.agent ?? "pi",
273
- ...(providerKey !== undefined ? { providerKey } : {}),
274
- ...(providerCap !== undefined ? { providerCap } : {}),
275
264
  fallback,
276
265
  fallbackFree: freeFallback !== null,
277
- modelFallback: modelFallbackView(episode, cfg, role),
278
266
  ...(account !== null ? { billing: account.billing, billingBackend } : {}),
279
267
  ...(billingWindow !== undefined ? { billingWindow } : {}),
280
268
  chainStep,
@@ -334,8 +322,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
334
322
  preview: true,
335
323
  })?.prompt ?? null,
336
324
  // The role's schedule — surfaced read-only from the same persisted state rolePayload
337
- // already loaded for the model-fallback episode and the next-run preview, so no new
338
- // read path exists and the collector stays one loadRunnerState per role. `lastRunEndedAt`
325
+ // already loaded for the next-run preview, so no new read path exists and the collector
326
+ // stays one loadRunnerState per role. `lastRunEndedAt`
339
327
  // is omitted (JSON unchanged) until the role has actually run.
340
328
  nextRunAt: state.nextRunAt,
341
329
  backoffSeconds: state.backoffSeconds,