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,36 +1,87 @@
1
- // vocabulary: plain
2
- import { aheadOfMain, commitMessage, deleteRef, headOf, isMergedInto, refSha, setRef } from "../git/git.js";
1
+ import { aheadOfMain, commitExists, commitMessage, deleteRef, deleteRefIfAt, headOf, isMergedInto, refSha, setRef } from "../git/git.js";
3
2
  import { parseCommitMetadata } from "../git/commit-message.js";
4
3
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
5
4
  import { enqueueMerge, queuedMerges } from "../merge/merge-queue.js";
6
5
  import { mergeRefName } from "../paths.js";
7
6
  import { shortSha } from "../text/format.js";
8
- /** Salvaging a commit a previous tick left unmerged (plans/merge-queue.md). Since merge queue
7
+ /** Salvaging a commit a previous run left unmerged (plans/merge-queue.md). Since merge queue
9
8
  * 2/5 the role's branch is reset to main the moment its commit is pinned, so the leftover
10
9
  * normally lives in `refs/tumwater/landing/<role>`: recovery puts that sha back on the durable
11
- * merge queue, so it merges through the SAME slot, gate, and strike cap a fresh tick's change
10
+ * merge queue, so it merges through the SAME slot, gate, and strike cap a fresh run's change
12
11
  * does — no crash or abort path smuggles unreviewed work into main (invariant 1), and main keeps
13
- * exactly one writer, the orchestrator's merge slot (an in-tick recovery merge used to race
12
+ * exactly one writer, the orchestrator's merge slot (an in-run recovery merge used to race
14
13
  * the slot's batches into `merge_blocked`). A commit with NO pin — a crash in the window between
15
- * the tick's commit and its pin write, or a failed pin write itself — still sits on the branch
14
+ * the run's commit and its pin write, or a failed pin write itself — still sits on the branch
16
15
  * ahead of main; recovery adopts that tip into the pin scheme and queues it, so invariant 1
17
- * holds whether or not the pin survived. Split out of loop.ts — which keeps the tick lifecycle
16
+ * holds whether or not the pin survived. Split out of runner.ts — which keeps the run lifecycle
18
17
  * around it — because this is a self-contained concern with its own entry condition and git
19
- * surface; the only things it borrows from the loop are identity, the tick number, the
18
+ * surface; the only things it borrows from the runner are identity, the run number, the
20
19
  * worktree (for the no-pin fallback), and three pieces of the role's merge state: its
21
20
  * conflict streak, outstanding hand-back, and permanent-reviewer hold. */
22
21
  /** Consecutive merges of one pinned sha that may end `merge_conflict` before recovery stops
23
- * re-queuing it (LoopState.mergeConflicts). Each attempt costs a conflict-resolution run, and
22
+ * re-queuing it (RunnerState.mergeConflicts). Each attempt costs a conflict-resolution run, and
24
23
  * a role holding a pin never authors: without a cap an unmergeable change would re-queue
25
24
  * forever, burning one resolution run per drain. At the cap the pin is handed back to its
26
25
  * author with the markers in place rather than discarded (PLANS.md "Robust conflict landing,
27
26
  * part 2/2"); a lineage already handed back once is discarded instead. */
28
27
  export const MERGE_CONFLICT_LIMIT = 2;
