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.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /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
|
|
9
|
+
import { refusalNote } from "../backlog/backlog-eligibility.js";
|
|
10
10
|
import { entryKey, partKey, planDependencyProblems, seriesPart } from "../backlog/backlog-parts.js";
|
|
11
|
-
import {
|
|
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
|
|
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.
|
|
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`,
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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 (
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|