Skip to content

usage-gate: hold the crons 5% BEHIND pace; retire USAGE_SLACK_PCT (refused, not read) - #159

Merged
thedavidmeister merged 2 commits into
mainfrom
2026-07-30-issue-158-pace-headroom
Jul 30, 2026
Merged

usage-gate: hold the crons 5% BEHIND pace; retire USAGE_SLACK_PCT (refused, not read)#159
thedavidmeister merged 2 commits into
mainfrom
2026-07-30-issue-158-pace-headroom

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #158

The semantic flip

The pace check inverts direction. Before: a tick ran until usage was more than 5 points AHEAD of the linear burn line (used - linear > slack paused). Now: a tick runs only while usage is at least ~5 points BEHIND it (used - linear > -headroom, i.e. used > linear - headroom, pauses). The budget is shared, and the old gate let the deferrable consumer — the cron, which self-heals and re-ticks every 4h — spend right up to the pace line, so a day of ordinary interactive work started above the pause threshold the crons themselves had saturated (the nine consecutive skipped ticks of 2026-07-29/30, which left three newly-merged vetter gates unexercised for over a day). The crons now keep a standing band of budget under the pace line for interactive/BAU work.

Everything else in the verdict is untouched: the 90% ceiling check (still first, still >=), the reset-passed branch, the no-reset-known branch, and the inert path when no reading exists. The boundary is still an allowance: at used == linear - headroom exactly the tick runs; one point past pauses. Log lines now read the decision that is actually being made: PAUSE: … — inside the 5% BAU headroom / OK: … — at least 5% behind pace.

Early-week consequence, stated plainly

At the start of each weekly window linear < headroom, so the pause band's floor is below zero and the crons idle — even at 0% used — until the pace line clears the headroom (~8.4 hours in at the default 5). That is intended, not an underflow to special-case away: the first points of every week belong to interactive work, and the pipeline catches up on later ticks. It is asserted by name in early_week_idles_until_linear_clears_headroom. Symmetrically, near the reset the pace bound approaches 100 - headroom = 95, and with the default 90% ceiling it is the ceiling that binds at week's end — asserted in near_reset_pace_bound_approaches_100_minus_headroom.

The knob rename, fail-closed

USAGE_HEADROOM_PCT (default 5, positive = points the crons stay behind pace) replaces USAGE_SLACK_PCT. The old var, if set, is REFUSED, never read:

  • Mechanism: stale_slack_refusal (pure, env-as-parameter per the repo's run_record_path_from convention) returns a typed UsageVerdict::Refuse whose message names the replacement and echoes the refused value. usage-gate prints it to stderr and exits 2 — the binary-wide config-error code (bad clap args, unreadable --covered-file, and the gc missing-work-dir refusal all exit 2). The check runs before the usage endpoint is even fetched: ambiguous config must not run, pause, or spend anything.
  • Why exit 2 cannot read as "run" or "pause": the runners branch on exactly two codes — 10 logs the reason and exits 0 (pause), and 0 falls through to launch the model. Before this PR any other code fell through to run, so the refusal alone would have been read as "run"; both runners now abort the tick on any exit that is neither 0 nor 10, log usage-gate refused its config (exit N) — fix cron.env, and propagate the non-zero code so cron surfaces the failure. Not exit 10, which means "pause, all is well" and would let a stale cron.env silently disable the pipeline forever with a healthy-looking log; not exit 0, which means run.
  • Why the old name still appears in the runners' export list: bash only exports names cron.env actually set, so dropping USAGE_SLACK_PCT from the export line would hide a stale cron.env from the guard — the exact silent-ignore the issue forbids. It stays exported, commented as retired, so a set value reaches the binary and refuses.
  • One judgment call: a trimmed-empty USAGE_SLACK_PCT counts as unset (no refusal), following the repo-wide env-as-parameter convention that an empty value is how an exported-but-unassigned name arrives — an empty assignment carries no limit to invert or drop. Asserted in the refusal test.
  • A negative USAGE_HEADROOM_PCT is arithmetically meaningful (it reproduces old-style ahead-of-pace slack) and is not refused: the sign is part of the new name's documented meaning ("positive = behind"), unlike the old name, whose sign silently inverted.

Mutation results

Each mutant applied alone against the full usage_gate test set; all killed:

mutation killed by
pace >>= (boundary trips) pace_boundary_runs_at_exactly_headroom_behind_and_pauses_one_point_past
-headroomheadroom (sign flip back to the base semantics) same, plus early_week_idles_until_linear_clears_headroom
used - linearlinear - used boundary test (45.0 would pause)
pause/run arms swapped (condition negated) every pace test
ceiling >=> ceiling_pauses_at_the_boundary_not_only_over_it (code and reason)
reset >=> reset_already_passed_runs_as_a_new_week (reason pins the branch)
Refuse exit 2 → 0 stale_slack_var_refuses_loudly_naming_the_new_knob
Refuse exit 2 → 10 same
refusal fires on empty value same (empty/blank must not refuse)
refusal never fires same
linear_pct lower clamp dropped linear_pct_clamps_at_both_ends
ceiling check moved below pace ceiling_is_checked_before_pace_and_wins

On the #157 lesson (adjacent gates with disjoint trigger states make swaps invisible): the ceiling and pace checks here DO overlap, and ceiling_is_checked_before_pace_and_wins now includes an input in both trigger states (95 used, 50 linear: over the ceiling AND inside the headroom) asserting the ceiling's reason wins — so an if-swap is visible on a shared input, not hidden behind disjoint ones.

QA

  • Discriminating tests: pace_boundary_runs_at_exactly_headroom_behind_and_pauses_one_point_past, early_week_idles_until_linear_clears_headroom, near_reset_pace_bound_approaches_100_minus_headroom, stale_slack_var_refuses_loudly_naming_the_new_knob, plus rewritten ceiling_is_checked_before_pace_and_wins — each fails on base (the sign-flip mutant restores the base comparison used - linear > headroom and is killed by the first three; the refusal tests reference stale_slack_refusal/UsageVerdict::Refuse, which do not exist on base, so they cannot compile there)
  • Mutations applied: 12 mutants over the pace comparison (direction, sign, operands, arm swap), ceiling boundary, reset boundary, refusal presence and exit codes, clamp, and check ordering — ledger above, all killed
  • Oracle: issue Usage gate: crons should hold 5% BEHIND pace, not run to 5% ahead — BAU work needs the headroom #158's stated semantics (pause when used > linear - headroom; boundary runs; ceiling/reset/no-reset/inert branches unchanged; old var refused loudly, never honoured, never ignored), with expected values hand-computed from exact binary fractions of USAGE_WEEK_MS (linear = 50 / 6.25 / 3.125 / 98.4375 / 99.90234375), independent of the implementation
  • Category check: issue asks (1) flipped pace default, (2) legible knob rename with the old name refused-if-set, (3) log lines reading the new decision, (4) boundary/clamp tests carried over incl. early-week idle named in a test; covered 1,2,3,4 — plus the runner-side handling so the refusal exit can never be read as run (the issue's fail-closed requirement made operational)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added headroom-based weekly usage pacing to determine when review runs should proceed or pause.
    • Added clear refusal handling when retired configuration is detected.
  • Bug Fixes
    • Review runs now stop safely on unexpected usage-gate failures instead of continuing.
    • Added logging for usage-gate decisions and aborted runs.
  • Documentation
    • Updated configuration guidance for headroom pacing and retired slack settings.
    • Documented the new refusal status and exit behavior.

claude added 2 commits July 30, 2026 13:43
…fused, not read)

The pace check inverts: a tick now pauses when used > linear - headroom
(inside the BAU headroom band under the linear burn), instead of only
when used - linear > slack (more than 5 points AHEAD of it). The budget
is shared, and the old gate let the deferrable consumer — the cron,
which self-heals and re-ticks every 4h — spend TO the pace line, so any
interactive day started above the pause threshold the crons themselves
had saturated (nine consecutive skipped ticks on 2026-07-29/30). The
crons now run only while at least ~5 points of budget remain standing
under the pace line for interactive work.

Boundary semantics carry over: exactly at used == linear - headroom the
tick still runs (the boundary is an allowance, not a trip point); the
ceiling, reset-passed, no-reset and inert branches are unchanged. At
week start linear < headroom, so the crons legitimately idle until the
pace line clears the headroom — asserted by name in
early_week_idles_until_linear_clears_headroom, not special-cased away.
Near the reset the pace bound approaches 100 - headroom = 95, and the
default 90% ceiling binds first — also asserted.

The knob renames to USAGE_HEADROOM_PCT (default 5, positive = points
the crons stay behind pace). USAGE_SLACK_PCT is REFUSED if set — exit
2, the binary-wide config-error code, with the reason on stderr naming
the replacement — never silently honoured (that would invert the
operator's limit) and never silently ignored (that would drop a limit
the operator believes is set). The runners keep exporting the retired
name so a stale cron.env reaches the guard, capture the gate's stderr
into the log, and abort the tick on any exit that is neither 0 (run)
nor 10 (pause), propagating the non-zero code so a refusal can never be
read as either.

Closes #158

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 30, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA evidence for the head commit — raw runs, so the body's claims are checkable against what actually executed.

Test suite

Full suite inside nix develop .#rust, all targets:

test result: ok. 636 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 7 passed; 0 failed   (mcp integration)
test result: ok. 47 passed; 0 failed  (require_qa_block)
test result: ok. 8 passed; 0 failed   (run_timings)
test result: ok. 7 passed; 0 failed   (run_usage / refresh_human_queue)

cargo fmt -- --check clean; cargo clippy --all-targets -- -D warnings clean with src/main.rs touched first so a warm target could not mask lints.

The 17 tests in usage_gate_tests (new and rewritten ones in bold):

absent_reset_runs_with_pacing_off
behind_pace_runs_and_reports_both_numbers
**ceiling_is_checked_before_pace_and_wins**        (both-fire input added: ceiling reason wins)
ceiling_pauses_at_the_boundary_not_only_over_it
**early_week_idles_until_linear_clears_headroom**  (new: linear<headroom idles by design)
endpoint_reading_parses_and_names_its_source
every_reading_path_names_its_source
**exit_codes_are_the_interface**                   (Refuse => 2 added)
fallback_is_used_only_when_set_and_says_so
linear_pct_clamps_at_both_ends
**near_reset_pace_bound_approaches_100_minus_headroom** (new: bound -> 95; default ceiling binds first)
no_reading_is_inert_and_runs
oauth_token_is_none_for_every_bad_shape
**pace_boundary_runs_at_exactly_headroom_behind_and_pauses_one_point_past** (rewritten from the +slack boundary test)
reset_already_passed_runs_as_a_new_week
**stale_slack_var_refuses_loudly_naming_the_new_knob** (new: refusal, exit 2, names USAGE_HEADROOM_PCT)
unreadable_usage_shapes_never_produce_a_reading

Mutation ledger

Each mutant applied alone to src/main.rs, usage_gate tests run, file restored. Baseline green first. Raw output:

BASELINE  green
KILLED    pace > to >= (boundary trips)
KILLED    headroom sign flip (behind->ahead)
KILLED    pace operand swap
KILLED    pace arms swapped (condition negated)
KILLED    ceiling >= to >
KILLED    reset >= to >
KILLED    Refuse exit 2 -> 0 (reads as run)
KILLED    Refuse exit 2 -> 10 (reads as pause)
KILLED    refusal fires on empty too
KILLED    refusal never fires
KILLED    linear_pct lower clamp dropped

Plus the ordering mutant, applied by hand (ceiling if moved below the pace check):

test usage_gate_tests::ceiling_is_checked_before_pace_and_wins ... FAILED
test usage_gate_tests::ceiling_pauses_at_the_boundary_not_only_over_it ... FAILED
test result: FAILED. 15 passed; 2 failed

12/12 killed. The sign-flip mutant (> -headroom -> > headroom) is the base semantics restored, so its kill doubles as "the new discriminating tests fail on base".

Runner scripts

nix build .#campaign-run .#review-run rebuilt clean — writeShellApplication runs shellcheck at build time, so the new refusal branch in both runners is shellcheck-passed. The refusal path is exercised by construction: exit 2 is neither the -eq 10 pause branch nor the -ne 0 fall-through to run, and the runner propagates it after logging.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a592de7c-5e3c-42ab-bcba-b5bf3b876307

📥 Commits

Reviewing files that changed from the base of the PR and between 034700d and a6df62a.

📒 Files selected for processing (4)
  • campaign-run.sh
  • cron.env.example
  • pr-review-report-rs/src/main.rs
  • review-run.sh

Walkthrough

The usage gate now uses USAGE_HEADROOM_PCT, refuses retired USAGE_SLACK_PCT configuration with exit code 2, updates pacing tests and documentation, and makes cron scripts propagate gate output and abort on non-pause failures.

Changes

Usage gate headroom migration

Layer / File(s) Summary
Headroom verdict and pacing logic
pr-review-report-rs/src/main.rs
Adds the Refuse verdict, changes pace decisions to headroom semantics, rejects stale slack configuration, and updates boundary and exit-code tests.
Headroom configuration contract
cron.env.example, pr-review-report-rs/src/main.rs
Documents USAGE_HEADROOM_PCT, retires USAGE_SLACK_PCT, and describes refusal exit code 2.
Cron gate propagation and abort handling
campaign-run.sh, review-run.sh
Exports headroom configuration, logs gate output, pauses on exit code 10, and aborts on other non-zero results.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: flipping usage-gate to behind-pace headroom and retiring USAGE_SLACK_PCT.
Linked Issues check ✅ Passed The changes implement the requested behind-pace headroom behavior, config rename/refusal, log updates, and test coverage from #158.
Out of Scope Changes check ✅ Passed The modified scripts, Rust gate logic, docs, and tests all support the linked usage-gate behavior change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-07-30-issue-158-pace-headroom

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed a6df62a: ready — crons hold 5% behind pace, stale USAGE_SLACK_PCT refused at exit 2 (a third code the runners cannot read as run or pause), empty-counts-as-unset and negative-headroom-as-escape-hatch both deliberate and documented, 12/12 mutants killed including the sign-flip and every refusal-exit corruption. Known consequence accepted on the human's word: the current pause extends to ~Aug 1 while pace catches up to used-minus-headroom.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Usage gate: crons should hold 5% BEHIND pace, not run to 5% ahead — BAU work needs the headroom

2 participants