tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -6,10 +6,9 @@
6
6
  import { finishedTitle, readdedHeadings } from "../backlog/backlog-structure.js";
7
7
  import { fenceAwareHeadingLines, headingTitle, parseEntryDetails, sectionBodyLines, } from "../backlog/backlog-md.js";
8
8
  import { DONE_DATE_RE, FIXED_DATE_RE } from "../backlog/backlog.js";
9
- import { refusalNote, waitingNote } from "../backlog/backlog-eligibility.js";
9
+ import { refusalNote } from "../backlog/backlog-eligibility.js";
10
10
  import { entryKey, partKey, planDependencyProblems, seriesPart } from "../backlog/backlog-parts.js";
11
- import { WAITING_FOR_FAILURE_NOTE } from "../roles/role-guidance.js";
12
- import { moreSuffix } from "../text/phrases.js";
11
+ import { moreSuffix, plural } from "../text/phrases.js";
13
12
  import { collapseWhitespace, truncate } from "../text/text.js";
14
13
  /** The finding for the heading `requires` clause problems that `head` (the edited PLANS.md)
15
14
  * adds over `base` (its text at the merge-base), or undefined when it adds none; `archivedTitles`
@@ -90,31 +89,6 @@ export function finishedDuplicateFindings(spec, live, archive) {
90
89
  }
91
90
  return findings;
92
91
  }
93
- /** The number of `- **Progress …` lines in an entry body. */
94
- function progressCount(body) {
95
- return body.split("\n").filter((l) => /^-\s+\*\*Progress\b/.test(l.trim())).length;
96
- }
97
- /** The finding for a BUGS.md bug that `head` (the edited file) shows with more Progress notes
98
- * than `base` (its text at the merge-base) while it stays under `## Open` with neither a Waiting
99
- * note nor a Refused note — bugfix's step 2 asks a run that cannot reproduce a bug to write the
100
- * Waiting note, and runs that skip it leave the bug open to re-investigation. A newly filed bug
101
- * is not a re-investigation. Names the first such bug in file order, with a count of the rest. */
102
- export function waitingNoteFinding(head, base) {
103
- const was = new Map(parseEntryDetails(base, "Open").map((e) => [entryKey(e.title), e.body]));
104
- const counting = parseEntryDetails(head, "Open").filter((e) => {
105
- const before = was.get(entryKey(e.title));
106
- return (before !== undefined &&
107
- progressCount(e.body) > progressCount(before) &&
108
- waitingNote(e.body) === null &&
109
- refusalNote(e.body) === null);
110
- });
111
- if (counting.length === 0)
112
- return undefined;
113
- return (`BUGS.md bug "${counting[0].title}" gained a Progress note but stays open with no Waiting for the next ` +
114
- `failure note — if this run could not reproduce it, add ${WAITING_FOR_FAILURE_NOTE} as its own line under ` +
115
- `the heading so later runs wait for new evidence (bugfix's step 2); if it did reproduce, leave the entry ` +
116
- `as it is${moreSuffix(counting.length - 1)}`);
117
- }
118
92
  /** The skeleton placeholder that marks an empty backlog section. */
119
93
  const PLACEHOLDER_LINE = "_None yet._";
120
94
  /** The live backlog files; the archives have no placeholder sections. */
