usage-gate: hold the crons 5% BEHIND pace; retire USAGE_SLACK_PCT (refused, not read) - #159
Conversation
…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>
|
QA evidence for the head commit — raw runs, so the body's claims are checkable against what actually executed. Test suiteFull suite inside
The 17 tests in Mutation ledgerEach mutant applied alone to Plus the ordering mutant, applied by hand (ceiling 12/12 killed. The sign-flip mutant ( Runner scripts
🤖 Generated with Claude Code |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
WalkthroughThe usage gate now uses ChangesUsage gate headroom migration
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
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. |
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 > slackpaused). 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: atused == linear - headroomexactly 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 inearly_week_idles_until_linear_clears_headroom. Symmetrically, near the reset the pace bound approaches100 - headroom = 95, and with the default 90% ceiling it is the ceiling that binds at week's end — asserted innear_reset_pace_bound_approaches_100_minus_headroom.The knob rename, fail-closed
USAGE_HEADROOM_PCT(default 5, positive = points the crons stay behind pace) replacesUSAGE_SLACK_PCT. The old var, if set, is REFUSED, never read:stale_slack_refusal(pure, env-as-parameter per the repo'srun_record_path_fromconvention) returns a typedUsageVerdict::Refusewhose message names the replacement and echoes the refused value.usage-gateprints it to stderr and exits 2 — the binary-wide config-error code (bad clap args, unreadable--covered-file, and thegcmissing-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.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.USAGE_SLACK_PCTfrom theexportline 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.USAGE_SLACK_PCTcounts 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.USAGE_HEADROOM_PCTis 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_gatetest set; all killed:>→>=(boundary trips)pace_boundary_runs_at_exactly_headroom_behind_and_pauses_one_point_past-headroom→headroom(sign flip back to the base semantics)early_week_idles_until_linear_clears_headroomused - linear→linear - used>=→>ceiling_pauses_at_the_boundary_not_only_over_it(code and reason)>=→>reset_already_passed_runs_as_a_new_week(reason pins the branch)Refuseexit 2 → 0stale_slack_var_refuses_loudly_naming_the_new_knobRefuseexit 2 → 10linear_pctlower clamp droppedlinear_pct_clamps_at_both_endsceiling_is_checked_before_pace_and_winsOn 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_winsnow 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 anif-swap is visible on a shared input, not hidden behind disjoint ones.QA
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 rewrittenceiling_is_checked_before_pace_and_wins— each fails on base (the sign-flip mutant restores the base comparisonused - linear > headroomand is killed by the first three; the refusal tests referencestale_slack_refusal/UsageVerdict::Refuse, which do not exist on base, so they cannot compile there)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 ofUSAGE_WEEK_MS(linear = 50 / 6.25 / 3.125 / 98.4375 / 99.90234375), independent of the implementation🤖 Generated with Claude Code
Summary by CodeRabbit