tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { imageReroute } from "../agent/image-input.js";
1
2
  import { DIRECTOR_ROLE } from "../roles/roles.js";
2
3
  import { baseRoleOf } from "../roles/runner-ids.js";
3
4
  import { isDirty, deleteRef } from "../git/git.js";
@@ -9,6 +10,7 @@ import { errorMessage } from "../text/text.js";
9
10
  import { recordParity } from "./ci-parity.js";
10
11
  import { resetWorktreeToMain } from "../git/worktree.js";
11
12
  import { recordDismissedFiling, recordDismissedFlow } from "./run-dismiss.js";
13
+ import { readPiListedModels } from "../pi/pi-listed-models.js";
12
14
  import { rejectedRefName } from "../paths.js";
13
15
  import { logEventBestEffort } from "../events/events.js";
14
16
  import { subscriptionWindowRejected } from "../agent/claude/claude-account.js";
@@ -23,6 +25,17 @@ function abortWithResume(s, ctx, pi, lastError, result, resumeCause) {
23
25
  ctx.pending.requeueForResume(s, ctx.userPrompt);
24
26
  return { result, resumeCause };
25
27
  }
28
+ /** End a run as an unfulfilled error: record `lastError`, re-queue the raw prompt for a later
29
+ * fresh run (requeueUnfulfilled) and return the error outcome — the terminal the timeout and
30
+ * image-blind arms share, filed here so the "record lastError, re-queue unfulfilled, end as
31
+ * error" contract cannot drift between them (the resume-arming twin is abortWithResume above;
32
+ * failedRunArm's error path keeps its own split because it arms a resume for a dirty worktree
33
+ * and sets lastError before that choice). */
34
+ function unfulfilledError(s, ctx, message) {
35
+ s.lastError = message;
36
+ ctx.pending.requeueUnfulfilled(ctx.userPrompt);
37
+ return { result: "error" };
38
+ }
26
39
  /** Classify what the finished run left behind; null means the run is fulfillable and staging
27
40
  * should take over. Never throws: every branch returns an outcome, recording what went wrong
28
41
  * in `state.lastError` or a warning event. */
@@ -134,11 +147,9 @@ function progressingTimeoutArm(ctx) {
134
147
  function timeoutArm(ctx) {
135
148
  const { pi, state: s } = ctx;
136
149
  if (pi.timedOut) {
137
- s.lastError = pi.errorMessage ?? "timed out";
138
150
  // The request never ran to completion and no work merged: put it back so the next
139
151
  // run retries it. (A killed run's half-done edits are discarded by the reset.)
140
- ctx.pending.requeueUnfulfilled(ctx.userPrompt);
141
- return { result: "error" };
152
+ return unfulfilledError(s, ctx, pi.errorMessage ?? "timed out");
142
153
  }
143
154
  return undefined;
144
155
  }
@@ -178,22 +189,73 @@ function budgetCappedArm(ctx) {
178
189
  }
179
190
  return undefined;
180
191
  }
