docs(stall-guard): state the boundary of a green budget line, and what the guard never sees - #12960
Conversation
The stall guard's budget depends on p+s (job prep plus how far into the guarded step output froze). check-stall-guard-budget judges C<T and T-C>=W, all statically readable; p+s is bounded by the healthy run length, which no static sweep can read, so it was carried in prose and had already gone stale. This is the re-derivation, so the number can rot loudly instead of silently. It imports the gate's own sweep rather than keeping a second list of guarded step names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
…t the guard never sees Completes the documented-acceptance half of #12846, as triage scoped it. The gate's header carried a bound that had already gone stale: it quoted an undeferred margin measured on a single run, and re-measurement found that margin had shrunk by roughly a quarter in days. Replaced with the four things a green line does NOT promise -- where the verdict lands, that the undeferred path is covered (with the direction of travel, which is the point), that the deferred path is not and no static gate can read why, and the two wedge classes the guard never sees at all. No figure is embedded as a constant. The header cites the method and the run IDs instead, and points at the re-runnable measurement so the next reader re-derives rather than trusts. No timeout-minutes, cap or window is changed by this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
|
ACCEPT — armed ( Entry qualification: every check. 32 runs at What this PR is, and what it deliberately is notOption 3 only — the boundary of a green line, written where a reader will find it. ⛔ No ⛔ My dispatch brief for this card was wrong and is corrected on #12846 (comment The measurement that makes the header worth readingThe guard fires on silence, not elapsed time: ⇒ Two wedge classes the guard never sees: one that writes while wedged, and one in an unguarded step (16 of the And the margin the card recorded as comfortable is nearly gone: worst The header sentence that earns its place:
⭐ Triage's data rule is honoured better than a number could. It ruled ⛔ "不要把 Population correction also landed: 7 guard-wrapped steps across 5 jobs, not 4 jobs, with
|
Part of #12846 — delivers option 3 (documented acceptance) as triage scoped it. Options 1, 2 and a fourth found during measurement are handed to the maintainer and deliberately not built here.
⛔ No
timeout-minutes, cap or stall window is changed by this PR.What landed
The gate header carried a bound measured on one run, and re-measurement found it had already gone stale in the direction that consumes budget. That paragraph is replaced with a four-item statement of what a green line does not promise, in
scripts/check-stall-guard-budget.mjs:p + s + C; room forp + sisT - C, on the ci.yml family30 - 20 = 10minutes.Test Corestalls mid-suite with frozen log output — three occurrences in one day, each costing a manual diagnosis + rerun #4250 shape, so a reader does not conclude the whole thing is uncovered. But the margin is now barely over a minute, having shrunk roughly a quarter in days while nothing in the tree changed and the gate stayed green throughout.testjob's 17 steps). No value ofTcloses either.Item 2 is the sentence that makes item 3 matter: a margin that moved 24% while nobody could see it move is the argument for why the invisible term is dangerous.
Also corrected where the header states the population: 7 guard-wrapped steps across 5 jobs, not 4 jobs —
temporal-conformancecarries two on one job clock, so the second step'spincludes the first's entire runtime.scripts/run-with-stall-guard.mjsgets the same two limits stated where it describes its own kill paths, pointing at the gate header rather than repeating the argument.No number is embedded as a constant
Triage's data rule is binding and the reason is on display in this very PR: the figure being replaced had rotted ~24%. The header cites the method and the run IDs instead —
readings from merge_group runs
33160601033,33162164422,33163163494; the card's original sample was33135187774.That tool enumerates guarded steps by importing the gate's own sweep, so a renamed step surfaces loudly instead of silently dropping out, and it refuses on an empty population rather than printing a green. Its header was trimmed in this commit to stop duplicating the argument that now lives in the gate header — two copies that must agree with nobody holding them to it is the defect class this repo dislikes most.
Handed up, not built
timeout-minutes) — fleet spend, and measurement shows its stated cost is real, not illusory: the guard cannot bound the two wedge classes above, sodelta-Tis billed one-for-one there.Verification
Run at
13c72bf5e9, this branch's head, after syncing withmain(the first derivation printed STALE TREE and was re-taken).Gate families derived from the final diff (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack), all exit 0, each captured before any pipe:check:agent-test-spelling,check:bash32-floor,check:cli-command-ids,check:cross-package-test-inputs,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,check:stall-guard,check:stall-guard-budget,check:watch-hint-literal,check-ci-filter-parity.mjs,check-cross-package-test-inputs.mjs. The two bold ones appeared only once this diff touched the guard scripts themselves.Run because a human would, not because the derivation named them:
check:nul-bytes(exit 0), the measurement tool's self-test (13 assertions, exit 0), and eslint over the changed files.Those greens are measurements, not silences —
check:stall-guardran the guard's own 41-case self-test andcheck:stall-guard-budgetruns--self-test(40 assertions, both violation tiers driven red, empty population proven to REFUSE) and then the real sweep. Since this diff edits the file the gate parses for its defaults, the assertion that matters is the one asserting those defaults are read rather than copied — it passed, so the new header prose is not being mistaken for a declaration.eslint narrowing, with the evidence a narrowing needs: the population is read from the config itself, which states it "never enables type-aware linting (no
parserOptions.project, no typed@typescript-eslintrules) for ANY file" — so this diff cannot move a verdict on an untouched file;--format jsonreports exactly 3 files linted, matching the 3 changed; 0 errors, 0 warnings, exit 0.skip-changeset:scripts/matches nopnpm-workspace.yamlglob and the root package isprivate: true, so this diff publishes nothing.Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw