tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -3,7 +3,6 @@ import { backendSessionResumable } from "../agent/agent-backend.js";
3
3
  import { HOLD_BASE_MS } from "../team/team-hold.js";
4
4
  import { backendKindPhrase } from "../text/phrases.js";
5
5
  import { configForRole, resolvedBackendConfig, seamHoldKey, seamPairName } from "../config/config-views.js";
6
- import { ensureProviderLease } from "../concurrency/provider-lease.js";
7
6
  import { buildSummaryRequestPrompt } from "../prompt/prompt-followup.js";
8
7
  import { readClaudeAccount, subscriptionWindowRejected } from "../agent/claude/claude-account.js";
9
8
  import { observeOpencodeRun, opencodeModelKey } from "../agent/opencode/opencode-catalog.js";
@@ -11,7 +10,7 @@ import { dailyCost, teamDailyCost, pairDailyCost } from "../budget/budget.js";
11
10
  import { loadRunnerState } from "./runner-state-load.js";
12
11
  import { baseRoleOf, runnerIds } from "../roles/runner-ids.js";
13
12
  import { buildStageFixPrompt } from "../prompt/gate-prompts.js";
14
- import { logEventBestEffort } from "../events/events.js";
13
+ import { logBlindRead } from "../events/events.js";
15
14
  import { nativeLogPath, piLogPath, roleNotesPath, sessionDir } from "../paths.js";
16
15
  import { piModelsPath } from "../pi/pi-models.js";
17
16
  import { DIRECTOR_ROLE } from "../roles/roles.js";
@@ -116,9 +115,10 @@ export class RunnerPi {
116
115
  notesPath,
117
116
  // A tool call silent for the configured stall threshold names itself in the event feed
118
117
  // while the quiet watchdog still counts down (BUGS.md 2026-09-13 sibling): before this,
119
- // a hung command was invisible until the kill. The dashboards derive their own flag from
120
- // the raw log, so only the harness event needs wiring here.
121
- onToolCallStalled: (message) => this.host.warn(message),
118
+ // a hung command was invisible until the kill. A model request unanswered that long does
119
+ // the same (BUGS.md 2026-10-29). The dashboards derive their own flags from the raw log,
120
+ // so only the harness event needs wiring here.
121
+ onStallWarning: (message) => this.host.warn(message),
122
122
  onLeftoversSwept: (message) => this.host.warn(message),
123
123
  // The translator's unmapped-native-type and cost_unknown warnings reach the event feed
124
124
  // as their own warning events, once per agent run.
@@ -127,14 +127,7 @@ export class RunnerPi {
127
127
  // blind read"): the runner killed the authoring run (action "fail") or a gate run
128
128
  // merely recorded it ("logged") — each logged here with this runner's identity so the
129
129
  // feed names who, on which run, read which file on which blind model.
130
- onBlindRead: (path, action) => logEventBestEffort(this.host.root, {
131
- source: this.host.role,
132
- type: "image_blind",
133
- run: this.host.runNumber(),
134
- path,
135
- model: resolved.model,
136
- action,
137
- }),
130
+ onBlindRead: (path, action) => logBlindRead(this.host.root, this.host.role, this.host.runNumber(), path, resolved.model, action),
138
131
  };
139
132
  }
