tumwater 0.4.4 → 0.4.6
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 +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-run.js +1 -0
- package/dist/src/agent/opencode/write-guard-plugin/index.js +11 -8
- package/dist/src/agent/subscription-account.js +68 -3
- package/dist/src/agent/write-guard.js +50 -13
- package/dist/src/backlog/backlog-eligibility.js +8 -4
- package/dist/src/backlog/backlog-index.js +2 -2
- package/dist/src/backlog/backlog-md.js +45 -19
- package/dist/src/backlog/backlog-release.js +3 -11
- package/dist/src/backlog/backlog-write.js +19 -11
- package/dist/src/baseline/main-baseline.js +47 -72
- package/dist/src/baseline/main-guardrails.js +11 -7
- package/dist/src/baseline/main-red.js +171 -19
- package/dist/src/baseline/main-watch.js +71 -24
- package/dist/src/budget/budget-fallback-view.js +96 -28
- package/dist/src/build/build-check-detect.js +22 -3
- package/dist/src/build/build-check-events.js +25 -5
- package/dist/src/build/build-check-report.js +40 -4
- package/dist/src/build/build-check-rerun.js +62 -0
- package/dist/src/build/build-check-scoped.js +13 -2
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +4 -0
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/build/host-sleep.js +9 -0
- package/dist/src/change/change-render.js +2 -1
- package/dist/src/cli/cli-args.js +48 -3
- package/dist/src/cli/cli-command-args.js +12 -4
- package/dist/src/cli/cli-flag-specs.js +5 -3
- package/dist/src/cli/cli-init.js +5 -1
- package/dist/src/cli/cli-log-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +31 -15
- package/dist/src/cli/cli-review-commands.js +3 -3
- package/dist/src/cli/cli-role-command.js +338 -244
- package/dist/src/cli/cli-start.js +10 -2
- package/dist/src/cli/completion.js +6 -2
- package/dist/src/cli/help.js +6 -3
- package/dist/src/cli.js +112 -140
- package/dist/src/collections.js +21 -0
- package/dist/src/concurrency/check-permit.js +30 -3
- package/dist/src/concurrency/lock.js +73 -14
- package/dist/src/config/config-editable-keys.js +9 -0
- package/dist/src/config/config-fallbacks.js +16 -5
- package/dist/src/config/config-schema.js +3 -3
- package/dist/src/config/config-validation.js +2 -0
- package/dist/src/config/config-views.js +55 -33
- package/dist/src/config/config-write.js +76 -16
- package/dist/src/config/config.js +12 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +33 -1
- package/dist/src/doctor/doctor-backlog.js +6 -8
- package/dist/src/doctor/doctor-check-names.js +2 -0
- package/dist/src/doctor/doctor-checks.js +13 -6
- package/dist/src/doctor/doctor-claude-checks.js +6 -13
- package/dist/src/doctor/doctor-codex-checks.js +2 -2
- package/dist/src/doctor/doctor-contention-checks.js +7 -6
- package/dist/src/doctor/doctor-fallback-checks.js +37 -2
- package/dist/src/doctor/doctor-holds.js +7 -10
- package/dist/src/doctor/doctor-model-checks.js +16 -1
- package/dist/src/doctor/doctor-opencode-checks.js +2 -2
- package/dist/src/doctor/doctor-orphans.js +103 -27
- package/dist/src/doctor/doctor-project-checks.js +43 -26
- package/dist/src/doctor/doctor.js +5 -3
- package/dist/src/events/event-format.js +113 -136
- package/dist/src/events/event-labels.js +3 -1
- package/dist/src/events/events.js +3 -3
- package/dist/src/failure/failure-data.js +6 -4
- package/dist/src/failure/failure-render.js +1 -0
- package/dist/src/failure/failure-state-change.js +14 -4
- package/dist/src/failure/failure-time-spend.js +1 -0
- package/dist/src/files/file-queue.js +32 -19
- package/dist/src/files/files.js +2 -2
- package/dist/src/files/json-object.js +3 -2
- package/dist/src/gates/budget-gates.js +37 -2
- package/dist/src/gates/gate-polls.js +8 -7
- package/dist/src/gates/{maintenance-quota.js → maintenance-quota-gates.js} +1 -1
- package/dist/src/gates/startup-gates.js +18 -1
- package/dist/src/git/git.js +8 -0
- package/dist/src/git/slot-salvage.js +6 -0
- package/dist/src/git/worktree-pool.js +18 -5
- package/dist/src/gui/gui-admin-endpoints.js +11 -1
- package/dist/src/gui/gui-args.js +2 -2
- package/dist/src/gui/gui-backlog-endpoints.js +2 -1
- package/dist/src/gui/gui-history-endpoints.js +7 -7
- package/dist/src/gui/gui-operator-commands.js +2 -3
- package/dist/src/history/history.js +12 -8
- package/dist/src/{run → history}/run-detail-data.js +2 -2
- package/dist/src/{run → history}/run-detail-render.js +2 -2
- package/dist/src/{run → history}/run-detail.js +2 -2
- package/dist/src/inbox/director-request.js +92 -0
- package/dist/src/inbox/inbox-top.js +2 -1
- package/dist/src/merge/merge-backlog-conflicts.js +33 -11
- package/dist/src/merge/merge-batch.js +22 -235
- package/dist/src/merge/merge-cancel.js +2 -1
- package/dist/src/merge/merge-core.js +26 -22
- package/dist/src/merge/merge-drain.js +116 -101
- package/dist/src/merge/merge-pipeline.js +11 -8
- package/dist/src/merge/merge-post-review-drain.js +8 -2
- package/dist/src/merge/merge-questions.js +3 -3
- package/dist/src/merge/merge-queue-render.js +2 -2
- package/dist/src/merge/merge-queue.js +9 -1
- package/dist/src/merge/merge-requeue.js +67 -26
- package/dist/src/merge/merge-slot.js +41 -10
- package/dist/src/merge/merge-to-main.js +20 -165
- package/dist/src/merge/merge-top.js +2 -1
- package/dist/src/merge/merge-verify.js +18 -5
- package/dist/src/merge/merge-withdrawn.js +71 -0
- package/dist/src/model-check/model-check-gate.js +206 -0
- package/dist/src/model-check/model-check-probe.js +109 -0
- package/dist/src/model-check/model-check.js +101 -0
- package/dist/src/{run → observer}/ci-parity.js +15 -2
- package/dist/src/operator/operator-commands.js +8 -25
- package/dist/src/operator/operator-revert.js +1 -1
- package/dist/src/orchestrator/orchestrator-launch.js +6 -1
- package/dist/src/orchestrator/orchestrator-poll.js +32 -5
- package/dist/src/orchestrator/orchestrator-scheduling.js +7 -7
- package/dist/src/orchestrator/orchestrator.js +74 -13
- package/dist/src/paths.js +41 -3
- package/dist/src/pi/pi-run-result.js +16 -0
- package/dist/src/pi/pi-watchdogs.js +15 -4
- package/dist/src/process/process.js +24 -0
- package/dist/src/prompt/gate-notes.js +10 -4
- package/dist/src/prompt/gate-prompts.js +19 -15
- package/dist/src/prompt/principles.js +13 -3
- package/dist/src/prompt/prompt-followup.js +14 -5
- package/dist/src/prompt/prompt.js +11 -0
- package/dist/src/redeploy/redeploy-probes.js +53 -18
- package/dist/src/redeploy/redeploy-target.js +100 -0
- package/dist/src/redeploy/redeployer.js +135 -70
- package/dist/src/report/report-main-red.js +2 -2
- package/dist/src/report/report-render.js +2 -2
- package/dist/src/report/report.js +2 -2
- package/dist/src/review/known-flakes.js +105 -42
- package/dist/src/review/post-merge-review-accept.js +2 -1
- package/dist/src/review/post-merge-review.js +46 -34
- package/dist/src/review/review-precheck.js +72 -28
- package/dist/src/review/review-verdict.js +25 -17
- package/dist/src/roles/role-catalog.js +23 -7
- package/dist/src/roles/role-render.js +158 -83
- package/dist/src/roles/role-view.js +297 -75
- package/dist/src/roles/roles.js +1 -1
- package/dist/src/run/run-apply.js +3 -8
- package/dist/src/run/run-dismiss.js +2 -2
- package/dist/src/run/run-finalize.js +4 -0
- package/dist/src/run/run-prompt.js +4 -4
- package/dist/src/run/run-stage.js +7 -1
- package/dist/src/run/run-timing.js +2 -2
- package/dist/src/run/run-usage.js +9 -1
- package/dist/src/run/run-verdict.js +80 -52
- package/dist/src/runner/runner-leftover.js +48 -1
- package/dist/src/runner/runner-outcome.js +1 -2
- package/dist/src/runner/runner-pi.js +6 -0
- package/dist/src/runner/runner-recovery.js +5 -4
- package/dist/src/runner/runner-revision.js +2 -1
- package/dist/src/runner/runner-run.js +165 -110
- package/dist/src/runner/runner-state-load.js +115 -53
- package/dist/src/runner/runner-state.js +3 -2
- package/dist/src/runner/runner.js +39 -20
- package/dist/src/{orchestrator/scheduling-admit.js → scheduling/admit.js} +2 -2
- package/dist/src/scheduling/backoff.js +6 -6
- package/dist/src/scheduling/claims.js +103 -26
- package/dist/src/{orchestrator/scheduling-scope.js → scheduling/scope.js} +24 -60
- package/dist/src/{orchestrator/scheduling-skip.js → scheduling/skip.js} +112 -78
- package/dist/src/{run → stage}/stage-path-checks.js +3 -3
- package/dist/src/status/status-budget.js +38 -4
- package/dist/src/status/status-data.js +23 -14
- package/dist/src/status/status-polls.js +83 -26
- package/dist/src/status/status-role-notes.js +25 -7
- package/dist/src/status/status-roles.js +56 -25
- package/dist/src/team/team-orchestrator-info.js +51 -0
- package/dist/src/team/team-state.js +25 -9
- package/dist/src/text/format.js +20 -2
- package/dist/src/text/phrases.js +59 -12
- package/dist/src/text/suggest.js +14 -1
- package/dist/src/text/text.js +31 -2
- package/dist/src/ui/badges.js +102 -17
- package/dist/src/ui/gui/gui-client-drawer-config.js +12 -6
- package/dist/src/ui/gui/gui-client-model.js +5 -0
- package/dist/src/ui/gui/gui-client-operator.js +11 -14
- package/dist/src/ui/gui/gui-client-pending.js +10 -3
- package/dist/src/ui/gui/gui-client-report-chart.js +22 -15
- package/dist/src/ui/gui/gui-client-settings.js +25 -11
- package/dist/src/ui/gui/gui-client-team.js +7 -0
- package/dist/src/ui/status-model.js +21 -6
- package/dist/src/ui/status-payload.js +19 -5
- package/dist/src/ui/status-render.js +14 -8
- package/dist/src/ui/team-alert-shared.js +2 -2
- package/dist/src/ui/team-alert-standings.js +96 -0
- package/dist/src/ui/team-alert-status.js +39 -12
- package/dist/src/ui/team-alerts.js +8 -4
- package/dist/src/ui/tone.js +2 -1
- package/dist/src/ui/tui/tui-backlog.js +12 -0
- package/dist/src/ui/tui/tui-editor-attach.js +2 -6
- package/dist/src/ui/tui/tui-editor-brief.js +2 -6
- package/dist/src/ui/tui/tui-editor-interval.js +2 -2
- package/dist/src/ui/tui/tui-editor-release.js +2 -6
- package/dist/src/ui/tui/tui-editor-retire.js +2 -6
- package/dist/src/ui/tui/tui-editor-role-enabled.js +2 -6
- package/dist/src/ui/tui/tui-editor-role-model.js +5 -1
- package/dist/src/ui/tui/tui-editor-session.js +14 -0
- package/dist/src/ui/tui/tui-guard.js +16 -0
- package/dist/src/ui/tui/tui-input.js +2 -2
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-backlog.js +2 -2
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +29 -35
- package/dist/src/ui/tui/tui-pane-history.js +158 -479
- package/dist/src/ui/tui/tui-pane-pending-selection.js +346 -0
- package/dist/src/ui/tui/tui-pane-pending.js +54 -344
- package/dist/src/ui/tui/tui-pane-queued.js +2 -2
- package/dist/src/ui/tui/tui-pane-trail.js +415 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +2 -7
- package/dist/src/ui/tui/tui-pane.js +3 -3
- package/dist/src/ui/tui/tui.js +2 -1
- package/dist/src/verdict/fix-claim.js +10 -4
- package/dist/src/verdict/reply-contract.js +14 -5
- package/package.json +9 -7
- package/dist/src/merge/merge-check-failures.js +0 -212
- package/dist/src/merge/merge-stack.js +0 -235
- /package/dist/src/{run → observer}/feedback-filings.js +0 -0
- /package/dist/src/{run → observer}/qa-coverage.js +0 -0
- /package/dist/src/{run → observer}/selfcheck-digest.js +0 -0
- /package/dist/src/{run → stage}/acceptance-greps.js +0 -0
- /package/dist/src/{run → stage}/stage-backlog-checks.js +0 -0
- /package/dist/src/{run → stage}/stage-check.js +0 -0
- /package/dist/src/{run → stage}/stage-checks-shared.js +0 -0
- /package/dist/src/{run → stage}/stage-name-checks.js +0 -0
- /package/dist/src/{run → stage}/stage-prose-checks.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { distinctOrdered } from "../collections.js";
|
|
2
|
-
import { checkMainBaseline } from "./main-baseline.js";
|
|
3
|
-
import {
|
|
2
|
+
import { checkMainBaseline, settleRedByRerun } from "./main-baseline.js";
|
|
3
|
+
import { canRerun, rerunTests } from "../build/build-check-rerun.js";
|
|
4
|
+
import { baselineCheckLogger, clearHealKeyOnGreen, dropHealPromptsCoveredBy, withMainTipWorktree } from "./main-red.js";
|
|
4
5
|
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
5
6
|
import { FAILING_TESTS_MAX } from "../build/build-check-report.js";
|
|
6
7
|
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
@@ -32,6 +33,14 @@ function mainCheckEvent(baseline, red, quarantined, covered, durationMs) {
|
|
|
32
33
|
}
|
|
33
34
|
/** The failing tests of each root's newest watch verdict, keyed by root. */
|
|
34
35
|
const knownFailures = new Map();
|
|
36
|
+
/** The commit each root's red spell was opened at, keyed by root — the tip of the first red
|
|
37
|
+
* verdict since the last green. It stays the named culprit while the spell is open: a later tip
|
|
38
|
+
* re-checks red (the red never inherits across a doc commit) but did not open it, so it is not
|
|
39
|
+
* re-attributed. A green verdict clears it, so a red that opens after a green names its own
|
|
40
|
+
* opener. In-memory only: a restart mid-red loses the spell's opener, and the next settled red
|
|
41
|
+
* tip then names itself as culprit — the pre-fix behaviour, and a bounded one (one tip per
|
|
42
|
+
* restart, since a spell's later tips inherit the red and settle without a re-run). */
|
|
43
|
+
const redSpellOpeners = new Map();
|
|
35
44
|
/** Main's known failures: the failing tests of the watch's newest verdict — main's tip, or the
|
|
36
45
|
* nearest tip it checked while the newest head waits — and every quarantined flaky test
|
|
37
46
|
* (review/known-flakes.ts). Empty when the verdict is green, red with no parsed test, or none has
|
|
@@ -138,10 +147,14 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
};
|
|
141
|
-
/** Run the declared build check on main's tip,
|
|
142
|
-
* whose
|
|
143
|
-
*
|
|
144
|
-
*
|
|
150
|
+
/** Run the declared build check on main's tip, confirming a red once and quarantining a flake
|
|
151
|
+
* whose confirmation passes. A red whose failing tests can be re-run on their own (canRerun) is
|
|
152
|
+
* confirmed by re-running just those tests, so the full check runs once per tip: a test that
|
|
153
|
+
* passes there is quarantined, and the red narrows to the tests that failed again (or turns
|
|
154
|
+
* green when none did). A re-run that reaches no verdict leaves the first red standing. Without
|
|
155
|
+
* a re-run command the whole check runs again, as before. Returns the check's result and
|
|
156
|
+
* whether any full run actually executed (false for a cache hit, a skip, or a sleep-spanned
|
|
157
|
+
* run). `config` and `root` are captured from `watchOnce`. */
|
|
145
158
|
async function runBaselineCheck(wt) {
|
|
146
159
|
let ran = false;
|
|
147
160
|
const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
|
|
@@ -150,13 +163,27 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
150
163
|
onRun(run);
|
|
151
164
|
};
|
|
152
165
|
let check = await checkMainBaseline(wt, config, hook, false, false, undefined, false, CHECK_TIER.merge);
|
|
153
|
-
if (check.baseline?.status
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
if (check.baseline?.status !== "red")
|
|
167
|
+
return { check, ran };
|
|
168
|
+
const first = check.baseline;
|
|
169
|
+
const named = first.failingTests ?? [];
|
|
170
|
+
if (canRerun(wt, config, named)) {
|
|
171
|
+
const rerun = await rerunTests(root, TUMWATER_SOURCE, wt, config, named, first.failingTestFiles, CHECK_TIER.merge);
|
|
172
|
+
if (rerun) {
|
|
173
|
+
// Each test that failed and then passed on this same tree is a flake.
|
|
174
|
+
const passed = named.filter((t) => !rerun.failing.includes(t));
|
|
175
|
+
if (passed.length > 0)
|
|
176
|
+
quarantineTests(root, passed, passed.join("\n"));
|
|
177
|
+
const settled = settleRedByRerun(first.sha, rerun.failing);
|
|
178
|
+
if (settled)
|
|
179
|
+
check = { baseline: settled };
|
|
180
|
+
}
|
|
181
|
+
return { check, ran };
|
|
159
182
|
}
|
|
183
|
+
check = await checkMainBaseline(wt, config, hook, true, false, undefined, false, CHECK_TIER.merge);
|
|
184
|
+
// A red the same tree's confirming re-run turns green is a flake: quarantine its tests.
|
|
185
|
+
if (check.baseline?.status === "green")
|
|
186
|
+
quarantineTests(root, named, named.join("\n"));
|
|
160
187
|
return { check, ran };
|
|
161
188
|
}
|
|
162
189
|
/** The covered-commit count for a main_check event: the first-parent commits between the
|
|
@@ -179,24 +206,37 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
179
206
|
logEventBestEffort(root, mainCheckEvent(baseline, red, quarantined, covered, ran ? Date.now() - started : 0));
|
|
180
207
|
return red;
|
|
181
208
|
}
|
|
182
|
-
/** Name a settled red tip as the culprit:
|
|
183
|
-
* the onCulprit callback with the tip and its failing tests.
|
|
209
|
+
/** Name a settled red tip's spell as the culprit: the merging runner, the main_culprit
|
|
210
|
+
* event, and the onCulprit callback with the tip and its failing tests. The commit named is
|
|
211
|
+
* the one that OPENED the red — the first red tip since the last green — not the newest red
|
|
212
|
+
* tip: a doc-exempt tip that lands after the red opened cannot have changed the suite, so
|
|
213
|
+
* blaming it would name a merge in the G1 phrase (src/baseline/main-guardrails.ts) and the
|
|
214
|
+
* ci heal prompt (src/baseline/main-red.ts) that did not break anything. The opener is named
|
|
215
|
+
* once per spell with the tip's failing tests and, when the newest merged event names the
|
|
216
|
+
* opener, its runner; while the spell is open a later tip re-settling red re-attributes
|
|
217
|
+
* nothing. */
|
|
184
218
|
function reportCulprit(baseline) {
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
219
|
+
// A red spell that is already open keeps its opener named: the first red tip since the
|
|
220
|
+
// last green caused the red, so a later tip re-settling red (a doc-exempt tip cannot
|
|
221
|
+
// change the suite, and a code tip does not open a SECOND spell until a green closed the
|
|
222
|
+
// first) is not re-attributed — no new event, no new heal prompt (the heal dedup already
|
|
223
|
+
// re-queues when the failing set returns after a green).
|
|
224
|
+
if (redSpellOpeners.has(root))
|
|
225
|
+
return;
|
|
226
|
+
const culprit = baseline.sha;
|
|
227
|
+
redSpellOpeners.set(root, culprit);
|
|
228
|
+
const runner = mergingRoleOf(root, culprit);
|
|
190
229
|
logEventBestEffort(root, {
|
|
191
230
|
source: TUMWATER_SOURCE,
|
|
192
231
|
type: "main_culprit",
|
|
193
|
-
commit:
|
|
232
|
+
commit: culprit,
|
|
194
233
|
...(runner !== undefined ? { runner } : {}),
|
|
195
234
|
failingTests: baseline.failingTests ?? [],
|
|
196
235
|
});
|
|
197
236
|
onCulprit?.({
|
|
198
|
-
commit:
|
|
237
|
+
commit: culprit,
|
|
199
238
|
failingTests: baseline.failingTests ?? [],
|
|
239
|
+
...(baseline.failingTestFiles ? { failingTestFiles: baseline.failingTestFiles } : {}),
|
|
200
240
|
tip: baseline.sha,
|
|
201
241
|
});
|
|
202
242
|
}
|
|
@@ -227,13 +267,20 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
227
267
|
// names the whole tip as the culprit, so the two red kinds are judged independently.
|
|
228
268
|
await watchRules(wt, previous, baseline.sha);
|
|
229
269
|
// A settled non-red verdict (a green, or a red naming only quarantined tests) clears the
|
|
230
|
-
// heal-dedup key, so a failing set that returns after a green main is healed again
|
|
231
|
-
//
|
|
232
|
-
// is red.
|
|
270
|
+
// heal-dedup key, so a failing set that returns after a green main is healed again, and
|
|
271
|
+
// drops the heal prompts still waiting for a red this tip has passed. A settled red
|
|
272
|
+
// verdict below does NOT clear it: the heal-dedup key stays set while main is red.
|
|
233
273
|
if (!red) {
|
|
234
274
|
clearHealKeyOnGreen();
|
|
275
|
+
await dropHealPromptsCoveredBy(root, wt, baseline.sha);
|
|
276
|
+
redSpellOpeners.delete(root); // a green closes the red spell; the next red names its own opener
|
|
235
277
|
return;
|
|
236
278
|
}
|
|
279
|
+
// The spell's opener is blamed once: the first settled red tip since the last green names
|
|
280
|
+
// itself, and a later tip re-settling the same open red re-checks the suite without re-
|
|
281
|
+
// naming the culprit (BUGS.md 2026-10-30 mis-attribution). The map holds the open spell's
|
|
282
|
+
// opener, so a second red while the spell is still open re-checks without re-naming; a
|
|
283
|
+
// green clears the entry (above) so a later red names its own opener.
|
|
237
284
|
reportCulprit(baseline);
|
|
238
285
|
});
|
|
239
286
|
}
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
* stage-check.ts). This module owns everything pollBudgetGate does from its `onFallback` seam
|
|
5
5
|
* through the end of the function: the seam/backends walk, the motion key (`resolvedKey`,
|
|
6
6
|
* `fromKey`), the fallback view derivation (`fallbackConfig`), `chainInfo`, and the `pairs`
|
|
7
|
-
* handback list. It no longer moves any tier for a used-up subscription window (part 5d/6):
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* handback list. It no longer moves any tier for a used-up subscription window (part 5d/6): a
|
|
8
|
+
* spent `maxDailyCostUsd` moves every priced tier to its free seam, and, at any spend, a tier
|
|
9
|
+
* whose own primary failed its model check moves to a verified step of its chain
|
|
10
|
+
* (plans/model-check.md "Fallback"). Its one entry reads the inputs pollBudgetGate's first half
|
|
11
|
+
* already resolved and returns the view fields `BudgetGatePoll` carries, so pollBudgetGate keeps
|
|
12
|
+
* its call signature and src/gates/gate-polls.ts's destructure of it is unchanged. The plan
|
|
13
|
+
* references the moved comments carry (part 5b/8, part 5c/8, 6a/7) hold here. */
|
|
13
14
|
import { MODEL_TIERS, } from "../config/config-schema.js";
|
|
14
15
|
import { modelPairName, pairDailyCost } from "../budget/budget.js";
|
|
15
16
|
import { fallbackServingPair, } from "../budget/fallback-breaker.js";
|
|
16
17
|
import { applyFallbackModel, tierChainSteps, } from "../config/config-fallbacks.js";
|
|
17
18
|
import { seamAgent } from "../config/config-model-selector.js";
|
|
18
|
-
import { withModelOverrides } from "../config/config-views.js";
|
|
19
|
+
import { seamPairName, withModelOverrides } from "../config/config-views.js";
|
|
19
20
|
import { piPairFree, seamFree } from "../pi/pi-models.js";
|
|
20
21
|
import { dayAt } from "../text/datetime.js";
|
|
21
22
|
/** Can the account not take extra usage: some window is rejected, or the newest non-reset
|
|
@@ -44,21 +45,41 @@ export function subscriptionBackendOf(x, accounts) {
|
|
|
44
45
|
const account = accounts(id);
|
|
45
46
|
return account?.billing === "subscription" ? id : null;
|
|
46
47
|
}
|
|
48
|
+
/** The pair a tier's own primary resolves to under the live config — the name the model check
|
|
49
|
+
* keys it by. */
|
|
50
|
+
function tierPrimaryPair(config, tier) {
|
|
51
|
+
return seamPairName(withModelOverrides(config, tier, {}));
|
|
52
|
+
}
|
|
53
|
+
/** `base` with each check-moved tier's entry replaced by its verified step. */
|
|
54
|
+
function withCheckMoves(base, checkResolved, checkMoved) {
|
|
55
|
+
const out = { ...base };
|
|
56
|
+
for (const tier of checkMoved)
|
|
57
|
+
out[tier] = checkResolved[tier];
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
47
60
|
/** Install `state.fallbackConfig` when the fallback changed the view key: at the cap, the free
|
|
48
61
|
* pair installed top-level and every per-role/reviewer model override dropped, so no seam can
|
|
49
62
|
* reach a priced model. A breaker-demoted fallback keeps the view too: its gate is `paused`,
|
|
50
63
|
* but a run parked in the semaphore when it tripped, the half-open probe, and the merges must
|
|
51
64
|
* still run on the free pair — a demotion must never promote them to the priced model the cap
|
|
52
|
-
* already spent.
|
|
53
|
-
|
|
65
|
+
* already spent. A tier the model check moved (deriveCheckMovedTiers) installs its verified
|
|
66
|
+
* step at any spend, and only its own primary seams move: every other tier, and a role or
|
|
67
|
+
* reviewer pinned to some other pair, keeps its model. */
|
|
68
|
+
function installFallbackConfig(a, fromKey, checkMoved) {
|
|
54
69
|
const { state, config: liveConfig, providers, catalog, accounts, priceResolved, reached } = a;
|
|
55
|
-
if (reached && (state.from !== liveConfig || state.fromKey !== fromKey)) {
|
|
70
|
+
if ((reached || checkMoved.size > 0) && (state.from !== liveConfig || state.fromKey !== fromKey)) {
|
|
56
71
|
state.from = liveConfig;
|
|
57
72
|
state.fromKey = fromKey;
|
|
73
|
+
const primaries = new Map([...checkMoved].map((t) => [t, tierPrimaryPair(liveConfig, t)]));
|
|
74
|
+
const checkKeeps = (seam, tier) => primaries.get(tier) !== seamPairName(seam);
|
|
58
75
|
// At the cap only free seams are available (a subscription claude seam is free money), so
|
|
59
76
|
// every role/reviewer override that resolves to a priced seam drops to the fallback pair.
|
|
60
|
-
const
|
|
61
|
-
|
|
77
|
+
const keeps = reached
|
|
78
|
+
? (seam, tier) => seamFree(providers, seam, accounts, catalog) && checkKeeps(seam, tier)
|
|
79
|
+
: checkKeeps;
|
|
80
|
+
// The cap's base view is the check-aware resolution while the check is on (a cap-moved
|
|
81
|
+
// tier's failed step stays out of the config its roles run under), else the price view.
|
|
82
|
+
state.fallbackConfig = applyFallbackModel(liveConfig, withCheckMoves(a.checkFreeResolved ?? priceResolved, a.checkResolved, checkMoved), keeps);
|
|
62
83
|
}
|
|
63
84
|
}
|
|
64
85
|
/** The tiers `applyFallbackModel` moved at the cap: those whose own seam fails the `available`
|
|
@@ -73,28 +94,64 @@ function deriveMovedTiers(a) {
|
|
|
73
94
|
}
|
|
74
95
|
return movedTiers;
|
|
75
96
|
}
|
|
97
|
+
/** The tiers the model check moves (plans/model-check.md "Fallback"): a tier still on its own
|
|
98
|
+
* primary — every tier below the cap, at it only one the cap leaves in place (`capMoved` holds
|
|
99
|
+
* the rest) — whose primary failed its check while the tier's verified-only resolution has a
|
|
100
|
+
* step to run on. A tier whose primary failed with no verified step stays held on it. */
|
|
101
|
+
function deriveCheckMovedTiers(a, capMoved) {
|
|
102
|
+
const moved = new Set();
|
|
103
|
+
if (a.checkResolved === undefined || a.checkFailed === undefined)
|
|
104
|
+
return moved;
|
|
105
|
+
for (const tier of MODEL_TIERS) {
|
|
106
|
+
if (capMoved.has(tier) || a.checkResolved[tier].pair === null)
|
|
107
|
+
continue;
|
|
108
|
+
if (a.checkFailed(tierPrimaryPair(a.config, tier)))
|
|
109
|
+
moved.add(tier);
|
|
110
|
+
}
|
|
111
|
+
return moved;
|
|
112
|
+
}
|
|
76
113
|
/** Derive the fallback view for one poll from the verdict half's already-resolved inputs
|
|
77
|
-
* (`priceResolved`, `servingResolved`, `
|
|
78
|
-
* the shared reads (`state`, `config`, `accounts`,
|
|
79
|
-
* cross-poll view state (`from`/`fromKey
|
|
80
|
-
*
|
|
81
|
-
*
|
|
114
|
+
* (`priceResolved`, `servingResolved`, `checkResolved`, `checkFreeResolved`, `chainPairs`,
|
|
115
|
+
* `pairNames`) and the `reached` flag and the shared reads (`state`, `config`, `accounts`,
|
|
116
|
+
* `providers`, `catalog`), writing the cross-poll view state (`from`/`fromKey`/
|
|
117
|
+
* `fallbackConfig`) and returning the view
|
|
118
|
+
* fields pollBudgetGate hands back. The view is installed at the cap, and below it while a tier
|
|
119
|
+
* the model check moved runs on its verified step; `onFallback` (the image-reroute regime) stays
|
|
120
|
+
* the cap's alone. The director always keeps the live config (part 5d/6): with the window move
|
|
121
|
+
* gone no input moves it, so `directorConfig` is the live config by construction. */
|
|
82
122
|
export function budgetFallbackView(a) {
|
|
83
123
|
const { state, config: liveConfig, reached, priceResolved } = a;
|
|
84
124
|
const onFallback = reached && a.pairNames.length > 0;
|
|
125
|
+
const movedTiers = deriveMovedTiers(a);
|
|
126
|
+
const checkMoved = deriveCheckMovedTiers(a, movedTiers);
|
|
85
127
|
// The resolved pairs are part of the key: a priced step crossing its own daily cap changes
|
|
86
|
-
// the resolution without a config or window change, and the view must follow it.
|
|
128
|
+
// the resolution without a config or window change, and the view must follow it. So are the
|
|
129
|
+
// model check's moves, appended only while there are some.
|
|
87
130
|
const resolvedKey = MODEL_TIERS.map((t) => {
|
|
88
131
|
const rp = priceResolved[t].pair;
|
|
89
132
|
return rp ? modelPairName(rp) : "-";
|
|
90
133
|
}).join(",");
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
const checkKey = MODEL_TIERS.map((t) => (checkMoved.has(t) ? modelPairName(a.checkResolved[t].pair) : "-")).join(",");
|
|
135
|
+
// The cap view's check-aware pairs follow the price pairs when present: a check verdict
|
|
136
|
+
// failing or clearing a step changes the installed config without a price change, so the
|
|
137
|
+
// view must re-install on the next poll. Absent while the check is off, so the key is
|
|
138
|
+
// unchanged then.
|
|
139
|
+
const checkFreeKey = a.checkFreeResolved
|
|
140
|
+
? "|" +
|
|
141
|
+
MODEL_TIERS.map((t) => {
|
|
142
|
+
const rp = a.checkFreeResolved[t].pair;
|
|
143
|
+
return rp ? modelPairName(rp) : "-";
|
|
144
|
+
}).join(",")
|
|
145
|
+
: "";
|
|
146
|
+
const fromKey = `${reached ? "c" : ""}|${resolvedKey}${checkMoved.size > 0 ? `|${checkKey}` : ""}${checkFreeKey}`;
|
|
147
|
+
installFallbackConfig(a, fromKey, checkMoved);
|
|
94
148
|
const chain = chainInfo({
|
|
95
149
|
config: liveConfig,
|
|
96
|
-
resolved: a.servingResolved,
|
|
97
|
-
moved: movedTiers,
|
|
150
|
+
resolved: withCheckMoves(a.servingResolved, a.checkResolved, checkMoved),
|
|
151
|
+
moved: new Set([...movedTiers, ...checkMoved]),
|
|
152
|
+
checkMoved,
|
|
153
|
+
...(a.checkFailed !== undefined ? { modelCheckFailed: a.checkFailed } : {}),
|
|
154
|
+
...(a.checkVerified !== undefined ? { modelCheckVerified: a.checkVerified } : {}),
|
|
98
155
|
breakers: state.breakers,
|
|
99
156
|
states: a.states,
|
|
100
157
|
now: a.now,
|
|
@@ -104,16 +161,20 @@ export function budgetFallbackView(a) {
|
|
|
104
161
|
return {
|
|
105
162
|
onFallback,
|
|
106
163
|
chain,
|
|
107
|
-
roleConfig: onFallback ? state.fallbackConfig : liveConfig,
|
|
164
|
+
roleConfig: onFallback || checkMoved.size > 0 ? state.fallbackConfig : liveConfig,
|
|
108
165
|
pairs: distinctPairs(priceResolved, a.chainPairs),
|
|
109
166
|
directorConfig: liveConfig,
|
|
167
|
+
checkMoved,
|
|
110
168
|
};
|
|
111
169
|
}
|
|
112
170
|
/** The `chain` block: for each tier this poll moved off its primary, the step it runs on and
|
|
113
171
|
* why the position before it cannot run (plans/fallback-chain.md "Events and surfaces").
|
|
114
172
|
* `moved` names the tiers the poll moved; a tier the gate pauses (`pair: null`) gets no entry.
|
|
115
173
|
* The reason is read from the position just before the active step — the previous chain step,
|
|
116
|
-
* or the tier's primary at step 0 — in the order demoted, step-cap, overage, cap.
|
|
174
|
+
* or the tier's primary at step 0 — in the order demoted, step-cap, overage, model-check, cap.
|
|
175
|
+
* `checkMoved` names the moved tiers whose primary failed its model check rather than the cap
|
|
176
|
+
* moving them; the model check's verdicts say whether a previous step failed (or, for such a
|
|
177
|
+
* tier, which moves only to verified steps, has not answered yet). */
|
|
117
178
|
export function chainInfo(a) {
|
|
118
179
|
const out = {};
|
|
119
180
|
const steps = tierChainSteps(a.config);
|
|
@@ -127,7 +188,7 @@ export function chainInfo(a) {
|
|
|
127
188
|
pair: modelPairName(r.pair),
|
|
128
189
|
step: position.step,
|
|
129
190
|
of: position.of,
|
|
130
|
-
...movedTierReason(a, position.prior, position.priorName, midnight),
|
|
191
|
+
...movedTierReason(a, tier, position.prior, position.priorName, midnight),
|
|
131
192
|
};
|
|
132
193
|
Object.assign(info, movedTierSpend(a, r.pair));
|
|
133
194
|
out[tier] = info;
|
|
@@ -146,10 +207,11 @@ function movedTierPosition(steps, tier, r) {
|
|
|
146
207
|
return { step: idx + 1, of: own.length, prior, priorName: prior ? modelPairName(prior) : "" };
|
|
147
208
|
}
|
|
148
209
|
/** Why the position before the active step cannot run, in INV-BN1's fixed order (demoted,
|
|
149
|
-
* step-cap, overage, cap). A demoted or
|
|
210
|
+
* step-cap, overage, model-check, cap). A demoted, overage or model-check result carries no
|
|
211
|
+
* `returnsAt` (a model check lifts when a re-check answers, not at a clock time); a step-cap or
|
|
150
212
|
* cap result pins it to `midnight` (INV-BN2: local midnight only for step-cap and cap).
|
|
151
213
|
* Answers "why can't the position before the active step run, and when does it come back?". */
|
|
152
|
-
function movedTierReason(a, prior, priorName, midnight) {
|
|
214
|
+
function movedTierReason(a, tier, prior, priorName, midnight) {
|
|
153
215
|
if (prior && a.breakers[priorName] !== undefined && !fallbackServingPair(a.breakers, priorName)) {
|
|
154
216
|
return { reason: "demoted" };
|
|
155
217
|
}
|
|
@@ -163,6 +225,12 @@ function movedTierReason(a, prior, priorName, midnight) {
|
|
|
163
225
|
claudeOverageRefused(a.accounts("claude"))) {
|
|
164
226
|
return { reason: "overage" };
|
|
165
227
|
}
|
|
228
|
+
const checkMoved = a.checkMoved?.has(tier) === true;
|
|
229
|
+
const priorChecked = prior === null
|
|
230
|
+
? checkMoved
|
|
231
|
+
: a.modelCheckFailed?.(priorName) === true || (checkMoved && a.modelCheckVerified?.(priorName) !== true);
|
|
232
|
+
if (priorChecked)
|
|
233
|
+
return { reason: "model-check" };
|
|
166
234
|
return { reason: "cap", returnsAt: midnight };
|
|
167
235
|
}
|
|
168
236
|
/** What the active step has spent against its own cap: both keys absent unless the step is a
|
|
@@ -119,9 +119,8 @@ export function detectBuildCheck(startDir, config, maxLevels = WALK_UP_LEVELS) {
|
|
|
119
119
|
/** The configured per-change gate command (`check.gateCommand`, PLANS.md Land-queue speed 3e) when
|
|
120
120
|
* it is a non-blank string, else undefined — off. Opt-in: when no `mergeCommand` is declared, a
|
|
121
121
|
* project that sets one merges on it, not the full check (mergeCommandOf's fallback, PLANS.md
|
|
122
|
-
* "Fast merge" "Defaults"). One reading
|
|
123
|
-
* "gate")
|
|
124
|
-
* full check's), so the two cannot disagree about whether it is on. */
|
|
122
|
+
* "Fast merge" "Defaults"). One reading every caller shares (runScopedBuildCheck runs it at
|
|
123
|
+
* scope "gate"), so none can disagree about whether it is on. */
|
|
125
124
|
export function gateCommandOf(config) {
|
|
126
125
|
const g = config?.check?.gateCommand;
|
|
127
126
|
return isNonBlankString(g) ? g : undefined;
|
|
@@ -141,6 +140,26 @@ export function mergeCommandOf(config) {
|
|
|
141
140
|
return gateCommandOf(config);
|
|
142
141
|
return isNonBlankString(m) ? m : undefined;
|
|
143
142
|
}
|
|
143
|
+
/** The npm script the re-run of named tests auto-detects (rerunCommandOf). */
|
|
144
|
+
const RERUN_SCRIPT = "test:rerun";
|
|
145
|
+
/** The command that re-runs only named tests (`check.rerunCommand`, build-check-rerun.ts), or
|
|
146
|
+
* undefined — off, and its callers run the full check instead. A configured non-blank string
|
|
147
|
+
* wins; a blank one (`""`) is the explicit opt-out. Absent, a project whose check is the npm
|
|
148
|
+
* walk-up (no `check.command`) re-runs with `npm run test:rerun --` when the package.json the
|
|
149
|
+
* walk-up finds declares that script, so an npm project opts in from its own manifest. Never
|
|
150
|
+
* throws. */
|
|
151
|
+
export function rerunCommandOf(startDir, config) {
|
|
152
|
+
const r = config?.check?.rerunCommand;
|
|
153
|
+
if (r !== undefined)
|
|
154
|
+
return isNonBlankString(r) ? r : undefined;
|
|
155
|
+
if (isNonBlankString(config?.check?.command))
|
|
156
|
+
return undefined;
|
|
157
|
+
const root = walkUp(startDir, WALK_UP_LEVELS, (dir) => (hasInstall(dir) ? dir : null));
|
|
158
|
+
if (root === null)
|
|
159
|
+
return undefined;
|
|
160
|
+
const scripts = readJsonFile(path.join(root, "package.json"))?.scripts;
|
|
161
|
+
return isJsonObject(scripts) && isCheckScript(scripts, RERUN_SCRIPT) ? `npm run ${RERUN_SCRIPT} --` : undefined;
|
|
162
|
+
}
|
|
144
163
|
/** Resolve `node_modules/<rel>` by walking UP from `startDir` — the same climb detectBuildCheck
|
|
145
164
|
* makes, and the same one npm's run-script PATH walk makes: a tumwater worktree has no install
|
|
146
165
|
* of its own (node_modules is gitignored, so it exists only where someone ran npm install), and
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* runScopedBuildCheck (build-check-scoped.ts) is the only caller that both runs a check and
|
|
9
9
|
* uses this wording; the baseline surfaces use the event/warning helpers alone. */
|
|
10
10
|
import { logEventBestEffort } from "../events/events.js";
|
|
11
|
+
import { hostLoadStarved } from "./host-sleep.js";
|
|
11
12
|
/** Does this build_check scope stand for a full-suite verdict — merge, batch, or the red-main
|
|
12
13
|
* baseline? A gate check can run a subset of the tree, so a scope outside this set is never a
|
|
13
14
|
* full-suite check. The one vocabulary the three predicates that recognize a full-suite check
|
|
@@ -143,16 +144,36 @@ const STARVED_DEADLINE_MAX_FACTOR = 2;
|
|
|
143
144
|
* firing on time under the team's own load, with a passing tail at 299.97 s — the check was
|
|
144
145
|
* measuring the load, not the tree (BUGS.md 2026-10-12). */
|
|
145
146
|
export function starvedDeadlineMs(baseMs, load) {
|
|
146
|
-
if (!(load
|
|
147
|
+
if (!hostLoadStarved(load))
|
|
147
148
|
return baseMs;
|
|
148
149
|
return Math.round(baseMs * Math.min(load.load1 / load.cpus, STARVED_DEADLINE_MAX_FACTOR));
|
|
149
150
|
}
|
|
151
|
+
/** The reviewed diff's size against which the post-merge reviewer's wall stretches. Calibrated
|
|
152
|
+
* to the class this exists for: the largest-diff member (BUGS-FIXED.md "post-merge review of
|
|
153
|
+
* 3b682bed …", a 270/247-line split, ~41 KB of diff) timed out 21× at the 900 s wall even after
|
|
154
|
+
* the load-only stretch landed, so a diff of that size must already sit past the reference and
|
|
155
|
+
* earn the 2× factor. It is deliberately NOT the reviewer's own `DIFF_MAX_BYTES` diff cap
|
|
156
|
+
* (src/git/git-diff.ts, 200 KB): that cap bounds the prompt's text, not how many turns a
|
|
157
|
+
* strong-tier review of the diff needs, and it sits well above every diff that has timed out,
|
|
158
|
+
* so referencing it would stretch none of the class's members. */
|
|
159
|
+
export const REVIEW_DIFF_REFERENCE_BYTES = 20_000;
|
|
160
|
+
/** The budget the post-merge reviewer's wall gets for the reviewed diff's size: `baseMs` at or
|
|
161
|
+
* under REVIEW_DIFF_REFERENCE_BYTES (see its calibration), and above it the overage ratio capped
|
|
162
|
+
* at STARVED_DEADLINE_MAX_FACTOR — the same cap as the load stretch, so load and diff each
|
|
163
|
+
* contribute at most 2× and a wedged review's quiet-watchdog wall is untouched (it is
|
|
164
|
+
* independent of the run budget). A review of a large diff needs the diff's proportionally more
|
|
165
|
+
* turns, so a wall that ignores its size kills a progressing review before its VERDICT. */
|
|
166
|
+
export function diffScaledMs(baseMs, diffBytes) {
|
|
167
|
+
if (!(diffBytes > REVIEW_DIFF_REFERENCE_BYTES))
|
|
168
|
+
return baseMs;
|
|
169
|
+
return Math.round(baseMs * Math.min(diffBytes / REVIEW_DIFF_REFERENCE_BYTES, STARVED_DEADLINE_MAX_FACTOR));
|
|
170
|
+
}
|
|
150
171
|
/** The note an on-time deadline kill carries when the host was starved: the load average
|
|
151
172
|
* beyond the core count means the check shared the machine with the rest of the team, so the
|
|
152
173
|
* 300 s kill is a statement about the deadline under load, not a measured verdict on the
|
|
153
174
|
* change (BUGS.md 2026-10-12). Empty when the host had a core to spare. */
|
|
154
175
|
export function loadedHostPhrase(load) {
|
|
155
|
-
if (!(load
|
|
176
|
+
if (!hostLoadStarved(load))
|
|
156
177
|
return "";
|
|
157
178
|
return ` (host load average ${Math.round(load.load1)} on ${load.cpus} cores: the check ran starved, so the kill says nothing about the change itself)`;
|
|
158
179
|
}
|
|
@@ -187,9 +208,8 @@ export function installFailedPhrase(label, install, proceeding) {
|
|
|
187
208
|
/** Did this outcome make no verdict about the tree? runScopedBuildCheck records that verdict-
|
|
188
209
|
* less shape explicitly (`unverified: true`) at the merge scopes; a gate-scope failure carries
|
|
189
210
|
* only the run's own sleep evidence (BuildCheckRun.sleptMs), so both spellings count. The
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* cannot drift between them (BUGS.md 2026-09-30). */
|
|
211
|
+
* consumer that attributes a red — the review gate (review-precheck.ts) — asks this instead of
|
|
212
|
+
* re-deriving the evidence, so the attribution policy cannot drift (BUGS.md 2026-09-30). */
|
|
193
213
|
export function unverifiedTreeOutcome(outcome) {
|
|
194
214
|
return outcome.unverified === true || (outcome.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
|
|
195
215
|
}
|
|
@@ -47,7 +47,7 @@ export function parseFailingTests(output) {
|
|
|
47
47
|
const line = raw.trim();
|
|
48
48
|
if (!FAILED_TEST_LINE.test(line))
|
|
49
49
|
continue;
|
|
50
|
-
const title = line
|
|
50
|
+
const title = resultTitle(line);
|
|
51
51
|
if (title !== "")
|
|
52
52
|
titles.add(title);
|
|
53
53
|
if (titles.size >= FAILING_TESTS_MAX)
|
|
@@ -55,6 +55,43 @@ export function parseFailingTests(output) {
|
|
|
55
55
|
}
|
|
56
56
|
return [...titles];
|
|
57
57
|
}
|
|
58
|
+
/** A failed test's line title: the `✖ ` mark and the trailing duration stripped. */
|
|
59
|
+
function resultTitle(line) {
|
|
60
|
+
return line.slice(2).replace(/ \([\d.]+m?s\)$/, "").trim();
|
|
61
|
+
}
|
|
62
|
+
/** A failure's location line in node:test's `✖ failing tests:` section, printed right above the
|
|
63
|
+
* failed test's `✖` line: `test at <file>:<line>:<col>`. */
|
|
64
|
+
const FAILED_TEST_AT = /^test at (.+):\d+:\d+$/;
|
|
65
|
+
/** The file each failing test lives in, keyed by the title parseFailingTests reports, read from the
|
|
66
|
+
* `test at <file>:<line>:<col>` line node:test's `✖ failing tests:` section prints above each
|
|
67
|
+
* failure. A file is named by its base name without the extension (`merger.test` for
|
|
68
|
+
* dist/test/merger.test.js): the form a re-run's `<file>#<name>` filter takes, the same in
|
|
69
|
+
* every worktree. A title whose location the output does not print is absent. */
|
|
70
|
+
export function parseFailingTestFiles(output) {
|
|
71
|
+
const files = new Map();
|
|
72
|
+
let at;
|
|
73
|
+
for (const raw of output.split("\n")) {
|
|
74
|
+
const line = raw.trim();
|
|
75
|
+
const location = FAILED_TEST_AT.exec(line);
|
|
76
|
+
if (location) {
|
|
77
|
+
at = location[1];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (at !== undefined && FAILED_TEST_LINE.test(line)) {
|
|
81
|
+
const title = resultTitle(line);
|
|
82
|
+
if (title !== "" && !files.has(title))
|
|
83
|
+
files.set(title, fileStem(at));
|
|
84
|
+
}
|
|
85
|
+
at = undefined;
|
|
86
|
+
}
|
|
87
|
+
return Object.fromEntries(files);
|
|
88
|
+
}
|
|
89
|
+
/** A test file's base name without its extension, from a path or a `file://` URL. */
|
|
90
|
+
function fileStem(file) {
|
|
91
|
+
const base = file.replace(/^file:\/\//, "").split(/[\\/]/).pop() ?? "";
|
|
92
|
+
const dot = base.lastIndexOf(".");
|
|
93
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
94
|
+
}
|
|
58
95
|
/** Keep the TAIL of a build's combined output: last ≤10 meaningful lines, each via clipReason —
|
|
59
96
|
* so a chatty build cannot bloat persisted state or the injected next-run note. Blank lines
|
|
60
97
|
* and npm's own script banner (`> pkg@1.0 script`, `> <command>`) are dropped: they name the
|
|
@@ -127,10 +164,9 @@ export function describeCheck(check) {
|
|
|
127
164
|
}
|
|
128
165
|
/** The single-line headline of a red check — the failureHeadline when the tail has one to
|
|
129
166
|
* offer (a test name or marker plus a non-framing message), else the check's describeCheck
|
|
130
|
-
* command name — the name the gate
|
|
167
|
+
* command name — the name the gate and review warnings print for a failed check.
|
|
131
168
|
* One definition for every red that names itself to a human: review-precheck's two
|
|
132
|
-
* flake/failure warnings
|
|
133
|
-
* merge-check-failures's kept-pin headline record and attributeRedCheck absent-tail headline.
|
|
169
|
+
* flake/failure warnings.
|
|
134
170
|
*/
|
|
135
171
|
export function redCheckHeadline(check, tail) {
|
|
136
172
|
return failureHeadline(tail) ?? describeCheck(check);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Re-running only named failing tests (`check.rerunCommand`, rerunCommandOf), instead of the whole
|
|
2
|
+
* check: the main watch confirms a red tip with it, and the review gate asks it whether a change's
|
|
3
|
+
* failing tests also fail on main. The full check runs at most once per main commit — the watch's
|
|
4
|
+
* own run — and every later question about the same failures is answered in seconds by running
|
|
5
|
+
* just those tests. Split out of build-check.ts, which keeps running and classifying the declared
|
|
6
|
+
* check: this module only builds the narrower command and reads back which named tests still
|
|
7
|
+
* fail. */
|
|
8
|
+
import { runBuildCheck } from "./build-check.js";
|
|
9
|
+
import { BUILD_CHECK_TIMEOUT_MS, detectBuildCheck, rerunCommandOf } from "./build-check-detect.js";
|
|
10
|
+
import { FAILING_TESTS_MAX } from "./build-check-report.js";
|
|
11
|
+
import { buildCheckEvent } from "./build-check-events.js";
|
|
12
|
+
import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
|
|
13
|
+
import { logEventBestEffort } from "../events/events.js";
|
|
14
|
+
import { shellQuote } from "../agent/write-guard.js";
|
|
15
|
+
/** The re-run's filter arguments, one per test: `<file>#<name>`, with the file's base name from
|
|
16
|
+
* the failure output (BuildCheckOutcome.failingTestFiles). When any test's file is unknown every
|
|
17
|
+
* argument is a bare `#<name>`: a runner that selects files by the named ones would otherwise
|
|
18
|
+
* never load the unlocated test. */
|
|
19
|
+
function rerunFilters(tests, files) {
|
|
20
|
+
const located = tests.every((t) => files?.[t] !== undefined);
|
|
21
|
+
return tests.map((t) => `${located ? files?.[t] : ""}#${t}`);
|
|
22
|
+
}
|
|
23
|
+
/** The shell command that re-runs `tests`: `command` followed by each filter, single-quoted. The
|
|
24
|
+
* line the harness runs, and the one a heal prompt hands an agent to reproduce the failures. */
|
|
25
|
+
export function rerunCommandLine(command, tests, files) {
|
|
26
|
+
return [command, ...rerunFilters(tests, files).map(shellQuote)].join(" ");
|
|
27
|
+
}
|
|
28
|
+
/** Can `tests` be re-run on their own: a re-run command is in effect at `wt` and the list is
|
|
29
|
+
* complete — non-empty and short of FAILING_TESTS_MAX, which may have been cut short. */
|
|
30
|
+
export function canRerun(wt, config, tests) {
|
|
31
|
+
return tests.length > 0 && tests.length < FAILING_TESTS_MAX && rerunCommandOf(wt, config) !== undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Re-run only `tests` in `wt` under the process-wide check permit, logged as a `rerun`-scope
|
|
34
|
+
* build_check event under `source`. Null when no verdict came of it: no re-run command, an
|
|
35
|
+
* incomplete list (canRerun), a skipped run (timeout, kill, broken toolchain, a host sleep), or a
|
|
36
|
+
* failure that names none of `tests` — a compile error, a test file that would not load, or a
|
|
37
|
+
* filter that matched nothing. Never throws. */
|
|
38
|
+
export async function rerunTests(root, source, wt, config, tests, files, tier = CHECK_TIER.other) {
|
|
39
|
+
const command = canRerun(wt, config, tests) ? rerunCommandOf(wt, config) : undefined;
|
|
40
|
+
if (command === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
const check = detectBuildCheck(wt, { ...config, check: { ...config?.check, command: rerunCommandLine(command, tests, files) } });
|
|
43
|
+
if (check === null)
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
const outcome = await withCheckPermit(config, tier, () => runBuildCheck(wt, check, BUILD_CHECK_TIMEOUT_MS), undefined, `${source} rerun`);
|
|
48
|
+
logEventBestEffort(root, buildCheckEvent(source, "rerun", outcome, Date.now() - started));
|
|
49
|
+
if (outcome.status === "passed")
|
|
50
|
+
return { failing: [] };
|
|
51
|
+
if (outcome.status !== "failed")
|
|
52
|
+
return null;
|
|
53
|
+
// A failure naming none of the tests asked about (a test file that would not load names
|
|
54
|
+
// itself) says nothing about them.
|
|
55
|
+
const named = outcome.failingTests ?? [];
|
|
56
|
+
const failing = tests.filter((t) => named.includes(t));
|
|
57
|
+
return failing.length > 0 ? { failing } : null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -50,7 +50,16 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
|
|
|
50
50
|
waiting: () => permitWait?.waiting?.(),
|
|
51
51
|
granted: () => permitWait?.granted?.(),
|
|
52
52
|
waited: (waitedMs, parked) => logCheckWaitEvent(root, role, scope, waitedMs, parked),
|
|
53
|
-
}, `${role} ${scope}
|
|
53
|
+
}, `${role} ${scope}`,
|
|
54
|
+
// A check whose caller was stopped (a user abort, a lapsed restart hand-off, a shutdown) while
|
|
55
|
+
// it waited for a permit settles the way a mid-run abort does, without spawning: otherwise
|
|
56
|
+
// the grant would run the whole suite for a merge that no longer exists, holding a permit
|
|
57
|
+
// other checks need. A check granted a free permit under a fired signal still runs (see
|
|
58
|
+
// PermitWaitAbort).
|
|
59
|
+
{
|
|
60
|
+
signal,
|
|
61
|
+
outcome: () => ({ status: "skipped", script: checkScriptName(runCheck), skipReason: "aborted" }),
|
|
62
|
+
});
|
|
54
63
|
return { raw, durationMs };
|
|
55
64
|
}
|
|
56
65
|
/** Classify the raw outcome: the merge-scope skip and slept-through rules become a deterministic
|
|
@@ -163,7 +172,9 @@ function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, un
|
|
|
163
172
|
* `check.mergeCommand` when set, else `check.gateCommand`, else the full check — replaces it at the
|
|
164
173
|
* merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
|
|
165
174
|
* detects on its own, keeps running check.command. `permitWait` hears
|
|
166
|
-
* about a wait for the check permit (the merge cell's "waiting for a check slot").
|
|
175
|
+
* about a wait for the check permit (the merge cell's "waiting for a check slot"). `signal` firing
|
|
176
|
+
* mid-run stops the check's tree, and firing while the check waits for its permit ends the wait
|
|
177
|
+
* unspawned — both settle `skipped` with skipReason "aborted". Never throws. */
|
|
167
178
|
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad, signal) {
|
|
168
179
|
const r = resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad);
|
|
169
180
|
if (!r)
|