Skip to content

fix(scripts): name the shared job clock in the stall-guard budget census - #13119

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-12959-stall-guard-sibling-slack
Aug 29, 2026
Merged

fix(scripts): name the shared job clock in the stall-guard budget census#13119
os-elon merged 1 commit into
mainfrom
claude/issue-12959-stall-guard-sibling-slack

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#12959

The census prints one slack per guard-wrapped site, but timeout-minutes runs one clock per job. Two guarded steps in the same job therefore do not each get the slack printed beside them — the later one starts with the earlier one's entire runtime already spent — and nothing in the line said so.

Census wording only. The criterion T - C >= W does not move, and no p/s modelling is introduced: the gate's header already explains why it refuses to model them. Every ablation below re-confirms the verdict path stays exit 0.

What the census prints now

ci.yml:969 temporal-conformance / Run driver-sql suite ...
... budget 30m (job timeout-minutes) · slack 10m (1 of 2 guarded steps in this job; they share one timeout clock)
ci.yml:1009 temporal-conformance / Run the non-SQL temporal backends ...
... budget 30m (job timeout-minutes) · slack 10m (2 of 2 guarded steps in this job; they share one timeout clock)

Four of the seven rows gain the clause; the other three are unchanged. The consequence clause is the load-bearing half — 1 of 2 alone leaves a reader still not knowing what it means.

Measured: the brief's population assumption did not survive

The brief assumed temporal-conformance was the only job holding two guarded steps. Re-running --list on today's tree says otherwise:

jobfileguarded stepsbudget
temporal-conformanceci.yml230m
rerun-safetyrerun-safety-nightly.yml2120m
test, dogfood, coverageci.yml, coverage-nightly.yml1 each30m

rerun-safety runs Test suite — pass 1 and Test suite — pass 2 (same working tree) back to back on one 120m clock — the sharing is at its most extreme there, since the second site's p is an entire preceding suite run. So the population is two sibling jobs, and the wording had to be right at N greater than 2 as well; a fixture pins 3 of 3.

The card's five slack 10m ci.yml-family rows do still hold verbatim, at the same line numbers. The card's runner-timestamp figures for p/s were not re-measurable from this seat (repo-scoped REST is 403 here) and are not load-bearing for a wording-only change.

The header paragraph describing the population named only temporal-conformance and still claimed the census "prints each site's slack independently". Both halves were stale in the same defect class as the census line itself, so they are corrected in the same edit rather than left contradicting the output two screens below.

The grouping key, and the two wrong derivations pinned against

Sibling grouping is derived from job membership, keyed on (file, job id). Each half is load-bearing:

  • Not the job id alone. A job id is unique only within its own file, and this tree reuses three of them across files today: publish (docker-publish.yml, release.yml), patrol (half-state-patrol.yml, release-coverage-patrol.yml), registry-canary (publish-smoke.yml, scaffold-e2e.yml). None currently holds a guarded step, so this mistake would be green on the live tree while inventing a shared clock that does not exist.
  • Not the file, and not a matching --stall-minutes value. One file holds many independent job clocks; five of the seven real sites are 10m across four different jobs.

The ordinal is trustworthy because a site's position within its job comes from job.steps — an array — so document order is guaranteed by construction, not by object-key iteration order. 2 of 2 really is the later step on the clock.

Verification — all on e05512ed6b, the final commit

node scripts/check-stall-guard-budget.mjs --self-test61 assertions, exit 0 (was 47). Includes triage's acceptance requirement as a test, not a glance: a job with a single guarded step must show no sibling note.

Ablations. Count predicted before each run; all three restored and byte-verified against the HEAD blob (git diff HEAD empty and git hash-object equal to HEAD:scripts/check-stall-guard-budget.mjs). Each mutation was confirmed on disk by counting the injected marker and the deleted anchor separately — a zero-hit edit exits 0 and would otherwise read as a passing ablation. No rebuild leg applies: this script is run from source by node, there is no dist/ between the edit and the run.

ablationpredictedobservedverdict path
sibling note always empty6 failures6still exit 0
group by job id alone1 failure1still exit 0
group by matching --stall-minutes7 failures7still exit 0

The three negative controls stayed green under ablation 1 — they assert absence of the note, so they cannot be the thing proving it exists; the six that failed are the ones that assert it does.

Gate family, derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on this commit (not a stale tree — it reported the same sha), all exit 0:

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-budget · check:watch-hint-literal · check-ci-filter-parity · check-cross-package-test-inputs · check-shard-attestation · check:nul-bytes

Both convention-triggered obligations for editing a gate script, exit 0: scripts/pm/bare-root-worklist.mjs --self-test (47 live rows, none stale/missing/contradicted) and pnpm check:pm-dispatch-gates (872 cases).

Two things the derivation did not name and a human would still run:

  • node scripts/measure-stall-guard-headroom.mjs --self-test — exit 0, 13 assertions. It is the one machine consumer of this file, importing scan and guardDefaults; it reads site fields, so the added fields are additive to it.
  • node scripts/check-test-completeness.mjs was in the derived list but is not measured here: bare, it exits 1 on a usage line because it takes a turbo test log argument (CI passes one). The unmodified script does the same, so it is a wrong invocation, not a finding.

No pin on the census wording exists to break: no test file, snapshot, or doc quotes the line format — checked before changing it.

Lint — declared narrowing, not a skipped run. Repo-wide eslint is CI's run. Narrowed to the touched file, with the three things that make the narrowing a measurement: (1) the population came from eslint's own config, which accepted the file rather than ignoring it; (2) --format json reports 1 file linted, 0 errors, 0 warnings; (3) eslint.config enables no type-aware linting anywhere (no parserOptions.project, no typed @typescript-eslint rules, its own words), so a single-file diff cannot move the verdict on any untouched file.

Changeset: deliberately none, derived

skip-changeset is applied. Derived rather than asserted: no non-private package's files whitelist names scripts/, and the root manifest is private: true, so a scripts/-only diff publishes nothing.

Deliberate boundaries

  • The violation renderer is untouched. It is a different block from the census and prints no slack; the card asks for the census. Widening it would add output the card did not ask for.
  • The note is unconditional on the budget source. A job's timeout clock is per-job whether declared or GitHub's default, so the clause is true either way; when a tighter step-level timeout-minutes is what binds, the census already says so in the budget ...m (step timeout-minutes) field printed immediately before the clause.

Generated by Claude Code


Generated by Claude Code

The census prints one `slack` per guard-wrapped site, but `timeout-minutes`
runs one clock per JOB. Two guarded steps in the same job therefore do not
each get the slack printed beside them -- the later one starts with the
earlier one's entire runtime already spent -- and nothing in the line said so.
Sibling grouping is derived from job membership, keyed on (file, job id):
a job id is unique only within its file, and this tree reuses three of them
across files today. Grouping by file, or by a matching --stall-minutes value,
would invent shared clocks that do not exist; the self-test pins both.
Census wording only. The criterion `T - C >= W` does not move.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stall-guard budget census prints the same slack for two guarded steps that share one job clock, so the later step's smaller real headroom is invisible

2 participants

@os-elon@claude