29
- /** Queue a commit a previous tick left unmerged. Entry condition: the merge ref exists and
28
+ /** Delete a pin main already contains — the work merged and a crash skipped its un-pinning —
29
+ * so nothing re-merges what main holds. True when it was stale and is gone. Shared by the
30
+ * author's recovery and the pipeline's re-queue (pinnedRetrySha). */
31
+ async function dropStalePin(root, ref, sha, mainBranch) {
32
+ if (!(await isMergedInto(root, sha, mainBranch)))
33
+ return false;
34
+ await deleteRef(root, ref);
35
+ return true;
36
+ }
37
+ /** Delete a pin whose commit is missing from the repository, with a warning: it can never merge,
38
+ * and re-queuing it would end every run on an error. True when it was dangling and is gone. */
39
+ async function dropDanglingPin(root, role, ref, sha) {
40
+ if (await commitExists(root, sha))
41
+ return false;
42
+ await deleteRefIfAt(root, ref, sha);
43
+ warnEventBestEffort(root, role, `dropped merge pin ${shortSha(sha)}: its commit is missing from the repository, so it can never merge`);
44
+ return true;
45
+ }
46
+ /** The permanent-reviewer-config hold when it names `sha`, else undefined. */
47
+ function reviewHold(held, sha) {
48
+ return held?.sha === sha ? held : undefined;
49
+ }
50
+ /** Consecutive conflict-ended merges of `sha` (0 when the streak names another sha). */
51
+ function conflictAttempts(streak, sha) {
52
+ return streak?.sha === sha ? streak.count : 0;
53
+ }
54
+ /** The sha of a role's kept pin when the merge pipeline may re-queue it itself: the pin exists,
55
+ * main does not already contain it (a stale pin is deleted, as recovery does), no reviewer
56
+ * configuration hold names it, and its conflict streak is under MERGE_CONFLICT_LIMIT. Null
57
+ * otherwise — a pin at the limit is the author's conflict hand-back, a held one waits for a
58
+ * config change, and both stay with recoverLeftover. */
59
+ export async function pinnedRetrySha(root, role, mainBranch, state) {
60
+ const ref = mergeRefName(role);
61
+ const sha = await refSha(root, ref).catch(() => null);
62
+ if (!sha)
63
+ return null;
64
+ if (await dropDanglingPin(root, role, ref, sha))
65
+ return null;
66
+ if (await dropStalePin(root, ref, sha, mainBranch))
67
+ return null;
68
+ if (reviewHold(state.mergeReviewError, sha))
69
+ return null;
70
+ if (conflictAttempts(state.mergeConflicts, sha) >= MERGE_CONFLICT_LIMIT)
71
+ return null;
72
+ return sha;
73
+ }
74
+ /** Whether the role holds a kept merge pin — the ref `recoverLeftover` reads. Ref presence is the
75
+ * whole answer: a pin main already holds is left to `recoverLeftover`, which deletes it. */
76
+ export async function hasUnmergedPin(root, role) {
77
+ return (await refSha(root, mergeRefName(role)).catch(() => null)) !== null;
78
+ }
79
+ /** Queue a commit a previous run left unmerged. Entry condition: the merge ref exists and
30
80
  * its sha is not yet contained in main — or, with no pin at all, the role's branch is ahead of
31
81
  * main (crash between the commit and the pin). A present-but-contained ref is stale — a crash
32
- * between the ff-merge and the ref deletion — and is deleted without queuing anything. Returns
33
- * null when there was nothing to salvage. The queued entry carries the pin's sha plus the
82
+ * between the ff-merge and the ref deletion — and is deleted without queuing anything, and so is
83
+ * a pin whose commit is missing from the repository (with a warning — it can never merge).
84
+ * Returns null when there was nothing to salvage. The queued entry carries the pin's sha plus the
34
85
  * summary, body, and high-friction flag read back from its commit message, because the
35
86
  * authoring run that set them is gone; from there the merge slot keeps or deletes the ref per
36
87
  * the merger's usual policy. An unreadable ref or worktree reads as no leftover — only git-level
@@ -44,18 +95,16 @@ export async function recoverLeftover(ctx) {
44
95
  const ref = mergeRefName(ctx.role);
45
96
  let sha = await refSha(ctx.root, ref).catch(() => null);
46
97
  if (sha) {
47
- if (await isMergedInto(ctx.root, sha, ctx.mainBranch)) {
48
- // Stale pin: the work already merged and a crash skipped its un-pinning. Clean it up so
49
- // the next tick does not re-merge what main already holds.
50
- await deleteRef(ctx.root, ref);
98
+ if (await dropDanglingPin(ctx.root, ctx.role, ref, sha))
99
+ return null;
100
+ if (await dropStalePin(ctx.root, ref, sha, ctx.mainBranch))
51
101
  return null;
52
- }
53
102
  }
54
103
  else {
55
104
  // No pin: either nothing was left behind, or a crash hit the commit→pin window (or
56
105
  // the pin write failed) and the commit still sits on the branch ahead of main. Recover that
57
106
  // tip too — invariant 1 must hold whether or not the pin survived. An unreadable worktree
58
- // reads as "no leftover", exactly like a failed ref read: never propagate into the tick.
107
+ // reads as "no leftover", exactly like a failed ref read: never propagate into the run.
59
108
  const ahead = await aheadOfMain(ctx.wt, ctx.mainBranch).catch(() => 0);
60
109
  if (ahead <= 0)
61
110
  return null;
@@ -76,10 +125,16 @@ export async function recoverLeftover(ctx) {
76
125
  // full gate check for a request that can never succeed until the model config changes. The
77
126
  // caller clears the hold (and re-queues) once the resolved reviewer selector differs, so a
78
127
  // fixed config recovers automatically (BUGS.md 2026-10-06).
79
- const held = ctx.mergeReviewError;
80
- if (held?.sha === sha)
128
+ const held = reviewHold(ctx.mergeReviewError, sha);
129
+ if (held)
81
130
  return { kind: "held", sha, message: held.message };
82
- const attempts = ctx.mergeConflicts?.sha === sha ? ctx.mergeConflicts.count : 0;
131
+ // `requeueMainRedPin` owns the re-queue of a main_red pin, once main's head moves. Queuing it
132
+ // here would pay a full gate check against the same red head, whose result is already known.
133
+ if (ctx.mainRedRetry) {
134
+ const message = `kept change ${shortSha(sha)} waits for main to move past red ${shortSha(ctx.mainRedRetry.head)} before its next merge`;
135
+ return { kind: "main_red_wait", sha, message };
136
+ }
137
+ const attempts = conflictAttempts(ctx.mergeConflicts, sha);
83
138
  if (attempts >= MERGE_CONFLICT_LIMIT) {
84
139
  const summary = meta.subject ?? `leftover ${shortSha(sha)}`;
85
140
  if (ctx.conflictHandback) {
@@ -91,14 +146,14 @@ export async function recoverLeftover(ctx) {
91
146
  }
92
147
  // Main has moved too far under this change for the resolver to reconcile it: keep the pin
93
148
  // and hand it back to its author, who re-applies it over current main with the markers in
94
- // place and resolves them. The loop's next step logs the conflict_handback event and sets
95
- // LoopState.conflictHandback (the pin survives until that re-apply).
149
+ // place and resolves them. The runner's next step logs the conflict_handback event and sets
150
+ // RunnerState.conflictHandback (the pin survives until that re-apply).
96
151
  return { kind: "handback", sha, summary, attempts };
97
152
  }
98
153
  const entry = {
99
154
  role: ctx.role,
100
155
  sha,
101
- run: ctx.tick,
156
+ run: ctx.runNumber,
102
157
  // Name what merges, not merely that a recovery happened: the recovered commit's own subject
103
158
  // rides the merge, so the failure digest's "Merged in the window" can correlate a
104
159
  // recovered merge with the work it names (BUGS.md 2026-09-21). Unreadable messages fall
@@ -121,7 +176,7 @@ export async function recoverLeftover(ctx) {
121
176
  /** The review-gate metadata a pinned leftover commit carries in its own message: its
122
177
  * subject, high-friction flag, and author's body, or an empty object when the commit
123
178
  * predates the contract or the message cannot be read. Recovery merges through the same gate
124
- * as a fresh tick, so it must present those fields the same way — otherwise a flagged change
179
+ * as a fresh run, so it must present those fields the same way — otherwise a flagged change
125
180
  * is silently reviewed as routine (BUGS.md 2026-09-19), and the merged event names the
126
181
  * recovery but not the work (BUGS.md 2026-09-21). */
