tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { DIRECTOR_ROLE } from "../roles/roles.js";
2
3
  import { branchExists, deleteRef, commitMessage, changeBaseRev, headOf } from "../git/git.js";
3
4
  import { git, gitTry } from "../git/git-run.js";
@@ -5,32 +6,32 @@ import { abortSync, ensureWorktree, resetWorktreeToMain } from "../git/worktree.
5
6
  import { leaseSlot } from "../git/worktree-pool.js";
6
7
  import { slotCount } from "../config/config.js";
7
8
  import { useWorktree } from "../git/worktree-use.js";
9
+ import { readPiListedModels } from "../pi/pi-listed-models.js";
8
10
  import { branchName, worktreePath, mergeRefName, rejectedRefName } from "../paths.js";
9
11
  import { logEventBestEffort } from "../events/events.js";
10
- import { buildConflictDiscardNote, buildConflictHandbackNote, buildRevisionNote, buildRejectedReviewNote, } from "../prompt/gate-prompts.js";
11
- import { modelSelectorField, resolvedAgentConfig, reviewRunConfig, scheduleConfigForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
12
+ import { buildConflictHandbackNote, buildSalvageRestoreNote } from "../prompt/gate-prompts.js";
13
+ import { buildConflictDiscardNote, buildMainKnownRedNote, buildRevisionNote, buildRejectedReviewNote, } from "../prompt/gate-notes.js";
14
+ import { modelSelectorField, resolvedBackendConfig, reviewRunConfig, scheduleConfigForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
12
15
  import { fallbackRoleConfig } from "../config/config-fallbacks.js";
13
16
  import { imageReroute, seamImageInput } from "../agent/image-input.js";
14
17
  import { piModelsPath, readPiProviders } from "../pi/pi-models.js";
15
18
  import { peekPrompt, peekRolePrompt } from "../inbox/inbox.js";
16
- import { planTickStart } from "../tick/tick-resume.js";
17
- import { DEFAULT_AGENT } from "../agent/agent-backend.js";
18
- import { stageTickMerge, foldAgentCommitsSince } from "../tick/tick-stage.js";
19
- import { stageCheckFindings } from "../tick/stage-check.js";
20
- import { bootstrapStatus } from "../gates/bootstrap-gates.js";
21
- import { briefStatusIncomplete } from "../brief/brief.js";
19
+ import { planRunStart, planWithSalvagedWork, takesSalvagedWork } from "../run/run-resume.js";
20
+ import { restoreSalvagedWork } from "../git/slot-salvage.js";
21
+ import { DEFAULT_BACKEND } from "../agent/agent-backend.js";
22
+ import { foldAgentCommitsSince } from "../run/run-stage.js";
23
+ import { planBacklogFull } from "../scheduling/claims.js";
22
24
  import { modelFallbackActive, modelFallbackProbe } from "./model-fallback.js";
23
- import { finalizeTick } from "../tick/tick-finalize.js";
25
+ import { finalizeRun } from "../run/run-finalize.js";
24
26
  import { errorMessage } from "../text/text.js";
25
- import { appendPromptNote } from "../text/format.js";
26
- import { recoverLeftover } from "./leftover.js";
27
+ import { appendPromptNote, prependPromptNote } from "../text/format.js";
28
+ import { hasUnmergedPin, recoverLeftover } from "./leftover.js";
27
29
  import { applyRevision, applyWithConflicts } from "./revision.js";
28
30
  import { conflictedFileHunks, hasConflictMarkers, mainCommitsTouching } from "../merge/merge-git.js";
29
31
  import { changedFiles } from "../git/git-diff.js";
30
- import { mergeToMain } from "../merge/merge-to-main.js";
32
+ import { handlePiResultPhase } from "./runner-outcome.js";
31
33
  import { bugfixMainRedNote, mainRedGate } from "../baseline/main-red.js";
32
- import { resolveTickVerdict } from "../tick/tick-verdict.js";
33
- import { extractFlow } from "../verdict/reply-contract.js";
34
+ import { extractFlow, extractFiled } from "../verdict/reply-contract.js";
34
35
  /** The intent the author needs to resolve a handed-back change: the change's own commit message
35
36
  * and the main commits that touched its conflicted files since its merge-base. Mirrors what
36
37
  * resolveConflict gathers for the merge resolver (src/merge/merge-to-main.ts) and feeds the
@@ -59,68 +60,69 @@ async function markerBearingFiles(wt) {
59
60
  const files = await changedFiles(wt);
60
61
  return files.filter((f) => hasConflictMarkers(wt, [f]));
61
62
  }
62
- /** Run one full tick of a role loop: resolve the config (including the model-fallback probe),
63
- * log the start, run the pipeline, then let finalizeTick schedule the next run. Never throws: a
64
- * failed tick is an "error" result, saved and logged like any other so the loop stays resumable
65
- * and observable. */
66
- export async function tickPhase(ctx) {
63
+ /** Run one full run of a role runner: resolve the config (including the model-fallback probe),
64
+ * log the start, run the pipeline, then let finalizeRun schedule the next run. Never throws: a
65
+ * failed run is an "error" result, saved and logged like any other so the runner stays
66
+ * resumable and observable. */
67
+ export async function runPhase(ctx) {
67
68
  const s = ctx.state;
68
69
  // This role's view of the config (per-role provider/model/thinking + minRunIntervalSeconds
69
70
  // overrides): resolved once so every interval-based scheduling branch honors a slow
70
- // clock (e.g. the steward's ~6 h) and a live-reloaded config applies from this tick on.
71
- // New-project bootstrap (plans/work-ratio.md, part 2/2): while active, plan schedules on the
72
- // global min-tick gap rather than its 3600 s default. And while the brief's status section is
73
- // still the placeholder (briefStatusIncomplete), plan schedules with a zero gap so a fresh
74
- // project plans back-to-back until a run changes nothing. The shared verdict below is the
75
- // same one the gate and isEligible read, and scheduleConfigForRole is the one home of the
76
- // rule, so the scheduler that admits plan and the nextRunAt this tick writes cannot disagree.
77
- const bootstrapActive = bootstrapStatus(ctx.root, ctx.config)?.active === true;
78
- const descriptionIncomplete = briefStatusIncomplete(ctx.root);
79
- const primaryCfg = scheduleConfigForRole(ctx.config, ctx.role, bootstrapActive, descriptionIncomplete);
80
- // Model-fallback episode (src/loop/model-fallback.ts): while one is active the tick runs
71
+ // clock (e.g. the steward's ~6 h) and a live-reloaded config applies from this run on.
72
+ // While PLANS.md still has planning room (planBacklogFull false), plan schedules with a zero
73
+ // gap so a productive plan run reruns on the next poll; an unproductive run drops to the
74
+ // idle backoff, and once the target is met the plans-full deferral holds the run. The shared
75
+ // verdict below is the same one the gate and isEligible read, and scheduleConfigForRole is
76
+ // the one home of the rule, so the scheduler that admits plan and the nextRunAt this run
77
+ // writes cannot disagree.
78
+ const planBacklogOpen = !planBacklogFull(ctx.root, ctx.config);
79
+ const primaryCfg = scheduleConfigForRole(ctx.config, ctx.role, planBacklogOpen);
80
+ // Model-fallback episode (src/runner/model-fallback.ts): while one is active the run runs
81
81
  // on the role's tier fallback pair, and once the cooldown elapses it runs on the primary
82
- // as the probe. Resolved at tick start so the authoring run below and the end-of-tick fold
82
+ // as the probe. Resolved at run start so the authoring run below and the end-of-run fold
83
83
  // agree on which pair ran. Null fallback (the tier resolves to pause) keeps the feature off.
84
84
  const now = Date.now();
85
85
  const fallback = fallbackRoleConfig(ctx.config, ctx.role);
86
86
  const probing = fallback !== null && modelFallbackProbe(s.modelFallback, now);
87
87
  const onFallback = fallback !== null && modelFallbackActive(s.modelFallback, now);
88
- // The fallback pair keeps primaryCfg's gap (they resolve the same one outside bootstrap),
89
- // so a plan on its model fallback during bootstrap still schedules on the global clock.
88
+ // The fallback pair keeps primaryCfg's gap (they resolve the same one outside the plan
89
+ // schedule's zero-gap window), so a plan on its model fallback still schedules the same gap.
90
90
  let cfg = onFallback && fallback !== null
91
91
  ? { ...fallback, minRunIntervalSeconds: primaryCfg.minRunIntervalSeconds }
92
92
  : primaryCfg;
93
93
  // Image-input reroute (plans/image-input.md part 4/5). A pending one-shot marker means
94
- // THIS tick is the blind-read reroute: start directly on the image-capable target the
95
- // prior tick recorded, over the kept worktree, in a fresh session. The marker is consumed
96
- // (cleared) here so it does not route a later tick; imageRerouteActive stays true for the
97
- // whole tick so the verdict's one-hop guard fails a second blind read instead of
94
+ // THIS run is the blind-read reroute: start directly on the image-capable target the
95
+ // prior run recorded, over the kept worktree, in a fresh session. The marker is consumed
96
+ // (cleared) here so it does not route a later run; imageRerouteActive stays true for the
97
+ // whole run so the verdict's one-hop guard fails a second blind read instead of
98
98
  // rerouting again. A preflight (below) sets the same active flag without a marker.
99
99
  s.imageRerouteActive = false;
100
100
  let preflightBlocked = false;
101
101
  if (s.imageReroute) {
102
102
  const r = s.imageReroute;
103
103
  cfg = withModelOverrides(ctx.config, r.tier, {
104
+ backend: r.backend,
104
105
  provider: r.provider,
105
106
  model: r.model,
106
107
  thinking: r.thinking,
107
108
  });
108
- // The marker is NOT cleared here: planTickStart reads its `from`/`path` when it builds
109
- // the handoff note (and clears it then), so a tick skipped before the plan (an empty
110
- // inbox) leaves the reroute pending for the next eligible tick.
109
+ // The marker is NOT cleared here: planRunStart reads its `from`/`path` when it builds
110
+ // the handoff note (and clears it then), so a run skipped before the plan (an empty
111
+ // inbox) leaves the reroute pending for the next eligible run.
111
112
  s.imageRerouteActive = true;
112
113
  }
113
114
  else {
114
115
  // Preflight (plans/image-input.md "Attachments"): a queued prompt carrying an
115
116
  // `[image attached: …]` line needs a model that can read images BEFORE any run starts,
116
- // so a text-only tick hops straight to the target instead of spending a failed tick
117
- // learning it; with no target the tick errors without spawning pi.
117
+ // so a text-only run hops straight to the target instead of spending a failed run
118
+ // learning it; with no target the run errors without spawning pi.
118
119
  const nextPrompt = ctx.role === DIRECTOR_ROLE ? peekPrompt(ctx.root) : peekRolePrompt(ctx.root, ctx.role);
119
120
  const carriesImages = nextPrompt !== null && /^\[image attached: .+\]$/m.test(nextPrompt);
120
121
  if (carriesImages) {
121
122
  const providers = readPiProviders(piModelsPath());
122
- if (seamImageInput(cfg, providers) !== true) {
123
- const target = imageReroute(ctx.config, ctx.role, ctx.rerouteRegime(now));
123
+ const listed = readPiListedModels(ctx.root);
124
+ if (seamImageInput(cfg, providers, undefined, listed) !== true) {
125
+ const target = imageReroute(ctx.config, ctx.role, ctx.rerouteRegime(now), undefined, null, undefined, null, listed);
124
126
  if (target !== null) {
125
127
  cfg = target.config;
126
128
  s.imageRerouteActive = true;
@@ -142,142 +144,143 @@ export async function tickPhase(ctx) {
142
144
  }
143
145
  }
144
146
  }
145
- // Capture what this tick runs on (the orchestrator's budget handback matches it against
147
+ // Capture what this run runs on (the orchestrator's budget handback matches it against
146
148
  // the fallback pair) and clear any stale handback flag: an abort request that lands while
147
- // the loop is idle must not name a later tick's resume.
149
+ // the runner is idle must not name a later run's resume.
148
150
  // The pair is keyed by the seam that ran (seamHoldKey), not the pi provider: a claude
149
- // fallback tick carries `claude`, matching the configured fallback pair that tickOnPair
150
- // (src/gates/budget-gates.ts) compares against, so a tick on a claude fallback is handed
151
+ // fallback run carries `claude`, matching the configured fallback pair that runOnPair
152
+ // (src/gates/budget-gates.ts) compares against, so a run on a claude fallback is handed
151
153
  // back when the budget gate reopens (PLANS.md "Mixed claude + pi fleets, part 2/4").
152
- ctx.setTickPair({ provider: seamHoldKey(cfg), model: cfg.model });
154
+ ctx.setRunPair({ provider: seamHoldKey(cfg), model: cfg.model });
153
155
  ctx.setHandedBack(false);
154
156
  s.runs += 1;
155
- // gen / peak ctx are per-tick windows, not lifetime totals (user decision 2026-08-25):
156
- // reset before the start-of-tick save so a working loop's columns grow live from 0 and
157
- // an idle loop's show its last completed tick. The loop's pi plumbing accumulates every pi
158
- // run of this tick (main + transient-timeout retry + conflict resolution) into them, and the
159
- // end-of-tick save persists the finished run's totals.
157
+ // gen / peak ctx are per-run windows, not lifetime totals (user decision 2026-08-25):
158
+ // reset before the start-of-run save so a working runner's columns grow live from 0 and
159
+ // an idle runner's show its last completed run. The runner's pi plumbing accumulates every pi
160
+ // run of this run (main + transient-timeout retry + conflict resolution) into them, and the
161
+ // end-of-run save persists the finished run's totals.
160
162
  s.generatedTokens = 0;
161
163
  s.peakContextTokens = 0;
162
164
  ctx.usage.reset();
163
165
  ctx.setRecoveryFailure(undefined);
164
- // A fresh per-tick abort controller and a cleared user-abort flag: an abort request that
165
- // lands while the loop is idle must not leak into the next tick.
166
- ctx.resetTickAbort();
166
+ // A fresh per-run abort controller and a cleared user-abort flag: an abort request that
167
+ // lands while the runner is idle must not leak into the next run.
168
+ ctx.resetRunAbort();
167
169
  s.running = true;
168
170
  s.lastRunStartedAt = Date.now();
169
- const tick = s.runs;
170
- const tickStartedAt = s.lastRunStartedAt;
171
+ const runNumber = s.runs;
172
+ const runStartedAt = s.lastRunStartedAt;
171
173
  ctx.save();
172
- // The selector this tick's runs start on (plans/model-tiers.md "Observability"): with
174
+ // The selector this run's runs start on (plans/model-tiers.md "Observability"): with
173
175
  // the budget fallback active the config already names the fallback pair, so the logged
174
176
  // string is what the runs actually use. Omitted when no model is configured (pi's own
175
177
  // default), so old logs and old configs render unchanged.
176
178
  logEventBestEffort(ctx.root, {
177
179
  source: ctx.role,
178
180
  type: "run_start",
179
- run: tick,
180
- // A tick routed onto the image-capable target — a blind-read handoff or an attachment
181
+ run: runNumber,
182
+ // A run routed onto the image-capable target — a blind-read handoff or an attachment
181
183
  // preflight — is stamped so the feed shows the reroute and the model it landed on.
182
184
  ...(s.imageRerouteActive ? { reroute: "image" } : {}),
183
185
  ...modelSelectorField(cfg),
184
186
  });
185
187
  let outcome;
186
188
  try {
187
- // The preflight can end the tick before any pi spawns: a text-only loop with an image
189
+ // The preflight can end the run before any pi spawns: a text-only runner with an image
188
190
  // attachment and no image-capable target has nothing to run, so it errors without
189
191
  // spending a model call. Everything else runs the pipeline as usual.
190
192
  outcome = preflightBlocked
191
193
  ? { result: "error" }
192
- : await ctx.runTick(cfg, { fallback, probe: probing, primary: primaryCfg });
194
+ : await ctx.runOnce(cfg, { fallback, probe: probing, primary: primaryCfg });
193
195
  }
194
196
  catch (err) {
195
197
  outcome = { result: "error" };
196
198
  s.lastError = errorMessage(err);
197
199
  // An exception between the dequeue/reclaim and the pi run leaves the request only in
198
- // the pending field — memory this failed tick is about to drop, with no outcome handler
199
- // left to re-queue it (the handlers run inside runTick, after the pi run). The queue is
200
+ // the pending field — memory this failed run is about to drop, with no outcome handler
201
+ // left to re-queue it (the handlers run inside runOnce, after the pi run). The queue is
200
202
  // the durable store, so the catch re-queues whatever is still pending, like every
201
203
  // unfulfilled outcome does; it is always null once a pi run has been accounted for.
202
204
  ctx.pending.requeuePendingUnfulfilled();
203
205
  }
204
- // The tick that owned the dequeued request is over: remove its image files unless an
206
+ // The run that owned the dequeued request is over: remove its image files unless an
205
207
  // unfulfilled outcome handed the request back to the queue (settleImages). Runs after the
206
208
  // catch above, which is the last point a requeue can happen.
207
209
  ctx.pending.settleImages();
208
210
  // Everything after the pi run — the outcome folds, the main-head read, next-run
209
- // scheduling, the episode warnings, and the run_end event — is the tick's per-result
210
- // bookkeeping policy, not runner lifecycle: it lives in src/tick/tick-finalize.ts, which owns
211
+ // scheduling, the episode warnings, and the run_end event — is the run's per-result
212
+ // bookkeeping policy, not runner lifecycle: it lives in src/run/run-finalize.ts, which owns
211
213
  // the ordering (the folds land on state before the schedule, the head read happens while
212
- // `running` is still true) and the per-episode warning crossings. The tick number and
213
- // start time are passed through as captured above: both must describe the tick run_start
214
- // announced, not whatever resetCounters (a documented mid-tick operation) may have left
214
+ // `running` is still true) and the per-episode warning crossings. The run number and
215
+ // start time are passed through as captured above: both must describe the run run_start
216
+ // announced, not whatever resetCounters (a documented mid-run operation) may have left
215
217
  // on the shared state by finalize time.
216
- const finalizeResult = await finalizeTick({
218
+ const finalizeResult = await finalizeRun({
217
219
  root: ctx.root,
218
220
  role: ctx.role,
219
221
  mainBranch: ctx.mainBranch,
220
222
  config: cfg,
221
223
  state: s,
222
224
  outcome,
223
- tick,
224
- tickStartedAt,
225
+ runNumber,
226
+ runStartedAt: runStartedAt,
225
227
  usage: ctx.usage,
226
228
  recoveryFailure: ctx.recoveryFailure,
227
229
  });
228
- // The tick is over: the captured pair must not outlive it (a later poll must never match
229
- // an idle loop's stale pair against a resumed fallback).
230
- ctx.setTickPair(undefined);
230
+ // The run is over: the captured pair must not outlive it (a later poll must never match
231
+ // an idle runner's stale pair against a resumed fallback).
232
+ ctx.setRunPair(undefined);
231
233
  return finalizeResult;
232
234
  }
233
- /** The tick pipeline's start half: resume-or-fresh policy, then run in the director's own
235
+ /** The run pipeline's start half: resume-or-fresh policy, then run in the director's own
234
236
  * worktree or on a leased pool slot. Returns the setup's outcome; a null plan means nothing to
235
237
  * run. */
236
- export async function runTickPhase(ctx, cfg, fallbackCtx) {
238
+ export async function runOncePhase(ctx, cfg, fallbackCtx) {
237
239
  const s = ctx.state;
238
- // How this tick starts — resume the interrupted session or run fresh, and which prompt —
239
- // is the resume policy, not runner mechanics: it lives in src/tick/tick-resume.ts, which also
240
- // consumes the resume flags and reclaims the interrupted tick's re-queued prompt. A null
241
- // plan means the loop has nothing to run.
242
- const plan = planTickStart({
240
+ // How this run starts — resume the interrupted session or run fresh, and which prompt —
241
+ // is the resume policy, not runner mechanics: it lives in src/run/run-resume.ts, which also
242
+ // consumes the resume flags and reclaims the interrupted run's re-queued prompt. A null
243
+ // plan means the runner has nothing to run.
244
+ const plan = planRunStart({
243
245
  root: ctx.root,
244
246
  role: ctx.role,
245
247
  state: s,
246
248
  pending: ctx.pending,
247
- tickPrompt: (agent, compact) => ctx.tickPrompt(agent, compact),
248
- agent: cfg.agent ?? DEFAULT_AGENT,
249
- // The team's configured claude config dir, so planTickStart's claude resume guard looks
249
+ runPrompt: (backend) => ctx.runPrompt(backend),
250
+ agent: cfg.agent ?? DEFAULT_BACKEND,
251
+ // The team's configured claude config dir, so planRunStart's claude resume guard looks
250
252
  // under the same dir the run's env writes its transcript to (note 4/4). Unset → undefined.
251
- claudeConfigDir: resolvedAgentConfig(cfg, "claude").configDir,
252
- // Every run gets the compact prompt (prompt.ts CompactPrompt), whatever window its model
253
- // declares: it re-sends ~3k fewer tokens on every turn (2026-10-18, to cut token spend).
254
- compact: true,
253
+ claudeConfigDir: resolvedBackendConfig(cfg, "claude").configDir,
254
+ // A director with no prompt but a kept merge pin still goes through the run, for its recovery.
255
+ recoveryOnly: ctx.role === DIRECTOR_ROLE && ctx.pending.get() === null && (await hasUnmergedPin(ctx.root, ctx.role)),
255
256
  });
256
257
  if (plan === null)
257
258
  return { result: "skipped" };
258
259
  if (ctx.role === DIRECTOR_ROLE) {
259
- // The image-reroute one-shot marker routes exactly ONE tick onto the image-capable
260
- // target (plans/image-input.md part 4/5, INV-J2): for a role loop, planTickStart consumes
261
- // it after reading its handoff-note facts. The director never resumes, so planTickStart's
260
+ // The image-reroute one-shot marker routes exactly ONE run onto the image-capable
261
+ // target (plans/image-input.md part 4/5, INV-J2): for a role runner, planRunStart consumes
262
+ // it after reading its handoff-note facts. The director never resumes, so planRunStart's
262
263
  // image-reroute branch never runs for it and the marker would otherwise linger and
263
- // re-route every later director tick off its own tier. It is cleared here — after this
264
- // tick is confirmed to run (an empty-inbox skip above returns before this, leaving the
265
- // reroute pending for the next eligible tick) — so the rerouted director prompt is
264
+ // re-route every later director run off its own tier. It is cleared here — after this
265
+ // run is confirmed to run (an empty-inbox skip above returns before this, leaving the
266
+ // reroute pending for the next eligible run) — so the rerouted director prompt is
266
267
  // delivered exactly once and the director is back on its own tier next.
267
- s.imageReroute = undefined;
268
+ // A promptless recovery run delivers no prompt, so it leaves a pending reroute for the next.
269
+ if (plan.prompt !== null)
270
+ s.imageReroute = undefined;
268
271
  // The director keeps its dedicated worktree (it runs outside the permit, consumes its
269
272
  // config-request file at that exact path, and never resumes). Hold it from before
270
- // ensureWorktree — creating or resetting it is part of the use — to the tick's end
273
+ // ensureWorktree — creating or resetting it is part of the use — to the run's end
271
274
  // (plans/disk-floor.md, part 2/4), so a concurrent pressure reclaim never cleans a tree
272
- // this tick is creating or working in.
273
- return useWorktree(ctx.root, worktreePath(ctx.root, ctx.role), () => tickWithWorktreePhase(ctx, plan, cfg, fallbackCtx));
275
+ // this run is creating or working in.
276
+ return useWorktree(ctx.root, worktreePath(ctx.root, ctx.role), () => runWithWorktreePhase(ctx, plan, cfg, fallbackCtx));
274
277
  }
275
- return runTickInLeasePhase(ctx, plan, cfg, fallbackCtx);
278
+ return runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx);
276
279
  }
277
- /** Whether a finished role tick's leased slot must stay pinned for the next tick's resume —
278
- * the resume flag a verdict armed inside the tick (`resumePending`), an abort or quiet kill,
280
+ /** Whether a finished role run's leased slot must stay pinned for the next run's resume —
281
+ * the resume flag a verdict armed inside the run (`resumePending`), an abort or quiet kill,
279
282
  * or a context cut-off. Exported so a test can evaluate the exact condition the lease release
280
- * uses: a resume flag set only later, by applyTickOutcome, is too late to keep the worktree. A
283
+ * uses: a resume flag set only later, by applyRunOutcome, is too late to keep the worktree. A
281
284
  * user abort discards its work and is never pinned. */
282
285
  export function slotPinnedForResume(state, outcome) {
283
286
  return (state.resumePending === true ||
@@ -285,13 +288,15 @@ export function slotPinnedForResume(state, outcome) {
285
288
  outcome?.result === "quiet_killed" ||
286
289
  outcome?.cutOff === true);
287
290
  }
288
- /** A non-director tick's whole life on a pooled slot (plans/worktree-pool.md, "Role ticks
291
+ /** A non-director run's whole life on a pooled slot (plans/worktree-pool.md, "Role runs
289
292
  * lease slots"): lease, author on the role's branch, release. The release pins the slot for
290
- * a tick whose next run resumes (aborted, quiet-killed, cut off, or a failed dirty run),
293
+ * a run whose next run resumes (aborted, quiet-killed, cut off, or a failed dirty run),
291
294
  * keeping the branch checked out and the uncommitted edits in the same cwd pi's `--continue`
292
295
  * needs; every other release detaches first so the branch is free for whichever slot the role
293
- * leases next. */
294
- async function runTickInLeasePhase(ctx, plan, cfg, fallbackCtx) {
296
+ * leases next. A run an exception ends releases its slot unsettled, so edits it leaves there
297
+ * are saved for the role's next run, and a run on a fresh checkout first takes back any such
298
+ * work an earlier run of the role left (restoreSalvagedPhase). */
299
+ async function runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx) {
295
300
  const lease = await leaseSlot(ctx.root, {
296
301
  role: ctx.role,
297
302
  purpose: "run",
@@ -299,30 +304,50 @@ async function runTickInLeasePhase(ctx, plan, cfg, fallbackCtx) {
299
304
  keep: plan.resuming || plan.handoff,
300
305
  signal: ctx.runSignal(),
301
306
  // The live view's pool budget: this read must not depend on tumwater.json being
302
- // readable right now (a broken file must not error the tick before its pi run).
307
+ // readable right now (a broken file must not error the run before its pi run).
303
308
  slotBudget: slotCount(ctx.config),
304
309
  });
305
310
  let outcome;
306
311
  try {
307
- if (!lease.preserved)
312
+ let run = plan;
313
+ if (!lease.preserved) {
308
314
  await checkoutRoleBranchPhase(ctx, lease.dir);
309
- outcome = await tickWithWorktreePhase(ctx, plan, cfg, fallbackCtx, lease.dir);
315
+ run = await restoreSalvagedPhase(ctx, plan, lease.dir);
316
+ }
317
+ outcome = await runWithWorktreePhase(ctx, run, cfg, fallbackCtx, lease.dir);
310
318
  return outcome;
311
319
  }
312
320
  finally {
313
- // The slot is pinned for the next tick's resume when the verdict armed `resumePending`
314
- // inside the tick (the error-dirty and rate-limit arms in src/tick/tick-verdict.ts), or
315
- // when the outcome is an abort, quiet kill, or context cut-off. applyTickOutcome sets
316
- // `resumePending` for the other outcomes only after runTick returns, so those are read off
321
+ // The slot is pinned for the next run's resume when the verdict armed `resumePending`
322
+ // inside the run (the error-dirty and rate-limit arms in src/run/run-verdict.ts), or
323
+ // when the outcome is an abort, quiet kill, or context cut-off. applyRunOutcome sets
324
+ // `resumePending` for the other outcomes only after runOnce returns, so those are read off
317
325
  // the outcome here instead.
318
326
  const pin = slotPinnedForResume(ctx.state, outcome);
319
- // Detach is best-effort so it can never mask the tick's own outcome or error; the release
327
+ // Detach is best-effort so it can never mask the run's own outcome or error; the release
320
328
  // still runs, and the next lease's checkout recovers an undetached branch either way.
321
329
  if (!pin)
322
330
  await gitTry(lease.dir, "checkout", "--detach");
323
- lease.release({ pin });
331
+ // An exception escaped the run (no outcome), so nothing settled the worktree — not even a
332
+ // dirty check, when the exception was git itself failing (2026-10-21's spawn wedge): any
333
+ // edits left in the slot are this role's unfinished work, saved for its next run.
334
+ lease.release({ pin, unsettled: outcome === undefined });
324
335
  }
325
336
  }
337
+ /** Give a run on a fresh checkout back the work an earlier run of its role left unsettled
338
+ * (src/git/slot-salvage.ts): restore the newest waiting patch into `dir` and run over it as
339
+ * planWithSalvagedWork says, told which files carry conflict markers and which hunks it must
340
+ * place by hand when the branch moved under the patch. A run that does not take such work, or
341
+ * has none waiting or cannot restore it now, keeps its plan. */
342
+ async function restoreSalvagedPhase(ctx, plan, dir) {
343
+ if (!takesSalvagedWork(plan))
344
+ return plan;
345
+ const restored = await restoreSalvagedWork(ctx.root, ctx.role, dir);
346
+ if (restored === null)
347
+ return plan;
348
+ const note = buildSalvageRestoreNote(conflictedFileHunks(dir, restored.conflicted), restored.unplaced, restored.patch);
349
+ return planWithSalvagedWork(ctx.role, plan, path.relative(ctx.root, restored.patch), note);
350
+ }
326
351
  /** Put `tumwater/<role>` on the leased slot before authoring. An existing branch is checked
327
352
  * out, never reset — `-B` would discard the unmerged commit it durably holds; an absent one
328
353
  * is created from main. `--ignore-other-worktrees` lets the slot take the branch while a
@@ -339,26 +364,32 @@ async function checkoutRoleBranchPhase(ctx, dir) {
339
364
  }
340
365
  await git(dir, "clean", "-fd");
341
366
  }
342
- async function tickWithWorktreePhase(ctx, plan, cfg, fallbackCtx,
343
- /** The leased slot the tick runs in; omitted by the director, which ensures its own. */
367
+ async function runWithWorktreePhase(ctx, plan, cfg, fallbackCtx,
368
+ /** The leased slot the run runs in; omitted by the director, which ensures its own. */
344
369
  leasedDir) {
345
370
  const s = ctx.state;
346
371
  const { priorMergeFailure, resuming, handoff, resumeCause, userPrompt } = plan;
347
- let prompt = plan.prompt;
372
+ // Empty only for a promptless recovery run, which ends before any use of it (see below).
373
+ let prompt = plan.prompt ?? "";
348
374
  const wt = leasedDir ?? (await ensureWorktree(ctx.root, ctx.role, ctx.mainBranch));
349
375
  if (resuming || handoff) {
350
376
  // Keep the interrupted run's uncommitted edits; clear only stray merge/rebase state. A
351
377
  // handoff resumes a FRESH session over those same edits (the newest author session belongs
352
378
  // to another agent), so it preserves the worktree exactly as a resume does.
353
379
  await abortSync(wt);
354
- logEventBestEffort(ctx.root, { source: ctx.role, type: "resume", cause: resumeCause });
380
+ logEventBestEffort(ctx.root, {
381
+ source: ctx.role,
382
+ type: "resume",
383
+ cause: resumeCause,
384
+ ...(plan.restored !== undefined ? { restored: plan.restored } : {}),
385
+ });
355
386
  }
356
387
  else {
357
- // Salvage a commit a previous tick left unmerged (src/loop/leftover.ts): recovery puts it
388
+ // Salvage a commit a previous run left unmerged (src/runner/leftover.ts): recovery puts it
358
389
  // back on the merge queue, so it merges through the orchestrator's merge pipeline — the
359
- // same gate as a fresh tick's change, and main keeps exactly one writer (an in-tick
390
+ // same gate as a fresh run's change, and main keeps exactly one writer (an in-run
360
391
  // recovery merge raced the slot's batches into `merge_blocked`). A salvaged leftover
361
- // ENDS the tick: the role holds one merge ref, and the leftover owns it until its
392
+ // ENDS the run: the role holds one merge ref, and the leftover owns it until its
362
393
  // merge resolves. With nothing to salvage the branch holds nothing either, so the reset
363
394
  // below leaves pristine main for the red-main gate.
364
395
  // A permanent reviewer config error — held from re-queueing — clears once the resolved
@@ -371,12 +402,20 @@ leasedDir) {
371
402
  root: ctx.root,
372
403
  role: ctx.role,
373
404
  mainBranch: ctx.mainBranch,
374
- tick: s.runs,
405
+ runNumber: s.runs,
375
406
  wt,
376
407
  mergeConflicts: s.mergeConflicts,
377
408
  conflictHandback: s.conflictHandback,
378
409
  mergeReviewError: s.mergeReviewError,
410
+ mainRedRetry: s.mainRedRetry,
379
411
  });
412
+ if (recovered?.kind === "handback" && plan.prompt === null) {
413
+ // A promptless recovery run has no authoring run to hand the pin back to: leave the pin and
414
+ // the conflict state exactly as they are, so the one hand-back the lineage is owed happens on
415
+ // the director's next prompt-driven run.
416
+ await resetWorktreeToMain(wt, ctx.mainBranch);
417
+ return { result: "skipped" };
418
+ }
380
419
  if (recovered?.kind === "handback") {
381
420
  // The pin hit the conflict cap and its lineage has not been handed back yet: keep the
382
421
  // pin, record the hand-back, and fall through to author the resolution on current main.
@@ -392,22 +431,26 @@ leasedDir) {
392
431
  }
393
432
  else if (recovered?.kind === "discarded") {
394
433
  // The pin hit the conflict cap and is gone: nothing holds the role any more, so this
395
- // tick authors on a fresh main like any other — told what was dropped and why.
434
+ // run authors on a fresh main like any other — told what was dropped and why.
396
435
  s.mergeConflicts = undefined;
397
436
  s.conflictHandback = undefined;
398
437
  s.conflictDiscard = { sha: recovered.sha, summary: recovered.summary, attempts: recovered.attempts, at: Date.now() };
399
438
  prompt = appendPromptNote(prompt, buildConflictDiscardNote(recovered.summary, recovered.attempts));
400
439
  }
401
440
  else if (recovered)
402
- return ctx.finishRecoveryTick(recovered, userPrompt, wt, priorMergeFailure);
441
+ return ctx.finishRecoveryRun(recovered, userPrompt, wt, priorMergeFailure);
403
442
  await resetWorktreeToMain(wt, ctx.mainBranch);
443
+ // A promptless recovery run found nothing to salvage (or deleted a stale pin): it authors
444
+ // nothing, so it ends here rather than reach the authoring run with a null prompt.
445
+ if (plan.prompt === null)
446
+ return { result: "skipped" };
404
447
  // Red-main baseline gate (src/baseline/main-red.ts): the worktree is pristine main right
405
448
  // now — verify main's own suite before spending an authoring run on top of it. Only roles
406
- // whose diff can carry code changes are blocked; resume ticks skip this by construction
449
+ // whose diff can carry code changes are blocked; resume runs skip this by construction
407
450
  // (their worktree is not pristine main). The `bugfix` healer is exempt because its fix is
408
451
  // the team's only way back to green, so instead of blocking it we hand it the failure
409
452
  //the same check runs, but a red main appends a <main-red>
410
- // note to this tick's prompt so it reproduces and fixes that failure rather than hunting
453
+ // note to this run's prompt so it reproduces and fixes that failure rather than hunting
411
454
  // blind.
412
455
  if (ctx.baseRole === "bugfix") {
413
456
  const note = await bugfixMainRedNote(ctx.root, ctx.role, wt);
@@ -415,21 +458,24 @@ leasedDir) {
415
458
  prompt = appendPromptNote(prompt, note);
416
459
  }
417
460
  else {
418
- const blocked = await mainRedGate(ctx.root, ctx.role, wt);
461
+ const knownRed = [];
462
+ const blocked = await mainRedGate(ctx.root, ctx.role, wt, undefined, (tests) => knownRed.push(tests));
419
463
  if (blocked) {
420
464
  // The dequeued prompt never ran: put it back in its queue before returning, so it is
421
465
  // not lost to a restart (the pending field is memory-only) or dropped by the next
422
- // tick's outcome handling — the queue is the durable store, and once main is green
423
- // the next tick dequeues it again (criterion b).
466
+ // run's outcome handling — the queue is the durable store, and once main is green
467
+ // the next run dequeues it again (criterion b).
424
468
  ctx.pending.requeueUnfulfilled(userPrompt);
425
469
  ctx.pending.clear();
426
470
  return blocked;
427
471
  }
472
+ if (knownRed[0])
473
+ prompt = appendPromptNote(prompt, buildMainKnownRedNote(knownRed[0]));
428
474
  }
429
475
  }
430
476
  // A change handed back after its merges kept conflicting — or a persisted hand-back the
431
- // author's tick has not applied yet — is re-applied over current main with the conflict
432
- // markers left as ordinary uncommitted edits so this tick resolves them (PLANS.md "Robust
477
+ // author's run has not applied yet — is re-applied over current main with the conflict
478
+ // markers left as ordinary uncommitted edits so this run resolves them (PLANS.md "Robust
433
479
  // conflict landing, part 2/2"). The merge ref is deleted once the diff is in the worktree.
434
480
  // Runs before the revision branch, which applies a revision hand-back itself. A resume or
435
481
  // handoff keeps the interrupted worktree as-is, so re-applying here would cherry-pick onto
@@ -457,7 +503,7 @@ leasedDir) {
457
503
  }
458
504
  else {
459
505
  // The object is gone or the apply failed for a non-conflict reason: drop the hand-back
460
- // so the author starts fresh rather than retrying it every tick.
506
+ // so the author starts fresh rather than retrying it every run.
461
507
  s.conflictHandback = undefined;
462
508
  await deleteRef(ctx.root, mergeRefName(ctx.role));
463
509
  logEventBestEffort(ctx.root, { source: ctx.role, type: "conflict_handback", action: "failed", sha: hb.sha, reason: hb.reason });
@@ -465,40 +511,40 @@ leasedDir) {
465
511
  }
466
512
  const piStartedAt = Date.now();
467
513
  // A rejected change owes a revision (plans/revise-rejected.md): re-apply its diff to current
468
- // main as uncommitted edits so this tick revises it instead of authoring from scratch, and
469
- // append the revision note the prompt skipped. `revisionRound` records that THIS tick holds
514
+ // main as uncommitted edits so this run revises it instead of authoring from scratch, and
515
+ // append the revision note the prompt skipped. `revisionRound` records that THIS run holds
470
516
  // the rejected diff — set only when the revision is applied here, or a resume carries the
471
- // prior attempt's already-applied edits — so the post-run handlers read the tick rather than
472
- // `state.revision`, which a per-role user-request tick leaves set by design. A conflict (or a
517
+ // prior attempt's already-applied edits — so the post-run handlers read the run rather than
518
+ // `state.revision`, which a per-role user-request run leaves set by design. A conflict (or a
473
519
  // gone object) clears the revision, deletes the rejected ref, and falls back to the plain
474
520
  // rejection note.
475
521
  let revisionRound;
476
522
  if (s.revision && ctx.role !== DIRECTOR_ROLE && userPrompt === null) {
477
523
  const { sha, round } = s.revision;
478
- // The prompt's rejected/revision notes read the loop's most recent rejection; an absent
479
- // one (a fresh loop or a hand-edited state) reads the empty reasons list. Computed once
524
+ // The prompt's rejected/revision notes read the runner's most recent rejection; an absent
525
+ // one (a fresh runner or a hand-edited state) reads the empty reasons list. Computed once
480
526
  // for the three revision-append sites and the rejected-note fallback below, so the
481
527
  // get-or-default idiom cannot drift between them.
482
528
  const lastReviewOrEmpty = s.lastReview ?? { reasons: [] };
483
529
  const revisionNote = buildRevisionNote(lastReviewOrEmpty, round);
484
530
  if (resuming || handoff) {
485
531
  // The interrupted revision run's applied edits are still in the worktree (both the
486
- // resume and the cross-agent handoff keep uncommitted work), so this tick still owns
532
+ // resume and the cross-agent handoff keep uncommitted work), so this run still owns
487
533
  // the revision. A handoff runs the revision in a fresh session rather than
488
534
  // re-cherry-picking it onto the already-dirty worktree.
489
535
  revisionRound = round;
490
536
  if (handoff) {
491
537
  // A handoff is a FRESH session: it has none of the interrupted run's context, and
492
- // tick-prompt.ts suppresses the rejected-review note whenever state.revision is set,
538
+ // run-prompt.ts suppresses the rejected-review note whenever state.revision is set,
493
539
  // so without this the fresh run would see neither the objections nor the revision
494
540
  // instructions. A resume keeps its session context and must not see the note twice.
495
- prompt = appendPromptNote(prompt, revisionNote);
541
+ prompt = prependPromptNote(prompt, revisionNote);
496
542
  }
497
543
  }
498
544
  else if (await applyRevision(wt, ctx.mainBranch, sha)) {
499
545
  revisionRound = round;
500
546
  logEventBestEffort(ctx.root, { source: ctx.role, type: "revision", action: "applied", round, sha });
501
- prompt = appendPromptNote(prompt, revisionNote);
547
+ prompt = prependPromptNote(prompt, revisionNote);
502
548
  }
503
549
  else {
504
550
  // The clean re-apply conflicted: re-apply with the markers left in place so the author
@@ -523,19 +569,19 @@ leasedDir) {
523
569
  reason: "revision",
524
570
  conflicted: withConflicts.conflicted,
525
571
  });
526
- prompt = appendPromptNote(prompt, revisionNote);
572
+ prompt = prependPromptNote(prompt, revisionNote);
527
573
  prompt = await appendHandbackNote(prompt, wt, ctx.mainBranch, sha, "revision", withConflicts.conflicted);
528
574
  }
529
575
  else {
530
576
  s.revision = undefined;
531
577
  await deleteRef(ctx.root, rejectedRefName(ctx.role));
532
578
  logEventBestEffort(ctx.root, { source: ctx.role, type: "revision", action: "conflict", round, sha });
533
- prompt = appendPromptNote(prompt, buildRejectedReviewNote(lastReviewOrEmpty, ctx.role));
579
+ prompt = prependPromptNote(prompt, buildRejectedReviewNote(lastReviewOrEmpty, ctx.role));
534
580
  prompt = appendPromptNote(prompt, "The rejected diff no longer applies to current main.");
535
581
  }
536
582
  }
537
583
  }
538
- // A handoff runs a FRESH session over the interrupted tick's kept worktree. When that tick had
584
+ // A handoff runs a FRESH session over the interrupted run's kept worktree. When that run had
539
585
  // already applied a conflict hand-back, the marker-bearing files are still there but the fresh
540
586
  // run has none of the interrupted session's context explaining them, so re-send the hand-back
541
587
  // note (a resume keeps its session context and must not see the note twice). The revision
@@ -547,101 +593,33 @@ leasedDir) {
547
593
  const conflicted = await markerBearingFiles(wt);
548
594
  prompt = await appendHandbackNote(prompt, wt, ctx.mainBranch, hb.sha, hb.reason, conflicted);
549
595
  }
550
- const tickBase = await headOf(wt, "HEAD");
596
+ const runBase = await headOf(wt, "HEAD");
597
+ // The stray-write warning only reports, so a `git status` that fails on either side skips it.
598
+ const primaryBefore = await changedFiles(ctx.root).catch(() => null);
551
599
  const pi = await ctx.pi.runAuthoringPi(wt, prompt, `tumwater-${ctx.role}-${s.runs}`, resuming, cfg);
552
- // The harness owns every commit (INV-H1): fold any the agent made into the tick's one commit.
553
- await foldAgentCommitsSince(ctx, wt, tickBase);
600
+ // The harness owns every commit (INV-H1): fold any the agent made into the run's one commit.
601
+ await foldAgentCommitsSince(ctx, wt, runBase);
602
+ // A write that landed in the primary checkout never reaches the worktree's commit, so the run
603
+ // can read as no_change with its work orphaned (BUGS.md, 2026-10-20). The before/after diff
604
+ // cannot attribute paths to this run when roles overlap, so only warn: the verdict, resume
605
+ // handling and model-fallback fold below stay as they were.
606
+ const primaryAfter = primaryBefore === null ? null : await changedFiles(ctx.root).catch(() => null);
607
+ const strayed = primaryAfter === null ? [] : primaryAfter.filter((f) => !primaryBefore.includes(f));
608
+ if (strayed.length > 0 && !pi.aborted) {
609
+ const shown = strayed.slice(0, 10).join(", ") + (strayed.length > 10 ? `, … (${strayed.length} paths)` : "");
610
+ ctx.warn(`wrote outside the worktree (possibly another writer's): ${shown}`);
611
+ }
554
612
  // Fold the authoring run's verdict into the role's model-fallback episode NOW, before the
555
613
  // post-run handlers spend any more pi runs: only this run is evidence about the primary.
556
614
  ctx.foldModelFallback(fallbackCtx, pi);
557
615
  // The `qa` observer's reply ends with a result-carrying `FLOW:` line (plans/observer-roles.md
558
616
  // 2/2). Extracted once here, recorded only at the success returns below — an interrupted or
559
- // failed tick must not advance the rotation. The harness records it, never pi.
617
+ // failed run must not advance the rotation. The harness records it, never pi.
560
618
  const flow = ctx.baseRole === "qa" ? extractFlow(pi.finalText) : null;
561
- return handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt, flow, piStartedAt, cfg);
562
- }
563
- /** Merge the worktree branch on main (see src/merge/merge-to-main.ts for the
564
- * rebase → verify → ff-merge → conflict-retry flow): delegates with the loop identity, tick
565
- * number, and shared pi wiring so a conflict-resolution run folds into this tick's counters
566
- * like any other pi run. Only the refusal-note merge (src/verdict/refusal.ts) still uses it —
567
- * reviewed changes merge through the merge pipeline's leased verification slot instead; md-only
568
- * notes are review-exempt by construction, so they keep this branch path (plans/merge-queue.md
569
- * 2/5). */
570
- async function mergePhase(ctx, wt, summary) {
571
- return mergeToMain({
572
- root: ctx.root,
573
- role: ctx.role,
574
- mainBranch: ctx.mainBranch,
575
- exemptPaths: ctx.config.review.exemptPaths,
576
- config: ctx.config,
577
- tick: ctx.state.runs,
578
- runPi: (w, prompt, sessionName, config) => ctx.pi.runRolePi(w, prompt, sessionName, false, config),
579
- }, wt, summary);
580
- }
581
- /** Turn a finished pi run into its TickOutcome: the post-run half of runTick, split off so each
582
- * half reads on its own screen — the setup above ends at the pi return. The verdict
583
- * classification (abort, config request, quiet kill, timeout, refusal, failure, no-change) lives
584
- * in resolveTickVerdict (src/tick/tick-verdict.ts), and the fulfillable path — staging —
585
- * stays here. `userPrompt` is the raw director prompt this tick is executing (null for role
586
- * loops) so unfulfilled outcomes can re-queue it; `revisionRound` is the revision this tick holds
587
- * (undefined when it is not a revision); `flow` is the qa observer's FLOW line (null for every
588
- * other role). */
589
- export async function handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt, flow, piStartedAt, cfg) {
590
- // Everything that decides the run left nothing mergeable — abort, config request,
591
- // quiet kill, timeout, refusal, failure without changes, no change — lives in
592
- // resolveTickVerdict (src/tick/tick-verdict.ts); null means the run IS fulfillable.
593
- const verdict = await resolveTickVerdict({
594
- root: ctx.root,
595
- role: ctx.role,
596
- mainBranch: ctx.mainBranch,
597
- state: ctx.state,
598
- pending: ctx.pending,
599
- turns: ctx.usage.turns,
600
- userPrompt,
601
- revisionRound,
602
- wt,
603
- pi,
604
- flow,
605
- // The image-reroute arm resolves its target from the live config and the current
606
- // regime, and names the model this tick ran on as the handoff's `from`.
607
- config: ctx.config,
608
- rerouteRegime: ctx.rerouteRegime(piStartedAt),
609
- runModel: cfg.model,
610
- warn: (message) => ctx.warn(message),
611
- merge: (w, sum) => mergePhase(ctx, w, sum),
612
- finishAbortedTick: () => ctx.finishAbortedTick(userPrompt, wt),
613
- });
614
- if (verdict)
615
- return verdict;
616
- // The commit is pinned and the worktree is free (src/tick/tick-stage.ts): stage it for the
617
- // merge queue and END the tick — the orchestrator drains the queue on its single merge
618
- // slot, outside the author semaphore, so this slot is free the moment the work is
619
- // committed (plans/merge-queue.md 3/5). Staging derives the commit message from the reply's
620
- // SUMMARY block (one bounded follow-up turn when the run left none), flags high-friction
621
- // ticks, pins the commit by the role's merge ref BEFORE freeing the worktree (invariant
622
- // 4 — a failed pin defers to next-tick recovery), and enqueues the merge, which runs the
623
- // same gate + merge flow through runMergePi/foldMergeUsage on this same state object
624
- // — recording the commit count, the outcome, and the reviewer spend into it — and logs
625
- // merge_done/merge_failed; a non-terminal outcome keeps the pin for next-tick leftover recovery.
626
- // `commits` was NOT incremented above: it counts merged changes only.
627
- return stageTickMerge({
628
- root: ctx.root,
629
- role: ctx.role,
630
- state: ctx.state,
631
- config: ctx.config,
632
- tickTurns: ctx.usage.turns,
633
- userPrompt,
634
- revisionRound,
635
- mainBranch: ctx.mainBranch,
636
- wt,
637
- finalText: pi.finalText,
638
- flow,
639
- piStartedAt,
640
- warn: (message) => ctx.warn(message),
641
- requestSummary: (w, n) => ctx.pi.requestSummary(w, cfg, n),
642
- stageCheck: (w) => stageCheckFindings(w, ctx.mainBranch, ctx.config.review.exemptPaths, ctx.role),
643
- requestStageFix: (w, findings) => ctx.pi.requestStageFix(w, findings, cfg),
644
- pinAndReset: (w, sha) => ctx.pinAndReset(w, sha),
645
- finishAbortedTick: () => ctx.finishAbortedTick(userPrompt, wt),
646
- });
619
+ // The `feedback` observer's reply ends with a result-carrying `FILED: #<n> — <title>` line
620
+ // (plans/feedback-role.md 2/2). Extracted once here, recorded only at the success returns
621
+ // below — an interrupted or failed run must not record a filing it never declared. The
622
+ // harness records it, never pi.
623
+ const filed = ctx.baseRole === "feedback" ? extractFiled(pi.finalText) : null;
624
+ return handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt, flow, filed, piStartedAt, cfg);
647
625
  }