192
+ /** The failure message a run that ended on a blind read names when the agent gave none of its
193
+ * own: the image path the model could not see, unless the kill carried a more specific reason
194
+ * (pi.errorMessage). Shared by the two failing arms of imageBlindArm below — the one-hop-already
195
+ * branch and the no-reroute-target branch — so the "could not read the image" wording cannot
196
+ * drift between them. The reroute arm does NOT use it: its own lastError names the target's
197
+ * tier too. */
198
+ function blindReadFailureMessage(pi, path) {
199
+ return pi.errorMessage ?? `read ${path}, but the model cannot read images`;
200
+ }
181
201
  /** The image-blind decision: a run stopped the moment its agent read an image its model cannot
182
- * see fails with `lastError` and the prompt re-queues. The peer-tier reroute was subtracted
183
- * (part 5g/6), so a blind read is never handed to another tier (plans/image-input.md "Failing
184
- * the run"). */
202
+ * see. One hop to an image-capable reroute target is taken when one exists; otherwise the
203
+ * run fails and the prompt re-queues (plans/image-input.md "Failing the run"). */
185
204
  function imageBlindArm(ctx) {
186
205
  const { pi, state: s } = ctx;
187
206
  // A run the runner killed the moment its agent read an image its model cannot see
188
207
  // (plans/image-input.md "Failing the run"): pi strips such an image from the request, so
189
208
  // the agent was about to answer about a picture it never saw. The run was deliberately
190
209
  // stopped before it could work on, so its half-done edits are discarded exactly as a
191
- // timed-out run's are — nothing to resume, and the prompt re-queues to run fresh later.
192
- // The error backoff and streak apply as for any error.
210
+ // timed-out run's are — nothing to resume, and the prompt re-queues to run fresh on a
211
+ // model that CAN see, with one reroute hop to an image-capable target (the arm below). The
212
+ // error backoff and streak apply as for any error.
193
213
  if (pi.imageBlind) {
194
- s.lastError = pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`;
195
- ctx.pending.requeueUnfulfilled(ctx.userPrompt);
196
- return { result: "error" };
214
+ // One hop only (plans/image-input.md "Rerunning on a model that can read images"): a
215
+ // run that ALREADY started on the image-capable target and still read blind is not
216
+ // rerouted a second time — it fails, so a misreadable image cannot bounce the role
217
+ // between tiers forever. Set at run start by runPhase when it routes a run onto the
218
+ // target (blind-read handoff or attachment preflight).
219
+ if (s.imageRerouteActive) {
220
+ // No work was done (the run was killed at the first read): nothing to resume, and the
221
+ // prompt re-queues to run fresh later like any other error. The error backoff/streak
222
+ // apply as usual.
223
+ return unfulfilledError(s, ctx, blindReadFailureMessage(pi, pi.imageBlind.path));
224
+ }
225
+ // Otherwise, when the role's tier has an image-capable reroute target, the failed work
226
+ // is not discarded: the next run starts directly on that target over the kept worktree.
227
+ // The one-shot field carries the target seam plus the `from`/`path` facts the handoff
228
+ // note needs; the run ends `aborted` with the `image-reroute` resume cause, and
229
+ // applyRunOutcome schedules it promptly (nextRunAt = now) while planRunStart hands the
230
+ // kept worktree off to a FRESH session on the target.
231
+ const regime = ctx.rerouteRegime ?? "primary";
232
+ const target = ctx.config !== undefined ? imageReroute(ctx.config, ctx.role, regime, undefined, null, null, readPiListedModels(ctx.root))
233
+ : null;
234
+ if (target) {
235
+ const from = ctx.runModel ?? target.config.model ?? "the model";
236
+ s.imageReroute = {
237
+ tier: target.tier,
238
+ backend: target.config.agent ?? "pi",
239
+ provider: target.config.provider,
240
+ model: target.config.model,
241
+ thinking: target.config.thinking,
242
+ from,
243
+ path: pi.imageBlind.path,
244
+ };
245
+ s.lastError = `read ${pi.imageBlind.path}, but ${from} cannot read images — rerouting to ${target.tier}`;
246
+ ctx.pending.requeueForResume(s, ctx.userPrompt);
247
+ logEventBestEffort(ctx.root, {
248
+ source: ctx.role,
249
+ type: "image_reroute",
250
+ run: s.runs,
251
+ from,
252
+ to: target.config.model,
253
+ tier: target.tier,
254
+ preflight: false,
255
+ });
256
+ return { result: "aborted", resumeCause: "image-reroute" };
257
+ }
258
+ return unfulfilledError(s, ctx, blindReadFailureMessage(pi, pi.imageBlind.path));
197
259
  }
198
260
  return undefined;
199
261
  }
@@ -300,51 +362,67 @@ function noChangeArm(ctx, changed) {
300
362
  // diagnoseNoChange (src/verdict/no-change.ts) tells which, so the warning event below is
301
363
  // diagnosable on its own.
302
364
  const diagnosis = diagnoseNoChange(pi);
303
- if (!pi.nothingToDo) {
304
- ctx.warn(`the agent finished without changes and without declaring nothing-to-do` +
305
- (diagnosis.notes.length ? ` (${diagnosis.notes.join(", ")})` : ""));
306
- }
365
+ warnUndeclaredNoChange(ctx, pi, diagnosis.notes);
307
366
  // A cut-off run did real work and was NOT fulfilled: a director prompt goes back
308
367
  // to the inbox to rerun fresh; a role runner resumes the just-compacted session
309
368
  // next run (see the cutOff handling in run()).
310
369
  if (diagnosis.cutOff)
311
370
  ctx.pending.requeueForResume(s, ctx.userPrompt);
312
- // A cut-off run did real work but was truncated before declaring its outcome: the FLOW
313
- // line it left mid-stream is not a finished verdict, so recording it would advance the
314
- // rotation past a check that did not complete. Only a run that was not cut off records.
315
- if (ctx.flow && !diagnosis.cutOff) {
316
- // The ledger only orders the qa menu, so a failed write warns rather than failing the run.
317
- recordDismissedFlow(ctx, ctx.root, ctx.flow.flow, ctx.flow.result, ctx.flow.result === "bug" ? extractSummary(pi.finalText) ?? undefined : undefined);
318
- }
319
- // A fulfilled feedback run that filed records its per-install ledger entry and logs the
320
- // issue_filed event so the filing is visible in the operator's activity feed. Recording is
321
- // bookkeeping that never fails the run: a failed ledger write warns and the no_change
322
- // outcome stands. A cut-off run records nothing — truncation is not a filing the run
323
- // declared (the same guard the FLOW line carries).
324
- if (ctx.filed && !diagnosis.cutOff) {
325
- recordDismissedFiling(ctx, ctx.root, ctx.filed);
371
+ recordNoChangeBookkeeping(ctx, pi, diagnosis.cutOff);
372
+ return {
373
+ result: "no_change",
374
+ cutOff: diagnosis.cutOff || undefined,
375
+ summary: noChangeSummary(ctx, pi, diagnosis.cutOff),
376
+ };
377
+ }
378
+ return undefined;
379
+ }
380
+ /** Warn when a no-change run declared neither a sentinel nor nothing-to-do, so the non-
381
+ * compliance or truncation the diagnosis saw is diagnosable on its own. */
382
+ function warnUndeclaredNoChange(ctx, pi, notes) {
383
+ if (!pi.nothingToDo) {
384
+ ctx.warn(`the agent finished without changes and without declaring nothing-to-do` +
385
+ (notes.length ? ` (${notes.join(", ")})` : ""));
386
+ }
387
+ }
388
+ /** A no-change run's bookkeeping: the qa flow, the filed feedback ledger entry and the ci
389
+ * parity coverage, each recorded only when its observer saw it and the run was not cut off. */
390
+ function recordNoChangeBookkeeping(ctx, pi, cutOff) {
391
+ // A cut-off run did real work but was truncated before declaring its outcome: the FLOW
392
+ // line it left mid-stream is not a finished verdict, so recording it would advance the
393
+ // rotation past a check that did not complete. Only a run that was not cut off records.
394
+ if (ctx.flow && !cutOff) {
395
+ // The ledger only orders the qa menu, so a failed write warns rather than failing the run.
396
+ recordDismissedFlow(ctx, ctx.root, ctx.flow.flow, ctx.flow.result, ctx.flow.result === "bug" ? extractSummary(pi.finalText) ?? undefined : undefined);
397
+ }
398
+ // A fulfilled feedback run that filed records its per-install ledger entry and logs the
399
+ // issue_filed event so the filing is visible in the operator's activity feed. Recording is
400
+ // bookkeeping that never fails the run: a failed ledger write warns and the no_change
401
+ // outcome stands. A cut-off run records nothing — truncation is not a filing the run
402
+ // declared (the same guard the FLOW line carries).
403
+ if (ctx.filed && !cutOff) {
404
+ recordDismissedFiling(ctx, ctx.root, ctx.filed);
405
+ }
406
+ // A fulfilled ci run that declared its coverage records it in the parity ledger so the
407
+ // next fresh session's block knows what the last run covered (part 3/3). The ledger is
408
+ // bookkeeping that never fails the run: a failed write warns and the no_change outcome
409
+ // stands. A cut-off run records nothing — truncation is not a coverage the run declared.
410
+ if (ctx.ci && !cutOff) {
411
+ try {
412
+ recordParity(ctx.root, ctx.ci.sha, ctx.ci.result);
326
413
  }
327
- // A fulfilled ci run that declared its coverage records it in the parity ledger so the
328
- // next fresh session's block knows what the last run covered (part 3/3). The ledger is
329
- // bookkeeping that never fails the run: a failed write warns and the no_change outcome
330
- // stands. A cut-off run records nothing — truncation is not a coverage the run declared.
331
- if (ctx.ci && !diagnosis.cutOff) {
332
- try {
333
- recordParity(ctx.root, ctx.ci.sha, ctx.ci.result);
334
- }
335
- catch (err) {
336
- ctx.warn(`ci parity not recorded: ${errorMessage(err)}`);
337
- }
414
+ catch (err) {
415
+ ctx.warn(`ci parity not recorded: ${errorMessage(err)}`);
338
416
  }
339
- // A prompt-driven (director) run that answers a question in prose ends `no_change` with
340
- // no SUMMARY block, so its reply would be lost — the dashboard reads a bare "No change"
341
- // and the feed carries nothing (BUGS.md 2026-10-14). Keep the prose above the sentinel as
342
- // the run's summary so `lastSummary` and the run_end event show the answer where the
343
- // operator asked the question. A role runner's bare nothing-to-do stays summary-less.
344
- const summary = ctx.userPrompt !== null && !diagnosis.cutOff
345
- ? (extractNoChangeAnswer(pi.finalText) ?? undefined)
346
- : undefined;
347
- return { result: "no_change", cutOff: diagnosis.cutOff || undefined, summary };
348
417
  }
349
- return undefined;
418
+ }
419
+ /** A prompt-driven (director) run that answers a question in prose ends `no_change` with
420
+ * no SUMMARY block, so its reply would be lost — the dashboard reads a bare "No change"
421
+ * and the feed carries nothing (BUGS.md 2026-10-14). Keep the prose above the sentinel as
422
+ * the run's summary so `lastSummary` and the run_end event show the answer where the
423
+ * operator asked the question. A role runner's bare nothing-to-do stays summary-less. */
424
+ function noChangeSummary(ctx, pi, cutOff) {
425
+ return ctx.userPrompt !== null && !cutOff
426
+ ? (extractNoChangeAnswer(pi.finalText) ?? undefined)
427
+ : undefined;
350
428
  }
@@ -1,15 +1,14 @@
1
- /** The merge gate's deterministic checks, run BEFORE a changed run commits.
1
+ /** The merge gate's checks that still refuse a change, plus staging-only heuristics for what a
2
+ * reviewer tends to reject, run BEFORE a changed run commits.
2
3
  *
3
- * The gate runs `backlogStructureReason` and `falseFixReason` at merge time, by which point
4
- * the author's session is over: a finding costs a queue slot, a verification, and a rejection that
5
- * discards the work (13 phantom-fix rejections between 2026-09-23 and 2026-10-02). Both checks
6
- * read the worktree and the change's merge-base, so the same questions can be asked at staging
7
- * time, while the authoring session can still be continued for one fix-up turn. This module
8
- * runs them over the run's uncommitted edits; the gate still has the final say.
9
- *
10
- * Scope mirrors the gate exactly: `backlogStructureReason` runs on every changed run, while
11
- * `falseFixReason` runs only for an exempt (doc-only) diff — the condition under which the gate
12
- * consults it.
4
+ * Since part 3m/3 the gate refuses only a brief-block edit, a protected edit and a leaked
5
+ * credential — the findings a later commit cannot repair. This module runs those same checks
6
+ * over the run's uncommitted edits while the authoring session can still be continued for one
7
+ * fix-up turn (a finding that reaches merge costs a queue slot, a verification, and a rejection
8
+ * that discards the work — 13 phantom-fix rejections between 2026-09-23 and 2026-10-02 priced
9
+ * that turn). The gate no longer refuses a false fix claim or a broken backlog structure —
10
+ * both are repairable by a later commit and reported by the main watch on main's tip — so
11
+ * those findings never spend the run's fix-up turn here.
13
12
  *
14
13
  * The git-level checks described next live across stage-path-checks.ts (`gitLevelFindings`),
15
14
  * stage-name-checks.ts and stage-prose-checks.ts, which this module appends.
@@ -20,8 +19,9 @@
20
19
  * files whose final newline the change removed. These stage the change (`git add -A`) to read
21
20
  * the index with git only, then restore the index to HEAD before returning: staging is a read
22
21
  * aid for the checks, not a change to the run's state. A git hiccup yields no finding rather
23
- * than failing the run. Unlike the backlog checks above, the gate does not run these: they are
24
- * heuristics for what a reviewer tends to reject, so neither path check looks inside record
22
+ * than failing the run. Unlike the gate's checks this module runs (which the gate also runs at
23
+ * merge time), the gate does not run these: they are heuristics for what a reviewer tends to
24
+ * reject, so neither path check looks inside record
25
25
  * prose (isRecordProse), where an old or not-yet-created path is correct as written.
26
26
  *
27
27
  * A fourth check, `staleNameFindings`, reports a compound identifier the change removed from a
@@ -59,11 +59,8 @@
59
59
  import path from "node:path";
60
60
  import { changedFiles } from "../git/git-diff.js";
61
61
  import { conflictMarkerReason } from "../merge/merge-git.js";
62
- import { isExemptDiff } from "../review/exemptions.js";
63
- import { backlogStructureReason } from "../backlog/backlog-structure.js";
64
62
  import { protectedEditReason } from "../review/protected-edits.js";
65
63
  import { secretLeakReason } from "../review/secret-leaks.js";
66
- import { falseFixReason } from "../verdict/fix-claim.js";
67
64
  import { changeBaseRev, fileContentAt } from "../git/git.js";
68
65
  import { readTextOrNull } from "../files/files.js";
69
66
  import { FINISHED_ARCHIVES, finishedArchiveOf, parseEntryDetails, } from "../backlog/backlog-md.js";
@@ -93,35 +90,24 @@ const pushReason = async (findings, check) => {
93
90
  if (reason)
94
91
  findings.push(reason);
95
92
  };
96
- /** The findings likely to get this uncommitted change rejected at merge, or an empty list when
97
- * it is clean: the merge gate's own deterministic checks plus the staging-only heuristics (plan
98
- * dependencies, path drift). Checks the worktree at `wt` against
99
- * `mainBranch`'s merge-base; `exemptPaths` are the config's review-exemption globs that scope
100
- * the false-fix check.
101
- *
102
- * Never throws: a check that fails yields no finding, so a git hiccup at staging time defers to
103
- * the gate instead of failing the run. */
104
- export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
105
- const files = await changedFiles(wt).catch(() => null);
106
- if (files === null || files.length === 0)
107
- return [];
108
- const findings = [];
109
- await pushReason(findings, backlogStructureReason(wt, mainBranch, files));
110
- if (files.includes("PLANS.md")) {
111
- const { head, base } = await headAndBase(wt, mainBranch, "PLANS.md");
112
- // Finished plans are archived in PLANS-DONE.md (plans/backlog-archive.md): a requires clause
113
- // naming a merged part resolves there, on both sides of the comparison alike.
114
- const archived = parseEntryDetails(readTextOrNull(path.join(wt, PLANS_ARCHIVE)) ?? "", "Done").map((e) => e.title);
115
- const dependency = base === undefined ? undefined : planDependencyFinding(head, base, archived);
116
- if (dependency !== undefined)
117
- findings.push(dependency);
118
- const refusal = base === undefined ? undefined : ungroundedRefusalFinding("PLANS.md", head, base);
119
- if (refusal !== undefined)
120
- findings.push(refusal);
121
- }
93
+ /** The PLANS.md record findings: a requires clause naming a part the archive lacks, and an
94
+ * ungrounded refusal that cites another part. */
95
+ async function plansFindings(wt, mainBranch) {
96
+ const { head, base } = await headAndBase(wt, mainBranch, "PLANS.md");
97
+ // Finished plans are archived in PLANS-DONE.md (plans/backlog-archive.md): a requires clause
98
+ // naming a merged part resolves there, on both sides of the comparison alike.
99
+ const archived = parseEntryDetails(readTextOrNull(path.join(wt, PLANS_ARCHIVE)) ?? "", "Done").map((e) => e.title);
100
+ const dependency = base === undefined ? undefined : planDependencyFinding(head, base, archived);
101
+ const refusal = base === undefined ? undefined : ungroundedRefusalFinding("PLANS.md", head, base);
102
+ return [dependency, refusal].filter((f) => f !== undefined);
103
+ }
104
+ /** The finished-archive findings: duplicates of entries the archive already holds, plus the
105
+ * acceptance greps of the plan entries the change finished (for a PLANS.md change). */
106
+ async function archiveFindings(wt, mainBranch, files) {
122
107
  const baseRev = FINISHED_ARCHIVES.some((s) => files.includes(s.file) || files.includes(s.archive))
123
108
  ? await changeBaseRev(wt, mainBranch).catch(() => undefined)
124
109
  : undefined;
110
+ const findings = [];
125
111
  for (const spec of FINISHED_ARCHIVES) {
126
112
  if (baseRev === undefined || !(files.includes(spec.file) || files.includes(spec.archive)))
127
113
  continue;
@@ -144,12 +130,18 @@ export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
144
130
  })().catch(() => []);
145
131
  findings.push(...greps);
146
132
  }
147
- if (files.includes("BUGS.md")) {
148
- const { head, base } = await headAndBase(wt, mainBranch, "BUGS.md");
149
- const refusal = base === undefined ? undefined : ungroundedRefusalFinding("BUGS.md", head, base);
150
- if (refusal !== undefined)
151
- findings.push(refusal);
152
- }
133
+ return findings;
134
+ }
135
+ /** The BUGS.md record finding: an ungrounded refusal that cites another bug. */
136
+ async function bugsFindings(wt, mainBranch) {
137
+ const { head, base } = await headAndBase(wt, mainBranch, "BUGS.md");
138
+ const refusal = base === undefined ? undefined : ungroundedRefusalFinding("BUGS.md", head, base);
139
+ return refusal === undefined ? [] : [refusal];
140
+ }
141
+ /** The stray-placeholder findings: a `_None yet._` line a changed record still leaves in a
142
+ * section that holds entries. */
143
+ async function placeholderFindings(wt, mainBranch, files) {
144
+ const findings = [];
153
145
  for (const file of PLACEHOLDER_FILES) {
154
146
  if (!files.includes(file))
155
147
  continue;
@@ -158,10 +150,28 @@ export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
158
150
  if (stray !== undefined)
159
151
  findings.push(stray);
160
152
  }
153
+ return findings;
154
+ }
155
+ /** The findings likely to get this uncommitted change rejected at merge, or an empty list when
156
+ * it is clean: the merge gate's remaining checks over the change plus the staging-only
157
+ * heuristics (plan dependencies, path drift). Checks the worktree at `wt` against
158
+ * `mainBranch`'s merge-base.
159
+ *
160
+ * Never throws: a check that fails yields no finding, so a git hiccup at staging time defers to
161
+ * the gate instead of failing the run. */
162
+ export async function stageCheckFindings(wt, mainBranch, role) {
163
+ const files = await changedFiles(wt).catch(() => null);
164
+ if (files === null || files.length === 0)
165
+ return [];
166
+ const findings = [];
167
+ if (files.includes("PLANS.md"))
168
+ findings.push(...(await plansFindings(wt, mainBranch)));
169
+ findings.push(...(await archiveFindings(wt, mainBranch, files)));
170
+ if (files.includes("BUGS.md"))
171
+ findings.push(...(await bugsFindings(wt, mainBranch)));
172
+ findings.push(...(await placeholderFindings(wt, mainBranch, files)));
161
173
  await pushReason(findings, protectedEditReason(wt, mainBranch, files, role));
162
174
  await pushReason(findings, secretLeakReason(wt, mainBranch, files));
163
- if (isExemptDiff(files, exemptPaths))
164
- await pushReason(findings, falseFixReason(wt, mainBranch, files));
165
175
  findings.push(...(await gitLevelFindings(wt, mainBranch).catch(() => [])));
166
176
  const markers = conflictMarkerReason(wt, files);
167
177
  if (markers !== undefined)
@@ -16,6 +16,34 @@ export const MAX_STALE_PATHS = 10;
16
16
  export function diffPlusPath(line) {
17
17
  return line.slice(4).replace(/^b\//, "");
18
18
  }
19
+ /** Parse a `-U0` unified diff into each file's removed and added content lines, marker-stripped.
20
+ * The `diff --git` header names the file, and git's `---`/`+++` old/new path headers carry no
21
+ * content; `@@` hunk headers, context lines and the `\ No newline` marker match neither branch,
22
+ * so the walk records only real `-`/`+` content lines. Consumers keep only what they want — a
23
+ * JS-file filter, a trim-and-min-length rule, a compound-name extraction — from the per-file
24
+ * removed/added lists; stage-path-checks's movedSourceFindings and stage-name-checks's
25
+ * declaredNameDeltas each walked this same grammar before the helper. stage-prose-checks's
26
+ * detachedDocFindings and addedDiffLines are deliberately not folded here: both need each hunk's
27
+ * start-line count from the `@@` header, which this shape drops. */
28
+ export function diffRemovedAdded(diff) {
29
+ const files = [];
30
+ let cur;
31
+ for (const line of diff.split("\n")) {
32
+ const header = /^diff --git a\/.* b\/(.*)$/.exec(line);
33
+ if (header) {
34
+ cur = { path: header[1], removed: [], added: [] };
35
+ files.push(cur);
36
+ continue;
37
+ }
38
+ if (!cur)
39
+ continue;
40
+ if (line.startsWith("-") && !line.startsWith("---"))
41
+ cur.removed.push(line.slice(1));
42
+ else if (line.startsWith("+") && !line.startsWith("+++"))
43
+ cur.added.push(line.slice(1));
44
+ }
45
+ return files;
46
+ }
19
47
  /** Parse `git grep -n` stage output into `file:line:text` hits. `git grep -n` prints one
20
48
  * `path:line:text` per match, and the four name/path checks (stage-name-checks.ts's
21
49
  * staleNameFindings and movedNameFindings, stage-path-checks.ts's movedSourceFindings and