@@ -142,7 +116,7 @@ export function strayPlaceholderFinding(file, head, base) {
142
116
  if (stray.length === 0)
143
117
  return undefined;
144
118
  const [title, n] = stray[0];
145
- return (`${file}'s "## ${title}" section holds ${n} entr${n === 1 ? "y" : "ies"}, but this change left the ` +
119
+ return (`${file}'s "## ${title}" section holds ${plural(n, "entry", "entries")}, but this change left the ` +
146
120
  `empty-section placeholder ${PLACEHOLDER_LINE} in it — delete that line: it marks a section with no ` +
147
121
  `entries${moreSuffix(stray.length - 1)}`);
148
122
  }
@@ -222,8 +196,7 @@ function groundedRefusalReason(reason, own) {
222
196
  * role's own notes, or a different series part (the `b9664a461` case). `head` is the edited
223
197
  * text, `base` its text at the merge-base. A refusal already on main is not this change's and
224
198
  * is skipped; only a newly added (or edited) one is judged. Names the first offending entry in
225
- * file order, with a count of the rest. Stages after `waitingNoteFinding`, sharing its guard.
226
- * Undefined when the change adds no such refusal. */
199
+ * file order, with a count of the rest. Undefined when the change adds no such refusal. */
227
200
  export function ungroundedRefusalFinding(file, head, base) {
228
201
  const refusalByKey = (md) => {
229
202
  const out = new Map();
@@ -43,14 +43,13 @@
43
43
  * not after `-`, `.` or `/`, "that that" and "had had" excepted). Like the path checks, the gate
44
44
  * does not run it.
45
45
  *
46
- * The backlog-record findings named below (`planDependencyFinding`, `waitingNoteFinding`,
46
+ * The backlog-record findings named below (`planDependencyFinding`,
47
47
  * `finishedDuplicateFindings`, `strayPlaceholderFinding`) live in stage-backlog-checks.ts, which
48
48
  * also holds the claim-move findings run-stage.ts uses.
49
49
  *
50
50
  * `planDependencyFinding` is staging-only: it is not part of the review gate or the in-lock
51
51
  * merge re-check, because `tumwater doctor`'s plan-dependency warning is the backstop for
52
- * anything that gets past the fix-up turn. `waitingNoteFinding` is staging-only and bugfix-only
53
- * for the same reason: it nudges a bugfix run that left a Progress note without the hold.
52
+ * anything that gets past the fix-up turn.
54
53
  * Like `planDependencyFinding`, `finishedDuplicateFindings` runs only at staging. The gate does
55
54
  * not run it, because the sweep that creates the second copy runs after it.
56
55
  * `strayPlaceholderFinding` is staging-only too. It flags a `_None yet._` line the change left in a
@@ -69,9 +68,8 @@ import { changeBaseRev, fileContentAt } from "../git/git.js";
69
68
  import { readTextOrNull } from "../files/files.js";
70
69
  import { FINISHED_ARCHIVES, finishedArchiveOf, parseEntryDetails, } from "../backlog/backlog-md.js";
71
70
  import { DONE_DATE_RE } from "../backlog/backlog.js";
72
- import { baseRoleOf } from "../roles/runner-ids.js";
73
71
  import { acceptanceGrepFindings } from "./acceptance-greps.js";
74
- import { PLACEHOLDER_FILES, finishedDuplicateFindings, finishedKey, planDependencyFinding, strayPlaceholderFinding, ungroundedRefusalFinding, waitingNoteFinding, } from "./stage-backlog-checks.js";
72
+ import { PLACEHOLDER_FILES, finishedDuplicateFindings, finishedKey, planDependencyFinding, strayPlaceholderFinding, ungroundedRefusalFinding, } from "./stage-backlog-checks.js";
75
73
  import { gitLevelFindings } from "./stage-path-checks.js";
76
74
  /** Where finished plans are archived (plans/backlog-archive.md). */
77
75
  const PLANS_ARCHIVE = finishedArchiveOf("PLANS.md").archive;
@@ -97,7 +95,7 @@ const pushReason = async (findings, check) => {
97
95
  };
98
96
  /** The findings likely to get this uncommitted change rejected at merge, or an empty list when
99
97
  * it is clean: the merge gate's own deterministic checks plus the staging-only heuristics (plan
100
- * dependencies, the bugfix Waiting note, path drift). Checks the worktree at `wt` against
98
+ * dependencies, path drift). Checks the worktree at `wt` against
101
99
  * `mainBranch`'s merge-base; `exemptPaths` are the config's review-exemption globs that scope
102
100
  * the false-fix check.
103
101
  *
@@ -146,12 +144,6 @@ export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
146
144
  })().catch(() => []);
147
145
  findings.push(...greps);
148
146
  }
149
- if (baseRoleOf(role) === "bugfix" && files.includes("BUGS.md")) {
150
- const { head, base } = await headAndBase(wt, mainBranch, "BUGS.md");
151
- const waiting = base === undefined ? undefined : waitingNoteFinding(head, base);
152
- if (waiting !== undefined)
153
- findings.push(waiting);
154
- }
155
147
  if (files.includes("BUGS.md")) {
156
148
  const { head, base } = await headAndBase(wt, mainBranch, "BUGS.md");
157
149
  const refusal = base === undefined ? undefined : ungroundedRefusalFinding("BUGS.md", head, base);
@@ -16,6 +16,21 @@ export const MAX_STALE_PATHS = 10;
16
16
  export function diffPlusPath(line) {
17
17
  return line.slice(4).replace(/^b\//, "");
18
18
  }
19
+ /** Parse `git grep -n` stage output into `file:line:text` hits. `git grep -n` prints one
20
+ * `path:line:text` per match, and the four name/path checks (stage-name-checks.ts's
21
+ * staleNameFindings and movedNameFindings, stage-path-checks.ts's movedSourceFindings and
22
+ * staleReferenceFindings) all read that grammar to turn the raw output into individual hits;
23
+ * malformed (or trailing empty) lines are dropped. Callers then filter the hits and keep the
24
+ * `file:no` coordinate they want. */
25
+ export function gitGrepHits(out) {
26
+ const hits = [];
27
+ for (const line of out.split("\n")) {
28
+ const m = /^([^:]+):(\d+):(.*)$/.exec(line);
29
+ if (m)
30
+ hits.push({ file: m[1], no: m[2], text: m[3] ?? "" });
31
+ }
32
+ return hits;
33
+ }
19
34
  /** True for record prose: a backlog file (BACKLOG_FILES: PLANS.md, BUGS.md, QUESTIONS.md and the
20
35
  * finished-entry archives) or a markdown design doc under `plans/`. They record history and
21
36
  * describe planned work, so a path there is correct as written even when it is absent from the
@@ -8,7 +8,7 @@
8
8
  * not run any of them. */
9
9
  import path from "node:path";
10
10
  import { gitTry } from "../git/git-run.js";
11
- import { JS_FILE, MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, isRecordProse, standalonePathIndexes, } from "./stage-checks-shared.js";
11
+ import { JS_FILE, MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, gitGrepHits, isRecordProse, standalonePathIndexes, } from "./stage-checks-shared.js";
12
12
  /** How many removed identifiers one stale-name check looks up. */
13
13
  const MAX_STALE_NAMES = 40;
14
14
  /** The most characters between a moved name and its old file's name for a line to count as
@@ -86,12 +86,7 @@ export async function staleNameFindings(wt, diff) {
86
86
  const out = await gitTry(wt, "grep", "-n", "-w", "-I", "--cached", ...candidates.flatMap((n) => ["-e", n]));
87
87
  if (!out)
88
88
  return [];
89
- const lines = [];
90
- for (const line of out.split("\n")) {
91
- const m = /^([^:]+):(\d+):(.*)$/.exec(line);
92
- if (m)
93
- lines.push({ file: m[1], no: m[2], text: m[3] ?? "" });
94
- }
89
+ const lines = gitGrepHits(out);
95
90
  const findings = [];
96
91
  for (const name of candidates) {
97
92
  if (findings.length >= MAX_STALE_PATHS)
@@ -162,12 +157,7 @@ export async function movedNameFindings(wt, diff) {
162
157
  const out = await gitTry(wt, "grep", "-n", "-w", "-I", "--cached", ...picked.flatMap((m) => ["-e", m.name]));
163
158
  if (!out)
164
159
  return [];
165
- const lines = [];
166
- for (const line of out.split("\n")) {
167
- const m = /^([^:]+):(\d+):(.*)$/.exec(line);
168
- if (m)
169
- lines.push({ file: m[1], no: m[2], text: m[3] ?? "" });
170
- }
160
+ const lines = gitGrepHits(out);
171
161
  const findings = [];
172
162
  for (const { name, from, to } of picked) {
173
163
  if (findings.length >= MAX_STALE_PATHS)
@@ -12,7 +12,7 @@
12
12
  import { changeBaseRev } from "../git/git.js";
13
13
  import { gitTry } from "../git/git-run.js";
14
14
  import { pathTokens } from "../text/text.js";
15
- import { MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, diffPlusPath, hasStandalonePathOccurrence, isRecordProse, } from "./stage-checks-shared.js";
15
+ import { MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, diffPlusPath, gitGrepHits, hasStandalonePathOccurrence, isRecordProse, } from "./stage-checks-shared.js";
16
16
  import { movedNameFindings, staleNameFindings } from "./stage-name-checks.js";
17
17
  import { detachedDocFindings, garbledWordingFindings, newlineFindings } from "./stage-prose-checks.js";
18
18
  /** How many nonexistent added paths one finding names before it stops. */
@@ -71,6 +71,29 @@ export async function gitLevelFindings(wt, mainBranch) {
71
71
  await gitTry(wt, "reset");
72
72
  }
73
73
  }
74
+ /** `git grep --cached` for `needle`, returning its matching `file:line` strings that sit outside
75
+ * record prose and (when given) `excludeFile`, and that name `needle` as a path of its own. The
76
+ * single home of the two stale-reference sweeps, movedSourceFindings and staleReferenceFindings:
77
+ * both run the same fixed-string grep and filter its hits the same way before capping at
78
+ * MAX_STALE_HITS, so one helper keeps them from drifting. */
79
+ async function pathGrepHits(wt, needle, excludeFile) {
80
+ // `git grep` exits 1 with no output when nothing matches — the common, clean case — and
81
+ // gitTry maps that to null exactly as it maps a real failure, both of which mean "no hits".
82
+ const out = await gitTry(wt, "grep", "-n", "-F", "-I", "--cached", "-e", needle);
83
+ if (!out)
84
+ return [];
85
+ const hits = [];
86
+ for (const { file, no, text } of gitGrepHits(out)) {
87
+ if (file === excludeFile || isRecordProse(file))
88
+ continue;
89
+ if (!hasStandalonePathOccurrence(text, needle))
90
+ continue;
91
+ hits.push(`${file}:${no}`);
92
+ if (hits.length >= MAX_STALE_HITS)
93
+ break;
94
+ }
95
+ return hits;
96
+ }
74
97
  /** (f) References to a file the change moved code out of. A modified (`M`) file is a source
75
98
  * when at least MOVED_LINES_MIN of its removed `-U0` lines (trimmed, at least MOVED_LINE_MIN_CHARS
76
99
  * long) were added verbatim to another file. Each hit of the source's path outside record prose
@@ -120,20 +143,7 @@ async function movedSourceFindings(wt, diff, entries) {
120
143
  const moved = src.removed.filter((l) => others.some(([, v]) => v.added.has(l))).length;
121
144
  if (moved < MOVED_LINES_MIN)
122
145
  continue;
123
- const out = await gitTry(wt, "grep", "-n", "-F", "-I", "--cached", "-e", e.path);
124
- if (!out)
125
- continue;
126
- const hits = [];
127
- for (const line of out.split("\n")) {
128
- const m = /^([^:]+):(\d+):(.*)$/.exec(line);
129
- if (!m || m[1] === e.path || isRecordProse(m[1]))
130
- continue;
131
- if (!hasStandalonePathOccurrence(m[3] ?? "", e.path))
132
- continue;
133
- hits.push(`${m[1]}:${m[2]}`);
134
- if (hits.length >= MAX_STALE_HITS)
135
- break;
136
- }
146
+ const hits = await pathGrepHits(wt, e.path, e.path);
137
147
  if (hits.length === 0)
138
148
  continue;
139
149
  findings.push(`${e.path} had ${moved} lines moved to ${cappedList(receivers, MAX_STALE_PATHS)}, but it is ` +
@@ -155,20 +165,7 @@ async function staleReferenceFindings(wt, entries) {
155
165
  if (reported >= MAX_STALE_PATHS)
156
166
  break;
157
167
  const old = e.oldPath ?? e.path;
158
- // `git grep` exits 1 with no output when nothing matches — the common, clean case — and
159
- // gitTry maps that to null exactly as it maps a real failure, both of which mean "no hits".
160
- const out = await gitTry(wt, "grep", "-n", "-F", "-I", "--cached", "-e", old);
161
- if (!out)
162
- continue;
163
- const hits = [];
164
- for (const line of out.split("\n")) {
165
- const m = /^([^:]+):(\d+):(.*)$/.exec(line);
166
- if (!m || isRecordProse(m[1]) || !hasStandalonePathOccurrence(m[3] ?? "", old))
167
- continue;
168
- hits.push(`${m[1]}:${m[2]}`);
169
- if (hits.length >= MAX_STALE_HITS)
170
- break;
171
- }
168
+ const hits = await pathGrepHits(wt, old);
172
169
  if (hits.length === 0)
173
170
  continue;
174
171
  const moved = e.status === "R" ? `was renamed to ${e.path}` : "was deleted";
@@ -76,6 +76,17 @@ export async function pinnedRetrySha(root, role, mainBranch, state) {
76
76
  export async function hasUnmergedPin(root, role) {
77
77
  return (await refSha(root, mergeRefName(role)).catch(() => null)) !== null;
78
78
  }
79
+ /** The main_red wait outcome a kept change returns while `ctx.mainRedRetry` is the only
80
+ * outstanding hold — same kept sha and same "waits for main to move past red" message for both
81
+ * recovery paths (the pinned ref and the no-pin adopted tip), so the wording cannot drift per
82
+ * path and the two branches stay byte-stable. */
83
+ function mainRedWait(sha, head) {
84
+ return {
85
+ kind: "main_red_wait",
86
+ sha,
87
+ message: `kept change ${shortSha(sha)} waits for main to move past red ${shortSha(head)} before its next merge`,
88
+ };
89
+ }
79
90
  /** Queue a commit a previous run left unmerged. Entry condition: the merge ref exists and
80
91
  * its sha is not yet contained in main — or, with no pin at all, the role's branch is ahead of
81
92
  * main (crash between the commit and the pin). A present-but-contained ref is stale — a crash
@@ -127,8 +138,7 @@ export async function recoverLeftover(ctx) {
127
138
  if (heldTip)
128
139
  return { kind: "held", sha, message: heldTip.message };
129
140
  if (ctx.mainRedRetry) {
130
- const message = `kept change ${shortSha(sha)} waits for main to move past red ${shortSha(ctx.mainRedRetry.head)} before its next merge`;
131
- return { kind: "main_red_wait", sha, message };
141
+ return mainRedWait(sha, ctx.mainRedRetry.head);
132
142
  }
133
143
  // Adopt the unpinned commit into the pin scheme so every downstream outcome — kept on an
134
144
  // under-cap review failure (the strike cap's retry), deleted on reject/merge/discard,
@@ -150,8 +160,7 @@ export async function recoverLeftover(ctx) {
150
160
  // `requeueMainRedPin` owns the re-queue of a main_red pin, once main's head moves. Queuing it
151
161
  // here would pay a full gate check against the same red head, whose result is already known.
152
162
  if (ctx.mainRedRetry) {
153
- const message = `kept change ${shortSha(sha)} waits for main to move past red ${shortSha(ctx.mainRedRetry.head)} before its next merge`;
154
- return { kind: "main_red_wait", sha, message };
163
+ return mainRedWait(sha, ctx.mainRedRetry.head);
155
164
  }
156
165
  const attempts = conflictAttempts(ctx.mergeConflicts, sha);
157
166
  if (attempts >= MERGE_CONFLICT_LIMIT) {
@@ -3,13 +3,13 @@ import { readTextIfExists, readTextOrNull, writeTextAtomic } from "../files/file
3
3
  import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
4
4
  import { gitTry, gitTryStarted } from "../git/git-run.js";
5
5
  import { resetWorktreeToMain } from "../git/worktree.js";
6
- import { resolveBacklogInsertConflicts } from "../merge/backlog-conflicts.js";
6
+ import { resolveBacklogInsertConflicts } from "../merge/merge-backlog-conflicts.js";
7
7
  import { conflictedFiles } from "../merge/merge-git.js";
8
8
  import { formatDate } from "../text/datetime.js";
9
9
  import { stampedSubject } from "../git/commit-message.js";
10
10
  import { baseRoleOf } from "../roles/runner-ids.js";
11
11
  import { entryKey } from "../backlog/backlog-parts.js";
12
- import { actionableEntryRanges } from "../backlog/backlog-structure.js";
12
+ import { actionableEntryRanges } from "../backlog/backlog-index-data.js";
13
13
  import { CONVERGENCE_HOLD_MARKER } from "../roles/role-guidance.js";
14
14
  import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
15
15
  import { logEventBestEffort } from "../events/events.js";
@@ -3,7 +3,7 @@ import { modelFallbackVerdict, providerFailureReason, recordModelFallback } from
3
3
  /** Fold one AUTHORING agent run's verdict into the role's model-fallback state
4
4
  * (src/runner/model-fallback.ts) and emit the episode's started/ended events. Only the agent run
5
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
6
+ * inbox, a recovered leftover, a merge-side `main_red` pin) returns from runOnce before this is
7
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
@@ -62,11 +62,6 @@ export async function handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt
62
62
  flow,
63
63
  filed,
64
64
  ci,
65
- // The image-reroute arm resolves its target from the live config and the current
66
- // regime, and names the model this run ran on as the handoff's `from`.
67
- config: ctx.config,
68
- rerouteRegime: ctx.rerouteRegime(piStartedAt),
69
- runModel: cfg.model,
70
65
  warn: (message) => ctx.warn(message),
71
66
  merge: (w, sum) => mergePhase(ctx, w, sum),
72
67
  finishAbortedRun: () => ctx.finishAbortedRun(userPrompt, wt),
@@ -232,67 +232,90 @@ export class RunnerPi {
232
232
  const pi = await runPi(opts);
233
233
  this.warnFallbackClone(pi);
234
234
  this.observeFreeModelCost(pi, opts.config);
235
- if (!pi.aborted &&
236
- !pi.timedOut &&
237
- !pi.quietKilled &&
238
- (pi.transientServerTimeout ||
239
- pi.transientPiCrash ||
240
- pi.transientRateLimit ||
241
- this.retryCoversBackendKind(pi)) &&
242
- !pi.ok &&
243
- // A subscription claude usage-window rejection is not the 429 the retry's minute-scale
244
- // wait beats: the window resets hours out, so a within-run retry is guaranteed to fail
245
- // after burning up to RATE_LIMIT_RETRY_AFTER_CAP_S. The run ends and the kept session
246
- // resumes once the team hold lifts (plans/agent-backends.md §8, part 6e).
247
- !subscriptionWindowRejected(pi)) {
248
- this.host.warn(pi.transientPiCrash
249
- ? `pi crashed on malformed JSON (${pi.errorMessage ?? "no detail"})${on} — resuming the session once`
250
- : pi.transientRateLimit
251
- ? `provider rate-limited the request (429${pi.retryAfterSeconds ? `, retry after ${pi.retryAfterSeconds}s` : `, no hint — waiting ${RATE_LIMIT_NO_HINT_RETRY_S}s`})${on} — retrying the agent run once`
252
- : pi.backendKind === "stream-severed"
253
- ? `provider severed the in-flight stream (terminated)${on} — retrying the agent run once`
254
- : pi.backendKind === "timeout"
255
- ? `provider request timed out after accepting the connection${on} — retrying the agent run once`
256
- : `model server timed out an idle predict stream (e.g. machine sleep)${on} — retrying the agent run once`);
235
+ if (this.transientRetryAdmits(pi)) {
236
+ this.host.warn(this.transientRetryWarning(pi, on));
257
237
  // The failed attempt folds NOW, before the wait and the retry: a 429 it ended on is the
258
238
  // team-wide rate-limit hold's input (RoleRunner.lastRateLimit, stamped at fold time), and
259
239
  // folding after a retry that ran on for an hour would report the storm an hour late.
260
240
  fold(pi);
261
- // The pause is the rate-limit branch's, plus the timeout kind's: a server timeout or
262
- // pi crash is a failure of the local path, retried at once, while a 429 must wait its
263
- // per-minute bucket out. A hint-less 429 defaults to that minute-scale refill pause
264
- // instead of 0 — an immediate retry lands in the same exhausted bucket and burns the
265
- // run's only retry (BUGS.md 2026-09-25). A present hint wins; the cap bounds either.
266
- // A hint of 0 (or any non-positive value) is no usable hint — "retry now" is exactly
267
- // the burned-retry bug a hint-less 429 causes — so it falls back to the refill pause
268
- // just like a missing hint, keeping the warning text and the actual wait in agreement.
269
- // The timeout kind (BUGS.md 2026-09-30) takes that same minute-scale pause: the
270
- // provider accepted the request and failed to answer it, an overload shape — an
271
- // immediate re-request lands at the back of the same queue and burns the run's only
272
- // retry, so the retry waits the refill pause out first.
273
- const hintS = pi.retryAfterSeconds && pi.retryAfterSeconds > 0 ? pi.retryAfterSeconds : RATE_LIMIT_NO_HINT_RETRY_S;
274
- const waitS = pi.transientRateLimit
275
- ? Math.min(hintS, RATE_LIMIT_RETRY_AFTER_CAP_S)
276
- : pi.backendKind === "timeout"
277
- ? RATE_LIMIT_NO_HINT_RETRY_S
278
- : 0;
241
+ const waitS = this.transientRetryWaitSeconds(pi);
279
242
  if (waitS > 0)
280
243
  await (this.host.sleep?.(waitS * 1000) ?? sleepFor(waitS * 1000));
281
- // Within-run continuity only: resume the session the first attempt created, so its
282
- // partial progress is not re-done. The next run still starts fresh.
283
- const retry = await runPi({ ...opts, continueSession: true });
284
- this.warnFallbackClone(retry);
285
- this.observeFreeModelCost(retry, opts.config);
286
- if (foldFinal)
287
- fold(retry);
288
- this.warnBackendKindFloor(retry, on);
289
- return retry;
244
+ return this.retryContinuingSession(opts, on, foldFinal, fold);
290
245
  }
291
246
  this.warnBackendKindFloor(pi, on);
292
247
  if (foldFinal)
293
248
  fold(pi);
294
249
  return pi;
295
250
  }
251
+ /** The retry's admission predicate: whether this run qualifies for the one bounded transient
252
+ * retry — not cleanly ended or killed, but ended on a transient server condition (a server
253
+ * timeout, a pi crash, a 429, or a backend kind the retry covers) and not a subscription
254
+ * usage-window rejection. */
255
+ transientRetryAdmits(run) {
256
+ return (!run.aborted &&
257
+ !run.timedOut &&
258
+ !run.quietKilled &&
259
+ (run.transientServerTimeout ||
260
+ run.transientPiCrash ||
261
+ run.transientRateLimit ||
262
+ this.retryCoversBackendKind(run)) &&
263
+ !run.ok &&
264
+ // A subscription claude usage-window rejection is not the 429 the retry's minute-scale
265
+ // wait beats: the window resets hours out, so a within-run retry is guaranteed to fail
266
+ // after burning up to RATE_LIMIT_RETRY_AFTER_CAP_S. The run ends and the kept session
267
+ // resumes once the team hold lifts (plans/agent-backends.md §8, part 6e).
268
+ !subscriptionWindowRejected(run));
269
+ }
270
+ /** The per-run retry warning text, keyed off which transient condition the first attempt
271
+ * ended on. */
272
+ transientRetryWarning(run, on) {
273
+ return run.transientPiCrash
274
+ ? `pi crashed on malformed JSON (${run.errorMessage ?? "no detail"})${on} — resuming the session once`
275
+ : run.transientRateLimit
276
+ ? `provider rate-limited the request (429${run.retryAfterSeconds ? `, retry after ${run.retryAfterSeconds}s` : `, no hint — waiting ${RATE_LIMIT_NO_HINT_RETRY_S}s`})${on} — retrying the agent run once`
277
+ : run.backendKind === "stream-severed"
278
+ ? `provider severed the in-flight stream (terminated)${on} — retrying the agent run once`
279
+ : run.backendKind === "timeout"
280
+ ? `provider request timed out after accepting the connection${on} — retrying the agent run once`
281
+ : `model server timed out an idle predict stream (e.g. machine sleep)${on} — retrying the agent run once`;
282
+ }
283
+ /** The seconds to wait before the bounded retry, the pause taken from the moved wait ladder;
284
+ * the sleep itself stays in the caller. */
285
+ transientRetryWaitSeconds(run) {
286
+ // The pause is the rate-limit branch's, plus the timeout kind's: a server timeout or
287
+ // pi crash is a failure of the local path, retried at once, while a 429 must wait its
288
+ // per-minute bucket out. A hint-less 429 defaults to that minute-scale refill pause
289
+ // instead of 0 — an immediate retry lands in the same exhausted bucket and burns the
290
+ // run's only retry (BUGS.md 2026-09-25). A present hint wins; the cap bounds either.
291
+ // A hint of 0 (or any non-positive value) is no usable hint — "retry now" is exactly
292
+ // the burned-retry bug a hint-less 429 causes — so it falls back to the refill pause
293
+ // just like a missing hint, keeping the warning text and the actual wait in agreement.
294
+ // The timeout kind (BUGS.md 2026-09-30) takes that same minute-scale pause: the
295
+ // provider accepted the request and failed to answer it, an overload shape — an
296
+ // immediate re-request lands at the back of the same queue and burns the run's only
297
+ // retry, so the retry waits the refill pause out first.
298
+ const hintS = run.retryAfterSeconds && run.retryAfterSeconds > 0 ? run.retryAfterSeconds : RATE_LIMIT_NO_HINT_RETRY_S;
299
+ return run.transientRateLimit
300
+ ? Math.min(hintS, RATE_LIMIT_RETRY_AFTER_CAP_S)
301
+ : run.backendKind === "timeout"
302
+ ? RATE_LIMIT_NO_HINT_RETRY_S
303
+ : 0;
304
+ }
305
+ /** The one bounded retry: resume the session the first attempt created on the same opts, so
306
+ * its partial progress is not re-done (the next run still starts fresh), folding the retried
307
+ * run when foldFinal is set and warning the backend-kind floor for its returned agent run. */
308
+ async retryContinuingSession(opts, on, foldFinal, fold) {
309
+ // Within-run continuity only: resume the session the first attempt created, so its
310
+ // partial progress is not re-done. The next run still starts fresh.
311
+ const retry = await runPi({ ...opts, continueSession: true });
312
+ this.warnFallbackClone(retry);
313
+ this.observeFreeModelCost(retry, opts.config);
314
+ if (foldFinal)
315
+ fold(retry);
316
+ this.warnBackendKindFloor(retry, on);
317
+ return retry;
318
+ }
296
319
  /** The backend-kind floor (BUGS.md 2026-09-29): a failed agent run that ended on a
297
320
  * provider-backend failure (connection down, 5xx, model load) still warns — parity with
298
321
  * the 429 branch above, so any episode is visible from the feed alone instead of living