140
133
  /** Run the orchestrator's merge slot's pi (merge queue 3/5): the shared per-runner wiring
@@ -223,12 +216,6 @@ export class RunnerPi {
223
216
  // Names the seam in per-run provider warnings; omitted when pi picks its own default model.
224
217
  const on = opts.config.model !== undefined ? ` on ${pair}` : "";
225
218
  const fold = (run) => authoring ? this.host.foldUsage(run, provider, pair) : this.host.foldMergeUsage(run, provider, pair);
226
- // The agent run re-keys the calling run's provider lease to the provider THIS agent run uses,
227
- // if it is not already there (plans/provider-concurrency.md §5): a budget handback or a
228
- // model-failure switch that moved this agent run onto another seam moves the run's lease with
229
- // it, so a claude-authored run whose gate agent run lands on omlx is counted there. No-op when
230
- // the run carries no lease (a bare RunnerPi unit test, `tumwater history`).
231
- await ensureProviderLease(opts.config);
232
219
  const pi = await runPi(opts);
233
220
  this.warnFallbackClone(pi);
234
221
  this.observeFreeModelCost(pi, opts.config);
@@ -358,7 +345,7 @@ export class RunnerPi {
358
345
  }
359
346
  /** Run pi for this runner's authoring run in worktree `wt`: the shared per-runner wiring and
360
347
  * the transient-failure retry, with usage folded into the run's counters. It is the ONE
361
- * entry point that carries the role notes (PLANS.md "Role notebook": TUMWATER_NOTES_PATH,
348
+ * entry point that carries the role notes (TUMWATER_NOTES_PATH,
362
349
  * read by the `tumwater-notes` command): every other agent run (runRolePi's merge/conflict path,
363
350
  * runMergePi, runGatePi) leaves it unset, and the director is excluded because its work is
364
351
  * the operator's prompt, not a recurring search.
@@ -409,8 +396,8 @@ export class RunnerPi {
409
396
  * abort.
410
397
  *
411
398
  * The follow-up continues this run's own session, so it must run on the same pair that
412
- * session ran on: the caller passes the run's effective config (the fallback pair while a
413
- * model-fallback episode is active). A caller without one rides the role's own config. The
399
+ * session ran on: the caller passes the run's effective config (a budget cap's fallback
400
+ * pair while a cap episode is active). A caller without one rides the role's own config. The
414
401
  * shared per-runner wiring (runnerPiOpts) takes the follow-up's hard caps overriding the
415
402
  * authoring run's budget: one short reply on a warm session. The agent run takes the runner's
416
403
  * SHARED transient-failure retry (BUGS.md 2026-10-01): a 429 on the follow-up turn waits its
@@ -48,7 +48,7 @@ export async function pinAndResetPhase(ctx, wt, sha) {
48
48
  await resetWorktreeToMain(wt, ctx.mainBranch);
49
49
  return true;
50
50
  }
51
- /** End a run whose leftover recovery found work to salvage (src/runner/leftover.ts) without an
51
+ /** End a run whose leftover recovery found work to salvage (runner-leftover.ts) without an
52
52
  * authoring run — the leftover owns the role's one merge ref until its merge resolves. A
53
53
  * run's dequeued user prompt goes back to its queue, since nothing ran it. A pin put on
54
54
  * the merge queue ends the run `queued` exactly like a fresh changed run (the merge-queue
@@ -56,10 +56,10 @@ export async function pinAndResetPhase(ctx, wt, sha) {
56
56
  * holding the commit. When the pin is left over from a merge that failed non-terminally
57
57
  * (`priorMergeFailure`), re-queuing it is a retry of a PERSISTENT failure the `queued` result
58
58
  * cannot express, so it rides the outcome's recoveryFailure into the error streak and the
59
- * warning that names the stuck gate (BUGS.md 2026-09-21). A role whose merge is still queued
60
- * ends `queued` on that entry — nothing enqueued twice, nothing reset; a queued entry whose
61
- * branch also holds a relief commit ahead of main ends `skipped`, leaving both untouched. A
62
- * leftover that could
59
+ * warning that names the stuck gate (BUGS.md 2026-09-21); an already-queued pin never reaches
60
+ * this phase (runner-run routes that arm to the authoring run). A queued entry whose branch
61
+ * also holds a relief commit ahead of main ends `skipped`, leaving both untouched. A leftover
62
+ * that could
63
63
  * not be pinned stays on the branch and the run fails like a fresh run's failed pin. Every
64
64
  * arm is `recoveredLeftover`: no model ran, so the run is no evidence about the backend. */