127
182
  async function recoveredMetadata(root, sha) {
@@ -5,18 +5,18 @@ export const MODEL_FALLBACK_COOLDOWN_MS = 5 * 60_000;
5
5
  /** The ceiling a repeatedly-failing probe's cooldown doubles toward. */
6
6
  export const MODEL_FALLBACK_MAX_COOLDOWN_MS = 30 * 60_000;
7
7
  /** True only while the role is in a fallback episode whose probe is still in the future — the
8
- * ticks that run on the fallback pair. */
8
+ * runs that use the fallback pair. */
9
9
  export function modelFallbackActive(state, now) {
10
10
  return state !== undefined && state.since > 0 && now < state.probeAt;
11
11
  }
12
12
  /** True only while the role is in a fallback episode whose cooldown has elapsed: the next
13
- * tick runs on the primary as the probe. */
13
+ * run uses the primary as the probe. */
14
14
  export function modelFallbackProbe(state, now) {
15
15
  return state !== undefined && state.since > 0 && now >= state.probeAt;
16
16
  }
17
- /** Classify one authoring run's verdict about the primary (src/tick/tick-usage.ts records the
18
- * same flags for the team-wide hold). A run that was aborted, quiet-killed, or timed out — or
19
- * that read an image its model cannot see (the runner killed it, plans/image-input.md) — is
17
+ /** Classify one authoring agent run's verdict about the primary (src/run/run-usage.ts records the
18
+ * same flags for the team-wide hold). An agent run that was aborted, quiet-killed, or timed out
19
+ * — or that read an image its model cannot see (the runner killed it, plans/image-input.md) — is
20
20
  * inconclusive even when it also reports provider evidence: the harness killed it, so it is
21
21
  * no verdict about the primary's health. */
22
22
  export function modelFallbackVerdict(pi) {
@@ -26,17 +26,17 @@ export function modelFallbackVerdict(pi) {
26
26
  return "provider-failure";
27
27
  return "answered";
28
28
  }
29
- /** Fold one authoring run's verdict into the role's fallback state, returning the next state
29
+ /** Fold one authoring agent run's verdict into the role's fallback state, returning the next state
30
30
  * or undefined for "running the primary" (no episode).
31
31
  *
32
32
  * - Not in fallback, provider failure: increment the consecutive count; the
33
33
  * MODEL_FALLBACK_FAILURES-th trips the episode.
34
- * - Not in fallback, an answering run: clear the running count (undefined).
35
- * - Not in fallback, an inconclusive run: unchanged — a harness kill is not evidence the
34
+ * - Not in fallback, an answering agent run: clear the running count (undefined).
35
+ * - Not in fallback, an inconclusive agent run: unchanged — a harness kill is not evidence the
36
36
  * primary recovered, so the running count survives it.
37
- * - In fallback, a fallback tick (`probe` false): unchanged — its outcome neither returns
37
+ * - In fallback, a fallback run (`probe` false): unchanged — its outcome neither returns
38
38
  * the role to primary nor re-trips it.
39
- * - In fallback, a probe tick: an inconclusive probe is unchanged (retried on the next tick);
39
+ * - In fallback, a probe run: an inconclusive probe is unchanged (retried on the next run);
40
40
  * an answering probe clears the episode; a provider-class failed probe doubles the cooldown
41
41
  * (capped) and schedules the next one.
42
42
  */
@@ -68,8 +68,8 @@ export function recordModelFallback(state, outcome) {
68
68
  }
69
69
  return { failures, since: 0, probeAt: 0, cooldownMs: 0, reason };
70
70
  }
71
- /** The reason label one run's provider evidence yields: "rate-limit" for a 429, else the
72
- * backend-failure kind. Exported so the loop's evidence assembly and tests share one
71
+ /** The reason label one agent run's provider evidence yields: "rate-limit" for a 429, else the
72
+ * backend-failure kind. Exported so the runner's evidence assembly and tests share one
73
73
  * spelling. */
74
74
  export function providerFailureReason(rateLimit, kind) {
75
75
  return rateLimit ? "rate-limit" : (kind ?? "backend failure");
@@ -0,0 +1,202 @@
1
+ import path from "node:path";
2
+ import { readTextIfExists, readTextOrNull, writeTextAtomic } from "../files/files.js";
3
+ import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
4
+ import { gitTry, gitTryStarted } from "../git/git-run.js";
5
+ import { resetWorktreeToMain } from "../git/worktree.js";
6
+ import { resolveBacklogInsertConflicts } from "../merge/backlog-conflicts.js";
7
+ import { conflictedFiles } from "../merge/merge-git.js";
8
+ import { shortSha } from "../text/format.js";
9
+ import { formatDate } from "../text/datetime.js";
10
+ import { stampedSubject } from "../git/commit-message.js";
11
+ import { baseRoleOf } from "../roles/runner-ids.js";
12
+ import { entryKey } from "../backlog/backlog-parts.js";
13
+ import { actionableEntryRanges } from "../backlog/backlog-structure.js";
14
+ import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
15
+ import { logEventBestEffort } from "../events/events.js";
16
+ /** The persistence count at which one repeated objection makes a rejection final
17
+ * (plans/revise-rejected.md; the repeat-finality policy in BUGS.md 2026-10-14): each numbered
18
+ * reason records how many consecutive revisions it has survived — a new (untagged) objection is
19
+ * 0, a `(prior N)` restatement is 1 plus the prior round's count for that objection — and when
20
+ * any reason reaches this count it was raised and then survived two revisions, so the change has
21
+ * stopped converging and the rejection is final. A converging chain keeps revising while each
22
+ * round's objections differ. */
23
+ export const REVISION_FINAL_ON_REPEAT = 2;
24
+ /** The hard backstop on one change's revision chain (plans/revise-rejected.md; BUGS.md
25
+ * 2026-10-14): at this many total rejections the rejection is final even when the objections
26
+ * never repeat, so a non-converging author cannot revise forever. Replaces the old fixed
27
+ * `REVISION_LIMIT = 2`, which ended a converging chain at its third rejection whenever each
28
+ * round raised a new objection. */
29
+ export const REVISION_BACKSTOP = 6;
30
+ /** The convergence rule (PLANS.md "Reviewer as a regular role, part 3/4"): when a rejection is
31
+ * final — the round cap or an objection that survived its revisions — the author could not
32
+ * converge, so the PLANS.md plan it was revising is held for the plan role instead of being left
33
+ * eligible for a fresh author pick that would restart the same cycle (plans/revise-rejected.md's
34
+ * motivation: the cap stops one cycle, the re-pick starts another). The hold is a
35
+ * **Needs review …** note under the entry's heading — the same needs-review hold every surface
36
+ * renders `[blocked: needs review]` and that the plan role resolves by splitting or replanning
37
+ * the plan (doctor-backlog-plans' checkStalledPlans names it) — whose text names the surviving
38
+ * objections verbatim (they are already in `RunnerState.lastReview.reasons`), so the plan role
39
+ * sees exactly what would not converge. PLANS.md plans only: no role clears a Needs-review note
40
+ * on a BUGS.md entry (`tumwater bug --release` keeps it, INV-AT1, and no role resolves one for
41
+ * a bug), so holding a bug would be a permanent dead-end; a bug that cannot converge stays with
42
+ * the existing re-pick behaviour (reviewer-feedback delivery, part 4/4, addresses that churn).
43
+ * Written through the primary worktree lock, as every other backlog write is, and then committed
44
+ * on main by commitOperatorBacklogEdit — the commit every other backlog writer makes — so the
45
+ * primary checkout stays clean for the next fast-forward and the hold reaches the worktrees
46
+ * that branch from main. Best-effort (write and commit each avoid throwing), so a non-converged
47
+ * but already-final rejection can never throw on the terminal-exhaustion path. No-op when the
48
+ * entry is gone (already moved or done), the role carries no claim, or the claim is a bug. */
49
+ export async function holdEntryAfterExhaustion(root, role, claim, reasons) {
50
+ if (claim?.file !== "PLANS.md")
51
+ return;
52
+ const note = `**Needs review ${formatDate(new Date())} by ${baseRoleOf(role)}: could not converge` +
53
+ (reasons.length === 0 ? "" : ` — ${reasons.join("; ")}`) +
54
+ `**`;
55
+ try {
56
+ withPrimaryWorktreeLock(root, () => {
57
+ const file = path.join(root, claim.file);
58
+ const md = readTextIfExists(file) ?? "";
59
+ const entry = actionableEntryRanges(md, "Planned").find((e) => entryKey(e.title) === claim.key);
60
+ if (entry === undefined)
61
+ return;
62
+ const lines = md.split("\n");
63
+ // The entry's 0-based line span is [start-1, end-1] inclusive (a non-last entry's range
64
+ // carries its trailing separator blank; the last entry's ends at its last content line).
65
+ // Find the last non-blank line inside that span, then rebuild the file with the note,
66
+ // one blank on each side, directly after the body and before whatever follows (the next
67
+ // heading, or EOF) — never inside the following entry.
68
+ let lastContent = entry.end - 1;
69
+ while (lastContent > entry.start - 1 && (lines[lastContent] ?? "").trim() === "")
70
+ lastContent--;
71
+ const rebuilt = [...lines.slice(0, lastContent + 1), "", note, "", ...lines.slice(entry.end)];
72
+ writeTextAtomic(file, rebuilt.join("\n"));
73
+ });
74
+ // Commit the hold on the base branch so the primary stays clean for ff-only and the note
75
+ // reaches the worktrees that branch from main — the same best-effort commit every other
76
+ // backlog writer makes (a primary not on the base branch just skips the commit).
77
+ await commitOperatorBacklogEdit(root, claim.file, stampedSubject("operator", `Hold converged plan: ${claim.key}`)); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
78
+ }
79
+ catch (err) {
80
+ logEventBestEffort(root, {
81
+ source: role,
82
+ type: "revision",
83
+ action: "hold-failed",
84
+ message: `could not hold the entry after a final rejection: ${String(err)}`,
85
+ });
86
+ }
87
+ }
88
+ /** The picks a stopped `cherry-pick` sequence still owes after the one it stopped on, oldest
89
+ * first — read from the sequencer's todo, whose first line is the stopped pick. null when the
90
+ * todo cannot be read, so the caller never settles a stop it cannot finish. */
91
+ async function picksAfterStop(wt) {
92
+ const todo = await gitTry(wt, "rev-parse", "--git-path", "sequencer/todo");
93
+ const text = todo === null ? null : readTextOrNull(path.resolve(wt, todo));
94
+ if (text === null)
95
+ return null;
96
+ const picks = text.split("\n").flatMap((line) => /^(?:p|pick)\s+(\S+)/.exec(line)?.[1] ?? []);
97
+ return picks.slice(1);
98
+ }
99
+ /** Cherry-pick `<base>..<sha>` onto `wt` as uncommitted edits, settling backlog conflicts
100
+ * without a model run on the way. A rejected or handed-back diff that prepends to PLANS-DONE.md
101
+ * or BUGS-FIXED.md conflicts with every entry main archived since, the same insert-only shape the
102
+ * merge's first pass settles (resolveBacklogInsertConflicts); left alone, its author got markers
103
+ * in a 1.9 MB file (feature-5, 2026-10-19). On a stop whose conflicted files all settle, the
104
+ * sequencer is quit (`--continue` refuses in `--no-commit` mode) and the picks it still owed are
105
+ * replayed the same way onto the settled index. */
106
+ async function pickSettlingBacklog(wt, base, sha) {
107
+ let picks = [`${base}..${sha}`];
108
+ for (;;) {
109
+ // diff3 carries the base section of every conflict, which the backlog settler reads; the
110
+ // markers an author is handed keep it too.
111
+ const picked = await gitTryStarted(wt, "-c", "merge.conflictStyle=diff3", "cherry-pick", "--no-commit", ...picks);
112
+ if (picked !== null)
113
+ return { status: "clean" };
114
+ // conflictedFiles, not a raw diff: it decodes git's C-quoted names, so a non-ASCII
115
+ // conflicted path is the on-disk name the prompt and hasConflictMarkers can use.
116
+ const conflicted = await conflictedFiles(wt);
117
+ if (conflicted.length === 0)
118
+ return { status: "failed" };
119
+ const rest = await picksAfterStop(wt);
120
+ // A settler that fails part-way (a failed `git add`) leaves every file to the author.
121
+ const remaining = rest === null ? conflicted : await resolveBacklogInsertConflicts(wt, conflicted).catch(() => conflicted);
122
+ await gitTry(wt, "cherry-pick", "--quit");
123
+ if (remaining.length > 0)
124
+ return { status: "conflicted", files: remaining };
125
+ if (rest === null || rest.length === 0)
126
+ return { status: "clean" };
127
+ picks = rest;
128
+ }
129
+ }
130
+ /** The exclusive start of the range to re-apply for `sha`: the merge-base with main, except that a
131
+ * commit already on main (a reverted change, plans/fast-merge.md part 4c) has merge-base `sha`
132
+ * itself and an empty range, so its own parent is the base and the range is that one commit. null
133
+ * (the caller drops the hand-back) only on git's own answer, never because git could not start. */
134
+ async function pickBase(wt, mainBranch, sha) {
135
+ const base = await gitTryStarted(wt, "merge-base", mainBranch, sha);
136
+ if (base === null)
137
+ return null;
138
+ const full = await gitTryStarted(wt, "rev-parse", sha);
139
+ if (full?.trim() !== base.trim())
140
+ return base;
141
+ // A commit main merely fast-forwarded onto keeps the empty range; only one main reverted is
142
+ // handed back to be re-applied.
143
+ const reverts = (await gitTryStarted(wt, "log", mainBranch, "--format=%s", "--fixed-strings", `--grep=Revert ${shortSha(sha)}`)) ?? "";
144
+ if (!reverts.split("\n").some((subject) => subject.endsWith(`Revert ${shortSha(sha)}`)))
145
+ return base;
146
+ return gitTryStarted(wt, "rev-parse", `${sha}^`);
147
+ }
148
+ /** Re-apply a rejected change `sha` onto current main as uncommitted edits in `wt`
149
+ * (plans/revise-rejected.md): the author's next run starts from the rejected diff instead of
150
+ * re-deriving it. `git cherry-pick --no-commit <merge-base>..<sha>` leaves the edits in the
151
+ * worktree without committing; a conflict confined to the backlog's insert-only shape is settled
152
+ * on the way (pickSettlingBacklog). On any other git failure — most often a conflict with what
153
+ * main merged since the rejection, but also a missing object — the cherry-pick is aborted and
154
+ * the worktree reset to main, and false is returned so the caller falls back to a plain
155
+ * rejection note. A git that could not start, or a stopped pick whose conflicts git cannot list,
156
+ * throws instead, keeping the revision and its ref for the next run. */
157
+ export async function applyRevision(wt, mainBranch, sha) {
158
+ const base = await pickBase(wt, mainBranch, sha);
159
+ if (base === null) {
160
+ await resetWorktreeToMain(wt, mainBranch);
161
+ return false;
162
+ }
163
+ if ((await pickSettlingBacklog(wt, base, sha)).status !== "clean") {
164
+ await gitTry(wt, "cherry-pick", "--abort");
165
+ await resetWorktreeToMain(wt, mainBranch);
166
+ return false;
167
+ }
168
+ return true;
169
+ }
170
+ /** Re-apply a handed-back change's diff onto current main with the conflict markers left in
171
+ * place as ordinary uncommitted edits, so its author can resolve them (PLANS.md "Robust
172
+ * conflict landing, part 2/2"). Like applyRevision it cherry-picks
173
+ * `<merge-base>..<sha> --no-commit` onto the worktree's current main, settling backlog
174
+ * insert-only conflicts on the way (pickSettlingBacklog), so only files that settler cannot
175
+ * settle keep markers. A conflicted pick is NOT aborted: its sequencer state is quit, a mixed
176
+ * `git reset` drops the index's unmerged entries so the marker-bearing files are plain
177
+ * working-tree edits the run can commit normally, and `{ applied: true, conflicted }` is
178
+ * returned. A failure with no unmerged paths (a missing object, a bad revision) aborts, resets
179
+ * to main, and returns `{ applied: false }` so the caller falls back to a fresh start. A git
180
+ * that could not start, or a stopped pick whose conflicts git cannot list, throws instead, so the
181
+ * run errors with the hand-back and its ref kept for the next. */
182
+ export async function applyWithConflicts(wt, mainBranch, sha) {
183
+ const base = await pickBase(wt, mainBranch, sha);
184
+ if (base === null) {
185
+ await resetWorktreeToMain(wt, mainBranch);
186
+ return { applied: false, conflicted: [] };
187
+ }
188
+ const picked = await pickSettlingBacklog(wt, base, sha);
189
+ if (picked.status === "conflicted") {
190
+ // Keep the marker-bearing files, drop only the index's unmerged stage entries: a mixed
191
+ // reset leaves the working tree untouched, so the markers are uncommitted edits the run
192
+ // stages and commits like any other author work.
193
+ await gitTry(wt, "reset");
194
+ return { applied: true, conflicted: picked.files };
195
+ }
196
+ if (picked.status === "failed") {
197
+ await gitTry(wt, "cherry-pick", "--abort");
198
+ await resetWorktreeToMain(wt, mainBranch);
199
+ return { applied: false, conflicted: [] };
200
+ }
201
+ return { applied: true, conflicted: [] };
202
+ }
@@ -1,15 +1,15 @@
1
1
  import { logEventBestEffort } from "../events/events.js";
2
2
  import { modelFallbackVerdict, providerFailureReason, recordModelFallback } from "./model-fallback.js";
3
- /** Fold one AUTHORING run's verdict into the role's model-fallback state
4
- * (src/loop/model-fallback.ts) and emit the episode's started/ended events. Only the run
5
- * that actually happened is evidence: a tick that never invoked pi (a skipped director
6
- * inbox, a recovered leftover, a red-main block) returns from runTick before this is
7
- * reached, and modelFallbackVerdict reports an aborted/killed/timed-out run as
3
+ /** Fold one AUTHORING agent run's verdict into the role's model-fallback state
4
+ * (src/runner/model-fallback.ts) and emit the episode's started/ended events. Only the agent run
5
+ * that actually happened is evidence: a run that never invoked pi (a skipped director
6
+ * inbox, a recovered leftover, a red-main block) returns from runOnce before this is
7
+ * reached, and modelFallbackVerdict reports an aborted/killed/timed-out agent run as
8
8
  * inconclusive — so neither can end an episode without the primary having answered.
9
9
  * A role whose tier resolves to no fallback pair never trips, and an episode persisted from a
10
10
  * config that HAD a pair is cleared when a live edit removes it (or sets it to "pause"), so a
11
11
  * stale "on fallback" mark cannot outlive the config that created it. Persists on any state
12
- * change; the tick's own finalize save carries it too. */
12
+ * change; the run's own finalize save carries it too. */
13
13
  export function foldModelFallbackPhase(ctx, fallbackCtx, pi) {
14
14
  const s = ctx.state;
15
15
  const prior = s.modelFallback;
@@ -19,8 +19,8 @@ export function foldModelFallbackPhase(ctx, fallbackCtx, pi) {
19
19
  const wasIn = prior !== undefined && prior.since > 0;
20
20
  if (fallbackCtx.fallback === null) {
21
21
  // No fallback pair resolves any more: the feature is off for this role. End a persisted
22
- // episode (the primary just ran, so this tick IS the return), and drop any pre-trip count
23
- // so a later config starts from zero. A killed/aborted/timed-out run is no verdict, so it
22
+ // episode (the primary just ran, so this run IS the return), and drop any pre-trip count
23
+ // so a later config starts from zero. A killed/aborted/timed-out agent run is no verdict, so it
24
24
  // leaves the episode in place exactly as it does on a configured fallback.
25
25
  if (prior === undefined)
26
26
  return;
@@ -0,0 +1,107 @@
1
+ /** The post-run outcome half of a role run, split out of src/runner/runner-run.ts: after the
2
+ * authoring pi run finishes, everything that turns the result into an outcome — the verdict
3
+ * classification (abort, config request, quiet kill, timeout, refusal, failure, no-change)
4
+ * through resolveRunVerdict, and the fulfillable path's staging of the work for the merge
5
+ * queue — plus the refusal-note merge path. The run-lifecycle phases that lead up to the pi
6
+ * run live in runner-run.ts; this module keeps only the outcome's own dependencies, and
7
+ * shares runner-run.ts's `RunnerRunContext` binder — the one contract between the two halves —
8
+ * so either half can change without touching the other's imports. */
9
+ import { mergeToMain } from "../merge/merge-to-main.js";
10
+ import { MERGE_LOCK_WAIT_MS } from "../paths.js";
11
+ import { resolveRunVerdict } from "../run/run-verdict.js";
12
+ import { modelTrailerLine } from "../git/commit-message.js";
13
+ import { seamAgent } from "../config/model-selector.js";
14
+ import { stageRunMerge } from "../run/run-stage.js";
15
+ import { stageCheckFindings } from "../run/stage-check.js";
16
+ /** Merge the worktree branch on main (see src/merge/merge-to-main.ts for the
17
+ * rebase → verify → ff-merge → conflict-retry flow): delegates with the runner identity, run
18
+ * number, and shared pi wiring so a conflict-resolution run folds into this run's counters
19
+ * like any other pi run. Only the refusal-note merge (src/verdict/refusal.ts) still uses it —
20
+ * reviewed changes merge through the merge pipeline's leased verification slot instead; md-only
21
+ * notes are review-exempt by construction, so they keep this branch path (plans/merge-queue.md
22
+ * 2/5). The refusal path waits MERGE_LOCK_WAIT_MS for merge.lock, not withLock's default: a
23
+ * merge holds it through its in-lock check, which outlasts the default. */
24
+ async function mergePhase(ctx, wt, summary) {
25
+ return mergeToMain({
26
+ root: ctx.root,
27
+ role: ctx.role,
28
+ mainBranch: ctx.mainBranch,
29
+ exemptPaths: ctx.config.review.exemptPaths,
30
+ config: ctx.config,
31
+ runNumber: ctx.state.runs,
32
+ lockTimeoutMs: MERGE_LOCK_WAIT_MS,
33
+ runPi: (w, prompt, sessionName, config) => ctx.pi.runRolePi(w, prompt, sessionName, false, config),
34
+ }, wt, summary);
35
+ }
36
+ /** Turn a finished pi run into its RunOutcome: the post-run half of runOnce, split off so each
37
+ * half reads on its own screen — the setup above ends at the pi return. The verdict
38
+ * classification (abort, config request, quiet kill, timeout, refusal, failure, no-change) lives
39
+ * in resolveRunVerdict (src/run/run-verdict.ts), and the fulfillable path — staging —
40
+ * stays here. `userPrompt` is the raw director prompt this run is executing (null for role
41
+ * runners) so unfulfilled outcomes can re-queue it; `revisionRound` is the revision this run holds
42
+ * (undefined when it is not a revision); `flow` is the qa observer's FLOW line (null for every
43
+ * other role); `filed` is the feedback observer's FILED line (null for every other role). */
44
+ export async function handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt, flow, filed, piStartedAt, cfg) {
45
+ // Everything that decides the run left nothing mergeable — abort, config request,
46
+ // quiet kill, timeout, refusal, failure without changes, no change — lives in
47
+ // resolveRunVerdict (src/run/run-verdict.ts); null means the run IS fulfillable.
48
+ const modelLine = modelTrailerLine(seamAgent(cfg), cfg.provider, cfg.model);
49
+ const verdict = await resolveRunVerdict({
50
+ modelLine,
51
+ root: ctx.root,
52
+ role: ctx.role,
53
+ mainBranch: ctx.mainBranch,
54
+ state: ctx.state,
55
+ pending: ctx.pending,
56
+ turns: ctx.usage.turns,
57
+ userPrompt,
58
+ revisionRound,
59
+ wt,
60
+ pi,
61
+ flow,
62
+ filed,
63
+ // The image-reroute arm resolves its target from the live config and the current
64
+ // regime, and names the model this run ran on as the handoff's `from`.
65
+ config: ctx.config,
66
+ rerouteRegime: ctx.rerouteRegime(piStartedAt),
67
+ runModel: cfg.model,
68
+ warn: (message) => ctx.warn(message),
69
+ merge: (w, sum) => mergePhase(ctx, w, sum),
70
+ finishAbortedRun: () => ctx.finishAbortedRun(userPrompt, wt),
71
+ });
72
+ if (verdict)
73
+ return verdict;
74
+ // The commit is pinned and the worktree is free (src/run/run-stage.ts): stage it for the
75
+ // merge queue and END the run — the orchestrator drains the queue on its single merge
76
+ // slot, outside the author semaphore, so this slot is free the moment the work is
77
+ // committed (plans/merge-queue.md 3/5). Staging derives the commit message from the reply's
78
+ // SUMMARY block (one bounded follow-up turn when the run left none), flags high-friction
79
+ // runs, pins the commit by the role's merge ref BEFORE freeing the worktree (invariant
80
+ // 4 — a failed pin defers to next-run recovery), and enqueues the merge, which runs the
81
+ // same gate + merge flow through runMergePi/foldMergeUsage on this same state object
82
+ // — recording the commit count, the outcome, and the reviewer spend into it — and logs
83
+ // merge_done/merge_failed; a non-terminal outcome keeps the pin for next-run leftover recovery.
84
+ // `commits` was NOT incremented above: it counts merged changes only.
85
+ return stageRunMerge({
86
+ modelLine,
87
+ root: ctx.root,
88
+ role: ctx.role,
89
+ state: ctx.state,
90
+ config: ctx.config,
91
+ runTurns: ctx.usage.turns,
92
+ userPrompt,
93
+ revisionRound,
94
+ mainBranch: ctx.mainBranch,
95
+ wt,
96
+ finalText: pi.finalText,
97
+ flow,
98
+ filed,
99
+ piStartedAt,
100
+ warn: (message) => ctx.warn(message),
101
+ requestSummary: (w, n) => ctx.pi.requestSummary(w, cfg, n),
102
+ stageCheck: (w) => stageCheckFindings(w, ctx.mainBranch, ctx.config.review.exemptPaths, ctx.role),
103
+ requestStageFix: (w, findings, change) => ctx.pi.requestStageFix(w, findings, change, cfg),
104
+ pinAndReset: (w, sha) => ctx.pinAndReset(w, sha),
105
+ finishAbortedRun: () => ctx.finishAbortedRun(userPrompt, wt),
106
+ });
107
+ }