65
65
  export async function finishRecoveryRunPhase(ctx, recovered, userPrompt, wt, priorMergeFailure) {
@@ -69,14 +69,6 @@ export async function finishRecoveryRunPhase(ctx, recovered, userPrompt, wt, pri
69
69
  ctx.state.lastError = `failed to pin leftover ${shortSha(recovered.sha)} by its merge ref; left for the next run's recovery`;
70
70
  return { result: "error", summary: ctx.state.lastError, recoveredLeftover: true };
71
71
  }
72
- if (recovered.kind === "held") {
73
- // A permanent reviewer configuration error holds the pin: do not re-queue (and pay
74
- // another gate check) until the model config changes. Ending `error` schedules the error
75
- // ladder, so the hold is re-checked at backoff pace rather than at suite speed
76
- // (BUGS.md 2026-10-06).
77
- ctx.state.lastError = recovered.message;
78
- return { result: "error", summary: recovered.message, recoveredLeftover: true };
79
- }
80
72
  if (recovered.kind === "main_red_wait") {
81
73
  // The pin waits for the orchestrator poll to re-queue it once main moves; `main_red` takes
82
74
  // the idle ladder, and the worktree stays as it is.
@@ -38,8 +38,7 @@ export const REVISION_BACKSTOP = 6;
38
38
  * objections verbatim (they are already in `RunnerState.lastReview.reasons`), so the plan role
39
39
  * sees exactly what would not converge. PLANS.md plans only, by design: the convergence hold
40
40
  * parks a revised plan whose author hit the round cap, so only the plan role needs to clear it;
41
- * a role's own too-large Needs-review note on a BUGS.md entry is the bug path (the
42
- * stalledEntries/operatorHeldEntries comments in src/backlog/backlog-eligibility.ts), which the
41
+ * a role's own too-large Needs-review note on a BUGS.md entry is the bug path, which the
43
42
  * plan role resolves by splitting it. A bug that cannot converge stays with the existing re-pick
44
43
  * behaviour (reviewer-feedback delivery, part 4/4, addresses that churn).
45
44
  * Written through the primary worktree lock, as every other backlog write is, and then committed
@@ -6,23 +6,25 @@ import { abortSync, ensureWorktree, resetWorktreeToMain } from "../git/worktree.
6
6
  import { leaseSlot } from "../git/worktree-pool.js";
7
7
  import { slotCount } from "../config/config.js";
8
8
  import { useWorktree } from "../git/worktree-use.js";
9
+ import { readPiListedModels } from "../pi/pi-listed-models.js";
9
10
  import { branchName, worktreePath, mergeRefName, rejectedRefName } from "../paths.js";
10
11
  import { logEventBestEffort } from "../events/events.js";
11
12
  import { buildConflictHandbackNote, buildSalvageRestoreNote } from "../prompt/gate-prompts.js";
12
13
  import { buildConflictDiscardNote, buildMainKnownRedNote, buildRevisionNote, buildRejectedReviewNote, } from "../prompt/gate-notes.js";
13
- import { modelSelectorField, resolvedBackendConfig, reviewRunConfig, reviewedAfterMerge, scheduleConfigForRole, seamHoldKey, } from "../config/config-views.js";
14
- import { fallbackRoleConfig } from "../config/config-fallbacks.js";
14
+ import { modelSelectorField, resolvedBackendConfig, scheduleConfigForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
15
+ import { imageReroute, seamImageInput } from "../agent/image-input.js";
16
+ import { piModelsPath, readPiProviders } from "../pi/pi-models.js";
17
+ import { peekPrompt, peekRolePrompt } from "../inbox/inbox.js";
15
18
  import { planRunStart, planWithSalvagedWork, takesSalvagedWork } from "../run/run-resume.js";
16
19
  import { restoreSalvagedWork } from "../git/slot-salvage.js";
17
20
  import { DEFAULT_BACKEND } from "../agent/agent-backend.js";
18
21
  import { foldAgentCommitsSince } from "../run/run-stage.js";
19
22
  import { planBacklogFull } from "../scheduling/claims.js";
20
- import { modelFallbackActive, modelFallbackProbe } from "./model-fallback.js";
21
23
  import { finalizeRun } from "../run/run-finalize.js";
22
24
  import { errorMessage } from "../text/text.js";
23
25
  import { appendPromptNote, prependPromptNote } from "../text/format.js";
24
- import { hasUnmergedPin, recoverLeftover } from "./leftover.js";
25
- import { applyRevision, applyWithConflicts } from "./revision.js";
26
+ import { hasUnmergedPin, recoverLeftover } from "./runner-leftover.js";
27
+ import { applyRevision, applyWithConflicts } from "./runner-revision.js";
26
28
  import { conflictedFileHunks, hasConflictMarkers, mainCommitsTouching } from "../merge/merge-git.js";
27
29
  import { changedFiles } from "../git/git-diff.js";
28
30
  import { handlePiResultPhase } from "./runner-outcome.js";
@@ -57,10 +59,9 @@ async function markerBearingFiles(wt) {
57
59
  const files = await changedFiles(wt);
58
60
  return files.filter((f) => hasConflictMarkers(wt, [f]));
59
61
  }
60
- /** Run one full run of a role runner: resolve the config (including the model-fallback probe),
61
- * log the start, run the pipeline, then let finalizeRun schedule the next run. Never throws: a
62
- * failed run is an "error" result, saved and logged like any other so the runner stays
63
- * resumable and observable. */
62
+ /** Run one full run of a role runner: resolve the config, log the start, run the pipeline, then
63
+ * let finalizeRun schedule the next run. Never throws: a failed run is an "error" result,
64
+ * saved and logged like any other so the runner stays resumable and observable. */
64
65
  export async function runPhase(ctx) {
65
66
  const s = ctx.state;
66
67
  // This role's view of the config (per-role provider/model/thinking + minRunIntervalSeconds
@@ -73,20 +74,67 @@ export async function runPhase(ctx) {
73
74
  // the one home of the rule, so the scheduler that admits plan and the nextRunAt this run
74
75
  // writes cannot disagree.
75
76
  const planBacklogOpen = !planBacklogFull(ctx.root, ctx.config);
77
+ // This run's effective config: the role's own resolved config (a run always resolves its
78
+ // role's own config — the model-fallback episode is gone, Subtract part 5o/6) unless an image
79
+ // reroute below moves it. The budget gate's fallback pair, when a cap episode is active, is
80
+ // installed into the live config itself, so the config here already names the pair the run
81
+ // uses.
76
82
  const primaryCfg = scheduleConfigForRole(ctx.config, ctx.role, planBacklogOpen);
77
- // Model-fallback episode (src/runner/model-fallback.ts): while one is active the run runs
78
- // on the role's tier fallback pair, and once the cooldown elapses it runs on the primary
79
- // as the probe. Resolved at run start so the authoring run below and the end-of-run fold
80
- // agree on which pair ran. Null fallback (the tier resolves to pause) keeps the feature off.
81
- const now = Date.now();
82
- const fallback = fallbackRoleConfig(ctx.config, ctx.role);
83
- const probing = fallback !== null && modelFallbackProbe(s.modelFallback, now);
84
- const onFallback = fallback !== null && modelFallbackActive(s.modelFallback, now);
85
- // The fallback pair keeps primaryCfg's gap (they resolve the same one outside the plan
86
- // schedule's zero-gap window), so a plan on its model fallback still schedules the same gap.
87
- let cfg = onFallback && fallback !== null
88
- ? { ...fallback, minRunIntervalSeconds: primaryCfg.minRunIntervalSeconds }
89
- : primaryCfg;
83
+ let cfg = primaryCfg;
84
+ // Image-input reroute (plans/image-input.md part 4/5). A pending one-shot marker means
85
+ // THIS run is the blind-read reroute: start directly on the image-capable target the
86
+ // prior run recorded, over the kept worktree, in a fresh session. The marker is consumed
87
+ // (cleared) here so it does not route a later run; imageRerouteActive stays true for the
88
+ // whole run so the verdict's one-hop guard fails a second blind read instead of
89
+ // rerouting again. A preflight (below) sets the same active flag without a marker.
90
+ s.imageRerouteActive = false;
91
+ let preflightBlocked = false;
92
+ if (s.imageReroute) {
93
+ const r = s.imageReroute;
94
+ cfg = withModelOverrides(ctx.config, r.tier, {
95
+ backend: r.backend,
96
+ provider: r.provider,
97
+ model: r.model,
98
+ thinking: r.thinking,
99
+ });
100
+ // The marker is NOT cleared here: planRunStart reads its `from`/`path` when it builds
101
+ // the handoff note (and clears it then), so a run skipped before the plan (an empty
102
+ // inbox) leaves the reroute pending for the next eligible run.
103
+ s.imageRerouteActive = true;
104
+ }
105
+ else {
106
+ // Preflight (plans/image-input.md "Attachments"): a queued prompt carrying an
107
+ // `[image attached: …]` line needs a model that can read images BEFORE any run starts,
108
+ // so a text-only run hops straight to the target instead of spending a failed run
109
+ // learning it; with no target the run errors without spawning pi.
110
+ const nextPrompt = ctx.role === DIRECTOR_ROLE ? peekPrompt(ctx.root) : peekRolePrompt(ctx.root, ctx.role);
111
+ const carriesImages = nextPrompt !== null && /^\[image attached: .+\]$/m.test(nextPrompt);
112
+ if (carriesImages) {
113
+ const providers = readPiProviders(piModelsPath());
114
+ const listed = readPiListedModels(ctx.root);
115
+ if (seamImageInput(cfg, providers, listed) !== true) {
116
+ const target = imageReroute(ctx.config, ctx.role, ctx.rerouteRegime(), undefined, null, null, listed);
117
+ if (target !== null) {
118
+ cfg = target.config;
119
+ s.imageRerouteActive = true;
120
+ logEventBestEffort(ctx.root, {
121
+ source: ctx.role,
122
+ type: "image_reroute",
123
+ run: s.runs + 1,
124
+ from: primaryCfg.model,
125
+ to: target.config.model,
126
+ tier: target.tier,
127
+ preflight: true,
128
+ });
129
+ }
130
+ else {
131
+ preflightBlocked = true;
132
+ s.lastError =
133
+ `an inbox message for ${ctx.role} attaches an image, but no configured tier can read it`;
134
+ }
135
+ }
136
+ }
137
+ }
90
138
  // Capture what this run runs on (the orchestrator's budget handback matches it against
91
139
  // the fallback pair) and clear any stale handback flag: an abort request that lands while
92
140
  // the runner is idle must not name a later run's resume.
@@ -122,11 +170,17 @@ export async function runPhase(ctx) {
122
170
  source: ctx.role,
123
171
  type: "run_start",
124
172
  run: runNumber,
173
+ // A run routed onto the image-capable target — a blind-read handoff or an attachment
174
+ // preflight — is stamped so the feed shows the reroute and the model it landed on.
175
+ ...(s.imageRerouteActive ? { reroute: "image" } : {}),
125
176
  ...modelSelectorField(cfg),
126
177
  });
127
178
  let outcome;
128
179
  try {
129
- outcome = await ctx.runOnce(cfg, { fallback, probe: probing, primary: primaryCfg });
180
+ // The preflight can end the run before any pi spawns: a text-only runner with an image
181
+ // attachment and no image-capable target has nothing to run, so it errors without
182
+ // spending a model call. Everything else runs the pipeline as usual.
183
+ outcome = preflightBlocked ? { result: "error" } : await ctx.runOnce(cfg);
130
184
  }
131
185
  catch (err) {
132
186
  outcome = { result: "error" };
@@ -170,7 +224,7 @@ export async function runPhase(ctx) {
170
224
  /** The run pipeline's start half: resume-or-fresh policy, then run in the director's own
171
225
  * worktree or on a leased pool slot. Returns the setup's outcome; a null plan means nothing to
172
226
  * run. */
173
- export async function runOncePhase(ctx, cfg, fallbackCtx) {
227
+ export async function runOncePhase(ctx, cfg) {
174
228
  const s = ctx.state;
175
229
  // How this run starts — resume the interrupted session or run fresh, and which prompt —
176
230
  // is the resume policy, not runner mechanics: it lives in src/run/run-resume.ts, which also
@@ -192,14 +246,25 @@ export async function runOncePhase(ctx, cfg, fallbackCtx) {
192
246
  if (plan === null)
193
247
  return { result: "skipped" };
194
248
  if (ctx.role === DIRECTOR_ROLE) {
249
+ // The image-reroute one-shot marker routes exactly ONE run onto the image-capable
250
+ // target (plans/image-input.md part 4/5, INV-J2): for a role runner, planRunStart consumes
251
+ // it after reading its handoff-note facts. The director never resumes, so planRunStart's
252
+ // image-reroute branch never runs for it and the marker would otherwise linger and
253
+ // re-route every later director run off its own tier. It is cleared here — after this
254
+ // run is confirmed to run (an empty-inbox skip above returns before this, leaving the
255
+ // reroute pending for the next eligible run) — so the rerouted director prompt is
256
+ // delivered exactly once and the director is back on its own tier next.
257
+ // A promptless recovery run delivers no prompt, so it leaves a pending reroute for the next.
258
+ if (plan.prompt !== null)
259
+ s.imageReroute = undefined;
195
260
  // The director keeps its dedicated worktree (it runs outside the permit, consumes its
196
261
  // config-request file at that exact path, and never resumes). Hold it from before
197
262
  // ensureWorktree — creating or resetting it is part of the use — to the run's end
198
263
  // (plans/disk-floor.md, part 2/4), so a concurrent pressure reclaim never cleans a tree
199
264
  // this run is creating or working in.
200
- return useWorktree(ctx.root, worktreePath(ctx.root, ctx.role), () => runWithWorktreePhase(ctx, plan, cfg, fallbackCtx));
265
+ return useWorktree(ctx.root, worktreePath(ctx.root, ctx.role), () => runWithWorktreePhase(ctx, plan, cfg));
201
266
  }
202
- return runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx);
267
+ return runOnceInLeasePhase(ctx, plan, cfg);
203
268
  }
204
269
  /** Whether a finished role run's leased slot must stay pinned for the next run's resume —
205
270
  * the resume flag a verdict armed inside the run (`resumePending`), an abort or quiet kill,
@@ -220,7 +285,7 @@ export function slotPinnedForResume(state, outcome) {
220
285
  * leases next. A run an exception ends releases its slot unsettled, so edits it leaves there
221
286
  * are saved for the role's next run, and a run on a fresh checkout first takes back any such
222
287
  * work an earlier run of the role left (restoreSalvagedPhase). */
223
- async function runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx) {
288
+ async function runOnceInLeasePhase(ctx, plan, cfg) {
224
289
  const lease = await leaseSlot(ctx.root, {
225
290
  role: ctx.role,
226
291
  purpose: "run",
@@ -238,7 +303,7 @@ async function runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx) {
238
303
  await checkoutRoleBranchPhase(ctx, lease.dir);
239
304
  run = await restoreSalvagedPhase(ctx, plan, lease.dir);
240
305
  }
241
- outcome = await runWithWorktreePhase(ctx, run, cfg, fallbackCtx, lease.dir);
306
+ outcome = await runWithWorktreePhase(ctx, run, cfg, lease.dir);
242
307
  return outcome;
243
308
  }
244
309
  finally {
@@ -288,7 +353,7 @@ async function checkoutRoleBranchPhase(ctx, dir) {
288
353
  }
289
354
  await git(dir, "clean", "-fd");
290
355
  }
291
- async function runWithWorktreePhase(ctx, plan, cfg, fallbackCtx,
356
+ async function runWithWorktreePhase(ctx, plan, cfg,
292
357
  /** The leased slot the run runs in; omitted by the director, which ensures its own. */
293
358
  leasedDir) {
294
359
  const s = ctx.state;
@@ -309,19 +374,13 @@ leasedDir) {
309
374
  });
310
375
  }
311
376
  else {
312
- // Salvage a commit a previous run left unmerged (src/runner/leftover.ts): recovery puts it
377
+ // Salvage a commit a previous run left unmerged (runner-leftover.ts): recovery puts it
313
378
  // back on the merge queue, so it merges through the orchestrator's merge pipeline — the
314
379
  // same gate as a fresh run's change, and main keeps exactly one writer (an in-run
315
380
  // recovery merge raced the slot's batches into `merge_blocked`). A salvaged leftover
316
381
  // ENDS the run: the role holds one merge ref, and the leftover owns it until its
317
382
  // merge resolves. With nothing to salvage the branch holds nothing either, so the reset
318
383
  // below leaves pristine main.
319
- // A permanent reviewer config error — held from re-queueing — clears once the resolved
320
- // reviewer selector changes, so fixing the model config recovers the pin automatically
321
- // (BUGS.md 2026-10-06).
322
- const reviewSelector = modelSelectorField(reviewRunConfig(ctx.config)).model;
323
- if (s.mergeReviewError && s.mergeReviewError.selector !== reviewSelector)
324
- s.mergeReviewError = undefined;
325
384
  const recovered = await recoverLeftover({
326
385
  root: ctx.root,
327
386
  role: ctx.role,
@@ -330,7 +389,6 @@ leasedDir) {
330
389
  wt,
331
390
  mergeConflicts: s.mergeConflicts,
332
391
  conflictHandback: s.conflictHandback,
333
- mergeReviewError: s.mergeReviewError,
334
392
  mainRedRetry: s.mainRedRetry,
335
393
  });
336
394
  if (recovered?.kind === "handback" && plan.prompt === null) {
@@ -361,14 +419,12 @@ leasedDir) {
361
419
  s.conflictDiscard = { sha: recovered.sha, summary: recovered.summary, attempts: recovered.attempts, at: Date.now() };
362
420
  prompt = appendPromptNote(prompt, buildConflictDiscardNote(recovered.summary, recovered.attempts));
363
421
  }
364
- else if (recovered &&
365
- !(recovered.kind === "already_queued" &&
366
- reviewedAfterMerge(ctx.config, ctx.role))) {
367
- // Every recovery kind ends the run here — EXCEPT `already_queued` for a post-merge-review
368
- // role, whose arm the scheduler admits (plans/interlock-relief.md part 3/4) and whose
369
- // branch is at main: that one falls through to author below instead of ending. Every
370
- // other kind, `relief_wait` (a held commit ahead of main beside a queued entry) included,
371
- // ends the run.
422
+ else if (recovered && recovered.kind !== "already_queued") {
423
+ // Every recovery kind ends the run here — EXCEPT `already_queued`, whose arm the
424
+ // scheduler admits (the interlock relief applies team-wide) and whose branch is at main:
425
+ // that one falls through to author below instead of ending. Every other kind,
426
+ // `relief_wait` (a held commit ahead of main beside a queued entry) included, ends the
427
+ // run.
372
428
  return ctx.finishRecoveryRun(recovered, userPrompt, wt, priorMergeFailure);
373
429
  }
374
430
  await resetWorktreeToMain(wt, ctx.mainBranch);
@@ -526,17 +582,14 @@ leasedDir) {
526
582
  await foldAgentCommitsSince(ctx, wt, runBase);
527
583
  // A write that landed in the primary checkout never reaches the worktree's commit, so the run
528
584
  // can read as no_change with its work orphaned (BUGS.md, 2026-10-20). The before/after diff
529
- // cannot attribute paths to this run when roles overlap, so only warn: the verdict, resume
530
- // handling and model-fallback fold below stay as they were.
585
+ // cannot attribute paths to this run when roles overlap, so only warn: the verdict and
586
+ // resume handling below stay as they were.
531
587
  const primaryAfter = primaryBefore === null ? null : await changedFiles(ctx.root).catch(() => null);
532
588
  const strayed = primaryAfter === null ? [] : primaryAfter.filter((f) => !primaryBefore.includes(f));
533
589
  if (strayed.length > 0 && !pi.aborted) {
534
590
  const shown = strayed.slice(0, 10).join(", ") + (strayed.length > 10 ? `, … (${strayed.length} paths)` : "");
535
591
  ctx.warn(`wrote outside the worktree (possibly another writer's): ${shown}`);
536
592
  }
537
- // Fold the authoring run's verdict into the role's model-fallback episode NOW, before the
538
- // post-run handlers spend any more pi runs: only this run is evidence about the primary.
539
- ctx.foldModelFallback(fallbackCtx, pi);
540
593
  // The `qa` observer's reply ends with a result-carrying `FLOW:` line (plans/observer-roles.md
541
594
  // 2/2). Extracted once here, recorded only at the success returns below — an interrupted or
542
595
  // failed run must not advance the rotation. The harness records it, never pi.
@@ -1,4 +1,4 @@
1
- import { APPROVED_PATCH_IDS_MAX, freshRunnerState, SKIP_REASONS } from "./runner-state.js";
1
+ import { freshRunnerState, SKIP_REASONS } from "./runner-state.js";
2
2
  import path from "node:path";
3
3
  import { dirEntryNames } from "../files/files.js";
4
4
  import { readJsonFile } from "../files/json-files.js";
@@ -58,13 +58,17 @@ const LEGACY_STATE_KEYS = {
58
58
  lastTickStartedAt: "lastRunStartedAt", // retired-name-ok: legacy name
59
59
  lastTickEndedAt: "lastRunEndedAt", // retired-name-ok: legacy name
60
60
  // retired-name-ok:start old state keys
61
- landingReviewError: "mergeReviewError", // vocabulary-ok: legacy state key read from old files
62
61
  landingCheckFailures: "mergeCheckFailures", // vocabulary-ok: legacy state key read from old files
63
62
  lastLandedAt: "lastMergedAt", // vocabulary-ok: legacy state key read from old files
64
63
  // retired-name-ok:end
65
64
  };
66
65
  /** Apply LEGACY_STATE_KEYS to a parsed state object in place. */
67
66
  function migrateLegacyStateKeys(saved) {
67
+ // The pre-merge reviewer-config hold is gone (part 3j/3): a state file may still name it under
68
+ // either its old (landingReviewError) or its recent (mergeReviewError) key, and with no field
69
+ // to load it into — no reader consumes it — drop both here so a later save writes neither.
70
+ delete saved.landingReviewError; // retired-name-ok: legacy state key read from old files
71
+ delete saved.mergeReviewError;
68
72
  for (const [oldKey, newKey] of Object.entries(LEGACY_STATE_KEYS)) {
69
73
  if (!(oldKey in saved))
70
74
  continue;
@@ -121,26 +125,9 @@ export function loadRunnerState(root, role) {
121
125
  healOptionalNumber(s, "parkedSince");
122
126
  if (s.skipReason !== undefined && !SKIP_REASONS.includes(s.skipReason))
123
127
  delete s.skipReason;
124
- // The one-id field an older state file carries folds into the list.
125
- const legacy = saved?.lastApprovedPatchId;
126
- delete s.lastApprovedPatchId;
127
- if (s.approvedPatchIds !== undefined || typeof legacy === "string") {
128
- const ids = [...(Array.isArray(s.approvedPatchIds) ? s.approvedPatchIds : []), ...(typeof legacy === "string" ? [legacy] : [])];
129
- s.approvedPatchIds = ids.filter((id) => typeof id === "string").slice(-APPROVED_PATCH_IDS_MAX);
130
- }
131
- if (s.reviewResume !== undefined) {
132
- const r = healRecord(s.reviewResume, (v) => {
133
- v.attempts = healNumber(v.attempts, 0);
134
- v.at = healNumber(v.at, 0);
135
- });
136
- // A marker without the two strings it keys on names no session to resume, so a malformed
137
- // one is dropped here rather than left to throw in sessionResumable.
138
- s.reviewResume =
139
- r && typeof r.patchId === "string" && typeof r.sessionDir === "string" ? r : undefined;
140
- }
141
128
  // The nested scheduling records carry the same numeric class — epoch-ms stamps, recovery
142
- // counts, and the fallback episode's clock — and are read by comparison or arithmetic
143
- // (`ctx.now - claim.at > CLAIM_IDLE_MAX_MS`, `prior.count + 1`, `cooldownMs * 2`), so a
129
+ // counts, and retry/review counts — and are read by comparison or arithmetic
130
+ // (`ctx.now - claim.at > CLAIM_IDLE_MAX_MS`, `prior.count + 1`, `attempts + 1`), so a
144
131
  // foreign value there is healed the same way the top-level fields are.
145
132
  if (s.lastReview !== undefined)
146
133
  s.lastReview = healRecord(s.lastReview, (v) => {
@@ -201,13 +188,16 @@ export function loadRunnerState(root, role) {
201
188
  });
202
189
  if (s.mergeFlakeExcused !== undefined && typeof s.mergeFlakeExcused !== "string")
203
190
  delete s.mergeFlakeExcused;
204
- if (s.modelFallback !== undefined)
205
- s.modelFallback = healRecord(s.modelFallback, (v) => {
206
- v.failures = healNumber(v.failures, 0);
207
- v.since = healNumber(v.since, 0);
208
- v.probeAt = healNumber(v.probeAt, 0);
209
- v.cooldownMs = healNumber(v.cooldownMs, 0);
210
- });
191
+ // The image-reroute marker is a one-shot nested record consumed by the next run; a torn
192
+ // scalar is dropped (its consumers read/mutate fields), and the tier is repaired to a
193
+ // known tier name so a corrupt tier cannot route a rerouted run to a phantom seam.
194
+ if (s.imageReroute !== undefined) {
195
+ const healed = healRecord(s.imageReroute, () => { });
196
+ s.imageReroute =
197
+ healed && (healed.tier === "small" || healed.tier === "default" || healed.tier === "strong")
198
+ ? healed
199
+ : undefined;
200
+ }
211
201
  return s;
212
202
  }
213
203
  /** Per-role cache of the persisted live claim, keyed by the state file's stat — one stat per
@@ -1,20 +1,6 @@
1
1
  import { writeJsonAtomic } from "../files/json-files.js";
2
2
  import { statePath } from "../paths.js";
3
3
  import { tryOrWarn } from "../events/events.js";
4
- /** The runner's persisted state file — one JSON object per role under .tumwater/ — and the
5
- * observation-window counter reset: fresh-state defaults, the atomic save, and zeroCounters
6
- * (the tolerant load and the live-claim scan are runner-state-load.ts). The scheduling POLICY
7
- * that mutates this state from run outcomes and operator wakes — the outcome application and
8
- * resume limits in run-apply.ts, the clock arithmetic (backoff ladders, wakes, yield ring) in
9
- * backoff.ts; the generic file helpers are src/files/files.ts and the JSON convention itself is
10
- * src/files/json-files.ts. */
11
- /** How many times a progressing-timeout review may be resumed before the next attempt
12
- * reviews the change from scratch (RunnerState.reviewResume). The budget bounds a reviewer that
13
- * keeps timing out mid-progress, so a wedged review converges to a fresh run instead of
14
- * resuming the same session forever. */
15
- export const REVIEW_RESUME_LIMIT = 2;
16
- /** Approved patch-ids a runner remembers (RunnerState.approvedPatchIds). */
17
- export const APPROVED_PATCH_IDS_MAX = 8;
18
4
  export const SKIP_REASONS = [
19
5
  "no-free-entry",
20
6
  "claim-hold",