Repository navigation
Description
Story
As the maintainer of the agent fleet,
I want a scheduled job that reads the Claude subscription usage windows and flips AGENTS_PAUSED on and off automatically,
so that budget conservation stops being a manual act of noticing pressure and withholding a secret — the failure that produced the 2026-08-16→18 opaque fleet outage (#1525).
⚠️ Status update 2026-09-21 — the public half landed, this is now the only thing standing between the fleet and the budget
Epic petry-projects/.github#636 closed on 2026-09-07 with phases 1–6 merged. The library (scripts/lib/agent-rate-limit.sh), the config (standards/agent-rate-limits.json), the 5-hour static breaker and the 7-day glide path all exist and are tested. Two things keep the whole apparatus inert:
- Nothing produces telemetry — this issue.
- Nothing consults the breaker —
scripts/agent-rate-limit-gate.shcontains no reference toarl_token_budget_gateorarl_token_weekly_glide_gate. Filed aspetry-projects/.github#1155.
Measured on the fleet while both gaps were open:
session 33% normal resets 2026-09-21T05:20Z
weekly_all 79% warning resets 2026-09-22T16:00Z
weekly_scoped 36% normal
79% of the weekly budget consumed with two days left in the window, against ~2,500 agent workflow runs over two days that merged nothing (see #1864). Nothing paused, nothing warned. The fleet's only other token-awareness check, check_provider_headroom, has never worked (#1862).
The integration contract is now fixed by the shipped library — build to it, do not invent a format. See the revised Scope §1.
Why this lives in the private repo
The public epic (petry-projects/.github#636) owns the decision (threshold config) and the gate (the library that defers a dispatch). It cannot own the actuation: the Claude credential (CLAUDE_CODE_OAUTH_TOKEN), the engine, and the token telemetry all live here. Epic #636 already lists this as an untracked cross-repo prerequisite. This issue tracks it.
Scope
-
Telemetry adapter —
scripts/lib/usage-telemetry.sh: one call toGET https://api.anthropic.com/api/oauth/usage, emitting the envelope shape the shipped public library already consumes (scripts/lib/agent-rate-limit.sh:485):{ "status": <http_status>, "retry_after": <int?>, "observed_at": <epoch?>, "body": <raw upstream response> }- The adapter's job is the transport envelope, not normalization.
bodycarries the upstream response unmodified; the library's ownarl_token_extract_percent/arl_token_extract_resets_at/arl_token_window_activedo the field extraction, preferringlimits[]over the flattenedfive_hour/seven_daykeys. Re-normalizing here would create a second, drifting parser — the exact failuremodel-pricing.tsvexists to prevent. - Publish it to the library through its existing seam: write the envelope to a file and export
AGENT_TOKEN_BUDGET_TELEMETRY_FILE, or expose a command viaAGENT_TOKEN_BUDGET_TELEMETRY_CMD. Do not add a new seam. - Headers are load-bearing:
Authorization: Bearer $CLAUDE_CODE_OAUTH_TOKEN,anthropic-beta: oauth-2025-04-20, andUser-Agent: claude-code/<version>— without the User-Agent the caller hits an aggressively rate-limited bucket and gets persistent 429s. - On a 429, populate
retry_afterandobserved_at; the library'sarl_token_transport_decisionuses them to distinguish a fresh hard-cap signal (defer) from a stale one (allow). - Non-200 / malformed / missing-window ⇒ allow-with-warning, never a hard failure (per the ADR default in
.github#637).
- The adapter's job is the transport envelope, not normalization.
-
Scheduled poller — an hourly workflow evaluating the configured thresholds and, on trip, setting the org Actions variable
AGENTS_PAUSED=trueplusAGENTS_PAUSE_SOURCE=weekly-glide(orfive-hour). -
Auto-resume — after the window's
resets_atpasses, clearAGENTS_PAUSEDonly ifAGENTS_PAUSE_SOURCEmarks it as automation-set. A pause a human set by hand is never cleared by the poller. -
Fleet-monitor awareness — paused runs already count as healthy (Make 'agents intentionally paused' a first-class state — deliberate token disable presented as fleet-wide outage (2026-08-16) #1525 AC Optimize review: small-PR and incremental fast paths #4); extend the report to name why (which window, what percent) so a budget pause is legible at a glance rather than looking like a quiet fleet.
-
Notification — one notice when the breaker trips and one when it clears. A pause nobody is told about reproduces the original Make 'agents intentionally paused' a first-class state — deliberate token disable presented as fleet-wide outage (2026-08-16) #1525 confusion in a new shape.
Acceptance Criteria
scripts/lib/usage-telemetry.shexists, isshellcheck-clean, and emits the envelope in Scope §1 verbatim; unit tests mockcurland cover 200 / 429-with-retry-after / other-non-200 / malformed / missing-window. No live network in tests.- The poller runs on a schedule (hourly is far inside the endpoint's ≥180 s guidance) and supports a dry-run mode that logs the decision without writing any variable — this is the default until the epic's human sign-off.
- Setting/clearing the org variable works with a credential that actually carries the permission (see the credential note below), and a permission failure surfaces as one actionable error, not a silent no-op.
- Auto-resume never clears a human-set pause; asserted by a test that sets
AGENTS_PAUSED=truewithoutAGENTS_PAUSE_SOURCEand confirms the poller leaves it alone. AGENTS.mddocuments the automatic pause alongside the existing manual pause/resume paragraph, including how to override it.- Verifies — and records — that the long-lived
claude setup-tokencredential is accepted by the usage endpoint (see below). - Round-trip proof against the real consumer. A test feeds the adapter's envelope to the shipped
arl_token_budget_gateandarl_token_weekly_glide_gateand asserts the expecteddecision=. An envelope that the public library cannot read is the single most likely way this ships broken and nobody notices. - Telemetry liveness is asserted, not assumed. The poller emits a positive "telemetry read OK, session=N% weekly_all=M%" record on every successful poll, and the fleet monitor raises a warning when no such record has appeared within a configurable staleness window. Every failure path in this design is fail-open, which means a silently broken poller is indistinguishable from a healthy under-budget fleet — the exact shape of the three-week gap this issue sat in. A breaker nobody can prove is alive is not a breaker.
- The dry-run decision is logged against live data before arming. Run the poller in dry-run for at least one full weekly window and record what it would have done, so the thresholds are calibrated against real utilization rather than assumed. The 79% observation above suggests the 5-hour breaker (90%) would rarely trip while the weekly glide path would be the binding control — confirm or refute that before arming either.
Dev Notes — two verification items found during the spike
Credential type is unproven. The endpoint was probed successfully (HTTP 200, live window data) using the short-lived OAuth access token from a local credential store — reconfirmed 2026-09-21. CI authenticates with the long-lived claude setup-token credential. Both carry the sk-ant-oat01- prefix and both are OAuth-issued, so acceptance is likely but not demonstrated. Verify by running the poller in dry-run in CI against the existing secret — do not mint a fresh setup-token locally to test, since that risks invalidating the credential the fleet currently runs on. If the long-lived token is rejected, the fallback is to run the poller with a dedicated short-lived credential refreshed by the workflow, which is a materially larger change and should be re-scoped before implementation.
Org-variable write needs a permission the fleet may not have. Writing an org-level Actions variable returns 403 — You must be an org admin or have the actions variables fine-grained permission for a classic token with repo / workflow / read:org, even when the caller is already an org admin. It requires admin:org (classic) or the fine-grained org Variables: write permission. Confirm whether GH_PAT_DON_PETRY carries this before implementation; if not, either widen that PAT or fall back to per-repo variables (writable under plain repo scope) at the cost of N writes and a slower fleet-wide stop.
Sequencing
This issue and petry-projects/.github#1155 are independently landable and jointly required — neither alone changes fleet behaviour. Land this first: the public gate can then be wired and armed against real telemetry rather than a mock. #1862 (the dead in-path headroom probe) should consume this adapter rather than build a second reader, and #1863 (weekly resets parsing as "unknown") should take its resets_at from this envelope instead of parsing prose.
References
petry-projects/.github#636— epic (closed 2026-09-07; closure hid the two remaining gaps)petry-projects/.github#1155— the public gate never consults the breaker (the other half of the gap)petry-projects/.github#637— ADR; §4.1 telemetry contract, §7 thresholds and fail-safe direction, §2.5 scope guardpetry-projects/.github#641/#994— the 5-hour and weekly-glide breakers this feeds- check_provider_headroom has never worked: probes with an unset ANTHROPIC_API_KEY, reads per-minute TPM instead of the subscription window, and pr-review never calls it #1862 —
check_provider_headroomhas never worked; should consume this adapter - A weekly Claude cap parses as 'unknown reset', so the retry cron re-dispatches every 2 hours against a block that can last 7 days #1863 — weekly cap parses as "unknown reset"; should take
resets_atfrom this envelope - Merge protections are unsatisfiable at N>1 open PRs: one merge to main forces a full re-review of every other PR — ~154 forced re-reviews in 48h, zero merges #1864 — the demand-side churn this breaker would otherwise be paying for
- Make 'agents intentionally paused' a first-class state — deliberate token disable presented as fleet-wide outage (2026-08-16) #1525 —
AGENTS_PAUSEDas a first-class state; this automates the switch it delivered scripts/engine.sh— reactive exit-2 fallback chain (the after-the-fact protection this front-runs)scripts/validate-engines.sh— already reports the paused state distinctly
Calibration evidence for AC #9 — the configured thresholds would NOT have fired this week
Observed on the live fleet, 2026-09-21 (weekly window resets 2026-09-22T16:00Z, i.e. ~1 day out):
session 3% normal ← 5-hour window had just reset
weekly_all 89% warning
weekly_scoped 40% normal
Against the thresholds currently in standards/agent-rate-limits.json:
| Breaker | Threshold at this moment | Actual | Would it pause? |
|---|---|---|---|
session static |
90% | 3% | no |
weekly_all glide |
100 − 2 × 1 day = 98% |
89% | no |
So on the week that ran the budget from 79% to 89% while merging almost nothing, neither breaker would have tripped even if both were armed and fed. The 5-hour window never approached 90% because the spend was spread thin and continuous rather than bursty; the glide path is at its most permissive exactly when the week is nearly spent.
This is not an argument against the design — it is the calibration AC #9 asks for, arriving early and for free. Two things it suggests, to be confirmed by the required dry-run window rather than acted on blindly:
reserve_pct_per_day = 2may be too lax. It only bites in the final ~1–2 days, by which point the budget is already gone. A steeper reserve, or a floor well below 86%, would engage while there is still something to conserve.- A continuous low-level drain is invisible to both breakers. Both are level-triggered on a percentage; neither looks at rate. A burn-rate signal ("at the current rate the weekly budget exhausts N hours before
resets_at") would have flagged this week on about day two. Worth considering as a third, advisory-only signal before adding another pause-worthy one.
Record what the dry-run would have done across a full window before arming, and compare it against this observation.
Design question raised 2026-09-21 — should the breaker pause, or degrade to Gemini?
The ADR gives this breaker exactly one action: set AGENTS_PAUSED and stop the fleet. That was specified before the multi-key Gemini fallback (#1777) was in place. It now is, and it is idle:
- Org secrets
GOOGLE_API_KEY,GOOGLE_API_KEY_2,GOOGLE_API_KEY_3all exist (three distinct keys). - Both agents receive them —
dev-lead.ymlandpr-review-trigger.ymlusesecrets: inherit, anddev-lead-reusable.yml/pr-review.ymlmap all three into env at every engine step. engine.shhas three nested fallback layers: per-key rotation within a model, then the in-Gemini model chain, then cross-providerclaude → gemini → copilot.- No
DEV_LEAD_ENGINESkill-switch is set, so the full chain is live.
And it has never fired. The most recent weekly cost report is 100% Claude — zero Gemini calls, zero Copilot calls — across the week that carried weekly_all from 79% to 97%. The reason is that cross-provider fallback is triggered only by a failed call (exit 2). Below 100% nothing fails, so nothing falls over. The response is binary: full Claude until hard exhaustion.
So at the moment the breaker would trip (90% session, or the weekly glide), there are three unused Gemini keys available. Stopping the fleet is not obviously the right action when a working, cheaper engine is sitting idle.
Worth deciding before arming:
- Degrade before pausing. At the pause threshold, set the engine chain to start at
gemini(theDEV_LEAD_ENGINESkill-switch already expresses exactly this, with no new mechanism) instead of halting. ReserveAGENTS_PAUSEDfor when every provider is exhausted or a human wants a hard stop. - If degrading, say so loudly. Gemini output quality differs from Claude's; a silent engine swap changes review and implementation behaviour without anyone being told. It needs the same "one notice when it trips, one when it clears" treatment as a pause (Scope §5), naming the engine.
- Keep
weekly_scopedout of it either way — per-model exhaustion is already the model-fallback chain's job (ADR §2.5).
This does not change the telemetry work in this issue, which is the prerequisite for either action. It changes what the gate does with the signal, so it likely belongs in petry-projects/.github#1155's scope as well. Raising it here so it is decided rather than inherited.
Activity
github-actions commentedon Aug 20, 2026
Thanks for the thorough write-up — the cross-repo context, the credential/permission findings from the spike, and the explicit non-goals make this very actionable. Before implementation begins, could you confirm whether
GH_PAT_DON_PETRYcurrently carries the org-level Variables write permission (oradmin:org), since that determines whether the poller can write directly to the org variable or needs to fall back to per-repo writes?don-petry commentedon Sep 22, 2026
Dev-Lead Implementation Plan
Issue: #1565 — Auto-pause companion: usage-telemetry adapter + scheduled poller that sets/clears AGENTS_PAUSED from the Claude budget
Scope of this PR — the "land this first" prerequisite (Sequencing §)
Implement
scripts/lib/usage-telemetry.sh: the transport-envelope adapter forGET https://api.anthropic.com/api/oauth/usage, emitting the exact envelope the shipped publiclibrary (
petry-projects/.githubscripts/lib/agent-rate-limit.sh) already consumes via itsAGENT_TOKEN_BUDGET_TELEMETRY_FILE/AGENT_TOKEN_BUDGET_TELEMETRY_CMDseam — plus unit tests, around-trip proof against a pinned copy of that consumer, and the AGENTS.md docs.
Why the poller/actuation is deliberately NOT in this PR
The consumer library does not exist in this private repo (it lives in the public
petry-projects/.github), so the poller cannotsourcea breaker to consult here yet. Wiring thehourly poller + org-variable actuation also depends on three things a code-only PR cannot discharge,
all called out in the issue's Dev Notes / ACs:
claude setup-tokencredential is unproven against theusage endpoint; must be verified by a CI dry-run, and explicitly not by minting a token locally.
GH_PAT_DON_PETRYmay lack org Variables: write; unconfirmed.The issue's own Sequencing says "Land this [adapter] first: the public gate can then be wired and
armed against real telemetry." This PR delivers exactly that landable, hermetic unit; the poller,
notifications, fleet-monitor awareness, and live actuation follow once the above are discharged.
Implementation Checklist
scripts/lib/usage-telemetry.sh— onecurlto the OAuth usage endpoint with the threeload-bearing headers (
Authorization: Bearer,anthropic-beta: oauth-2025-04-20,User-Agent: claude-code/<version>); emits{status, retry_after?, observed_at?, body}withbodythe unmodified upstream response (no re-normalization — the library owns extraction).retry_after(from theRetry-Afterheader) +observed_at.statuswith no/empty body (library fails safe toallow-with-warning). Never a hard failure.
usage_telemetry_publish_filehelper — writes the envelope + exports the existingAGENT_TOKEN_BUDGET_TELEMETRY_FILEseam (no new seam)..github/workflows/lint.yml.AGENTS.md— document the (forthcoming) automatic pause alongside the manual pause paragraph.Tests to Write
tests/dev-lead/unit/test_usage_telemetry.bats— mockcurl, cover 200 / 429-with-retry-after/ other-non-200 / malformed / missing-window / missing-token. No live network.
tests/dev-lead/integration/test_usage_telemetry_roundtrip.bats— feed the adapter's envelope tothe real
arl_token_budget_gateandarl_token_weekly_glide_gate(pinned copy undertests/fixtures/agent-rate-limit/) and assertdecision=allow/deferfor session ≥/<90% andthe weekly glide path (AC Workflow should skip no-op PRs in MAX_PRS count #7).
Test Command
bats tests/dev-lead/unit/test_usage_telemetry.bats tests/dev-lead/integration/test_usage_telemetry_roundtrip.batsthen the full
scripts/run-bats.sh …list +bash .dev-lead/scripts/dev-lead-lint.sh.don-petry commentedon Sep 22, 2026
Dev-Lead: Progress (in progress — not yet durable)
What this workspace contains (intended)
The "land this first" prerequisite from the issue's Sequencing — the transport
adapter and its proof of contract compatibility — plus docs:
scripts/lib/usage-telemetry.sh— onecurltoGET …/api/oauth/usagewith thethree load-bearing headers (
Authorization: Bearer,anthropic-beta: oauth-2025-04-20,User-Agent: claude-code/<version>), emitting{status, retry_after?, observed_at?, body}with
bodythe unmodified upstream response. 429 populatesretry_after+observed_at; non-200 / transport error / missing token → status-only envelope(library fails safe to allow). Publishes via the existing
AGENT_TOKEN_BUDGET_TELEMETRY_FILEseam — no new seam.tests/dev-lead/unit/test_usage_telemetry.bats— mockscurl; covers200 / 429-with-retry-after / other-non-200 / malformed / missing-window / missing-token.
tests/dev-lead/integration/test_usage_telemetry_roundtrip.bats+ a pinned copy ofthe public consumer under
tests/fixtures/agent-rate-limit/— feeds the adapter'senvelope through the file seam into the real
arl_token_budget_gate/arl_token_weekly_glide_gateand assertsdecision=allow/defer(AC Workflow should skip no-op PRs in MAX_PRS count #7).AGENTS.md— documents the automatic budget pause + the human override, beside theexisting manual pause paragraph (AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5).
.github/workflows/lint.yml— registers the two new bats files.Deliberately deferred (not in this slice — see the plan comment for rationale)
The scheduled poller + org-variable actuation, notification, and fleet-monitor
awareness (Scope §2–5) are not included, because the consumer library is not
co-located in this private repo yet and because arming depends on verifications a
code-only change cannot discharge: the long-lived
claude setup-tokenacceptance bythe endpoint (AC #6 / Dev Note 1),
GH_PAT_DON_PETRYcarrying org Variables: write(AC #3 / Dev Note 2), and a full-weekly-window dry-run calibration (AC #9). The issue's
own Sequencing says to land this adapter first and wire/arm the gate against real
telemetry afterward.
Local check output (provisional — not the pushed run)
bash .dev-lead/scripts/dev-lead-lint.sh: clean (shellcheck on 154 files, profiles, CODEOWNERS).cases in three
pr-reviewfiles (they reproduce with none of my files involved andpass in isolation) — unrelated to this change.
Notes
The vendored
tests/fixtures/agent-rate-limit/copy is a pinned snapshot of the publicconsumer for hermetic round-trip testing; a diff there is the intended drift signal to
refresh it. Follow-ups this unblocks: #1155 (wire the gate), #1862 (consume this adapter
instead of the dead headroom probe), #1863 (take
resets_atfrom this envelope).don-petry commentedon Sep 22, 2026
Dev-Lead: Implementation Complete — PR #1888
The implementation for issue #1565 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
4606847005010e97e7eef4f1e07d4690b1ef9d27main: 9 file(s), +1967/-0 linesReview happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
fix(standards): render private issue reference as plain text
feat: implement issue #1155 — The token-budget breaker is unreachable…
feat: implement issue #1565 — Auto-pause companion: usage-telemetry a…
don-petry commentedon Oct 2, 2026
Maintainer decision recorded 2026-10-02: when the breaker trips the fleet degrades to the Gemini engine rather than pausing, and Gemini quota must be checked and respected the same as Claude's. Consequences for this issue: arming is blocked on Gemini metering (#2030); the actuation is likely a flip of the AI_ENGINES variable (e.g. to gemini,claude) rather than AGENTS_PAUSED, to be restored after the window resets, and it must refuse to degrade when Gemini itself is constrained. Order of work: #2030 (meter Gemini), #2029 (dry-run poller), then an actuation issue. Scope items 2 and 3 above (variable set/clear, auto-resume) are unchanged in shape but target AI_ENGINES.
don-petry commentedon Oct 9, 2026
Decision update, 2026-10-09 (supersedes the earlier note that the fleet would degrade to Gemini): pause is the primary control; Gemini is a small trickle for cheap work only, sized by data so spend can be raised later. Consequences for this issue: (1) arming means setting AGENTS_PAUSED with AGENTS_PAUSE_SOURCE, as originally scoped; the AI_ENGINES-flip idea is dropped. (2) The Gemini trickle is tracked in #2147 (default off, exempts only a budget-source pause, allowlisted cheap tiers, only when Gemini capacity exists). (3) Order of work: dry-run poller (#2029) for a full weekly window (AC 9 here), then arming, with the trickle layered on after #2041 and #2146. (4) Open prerequisite, not code: the org-variable write permission noted in this issue (needs admin:org or fine-grained org Variables: write); confirm GH_PAT_DON_PETRY carries it before the arming PR.
don-petry commentedon Oct 9, 2026
Constraint confirmed by the maintainer, 2026-10-09: GH_PAT_DON_PETRY is a classic token without organisation variable write. GitHub documents admin:org as the classic-token requirement for org variables and the repo scope for repository variables (the page does not document fine-grained or GitHub App permission names; confirm those in the token or App creation UI). Note AGENTS_PAUSED is read as vars.AGENTS_PAUSED inside the reusable workflows, which resolves in the CALLING repository context, so a repo-level variable pauses only that repo. Options under consideration for the arming step (decision pending): (a) a dedicated fine-grained PAT owned by the org with org Variables write, (b) a GitHub App token, (c) per-repo variables written with the existing repo scope for each consumer repo, (d) notify-only until a human sets the variable. Widening the shared classic token to admin:org is NOT proposed. The dry-run poller is unaffected and proceeds.
don-petry commentedon Oct 9, 2026
Decision recorded 2026-10-09: the arming credential is a dedicated fine-grained PAT owned by the org with organisation Variables write only (option A), stored as the secret BUDGET_BREAKER_PAT, available only to this repository. The writer is tracked in #2153 as a separate default-off workflow so the dry-run poller keeps its no-write-path guarantee. The shared classic token is not widened. The maintainer creates and stores the token; no automation touches its value.
don-petry commentedon Oct 10, 2026
Maintainer decision 2026-10-10: the breaker is reactive (it does not read the usage endpoint; see the evidence and decision on #2029). The pieces are "Reactive budget breaker, decision only" (#2221) and "Budget breaker writer" (#2153). For criterion 9 here, the dry-run evidence reviewed before arming now comes from the writer running disarmed and logging the write it would have made, not from poller records.
don-petry commentedon Oct 11, 2026
Arming status, 2026-10-11 17:15 UTC. The breaker is still disarmed and
BUDGET_BREAKER_ARMEDis unset.workflow_runwake-up, schedule kept as backstop. The maintainer agreed in principle today.workflow_runfires after a Dev-Lead Agent run that endedskippedfeature-ideation/v1-*channels move; most pinv1-stable.Also merged today: "Three monitors misread a paused run" (#2266, by #2281), so the canaries and the fleet monitor report a paused run as paused.
don-petry commentedon Oct 14, 2026
Arming precondition 6: observed (2026-10-14)
Both halves of "the event trigger has been seen working in disarmed runs" are now on record.
1. A run woken by
workflow_runwhoseprevious run:line shows a time (so the read credential can list the workflow's runs). Budget Breaker run 38229601217, 2026-10-12:2. A Budget Breaker run that starts within seconds of a Dev-Lead Agent run that ended
skipped(what a resume relies on, and until now taken from GitHub's documentation only).check_run) endedskippedat 09:23:39 UTC on 2026-10-12.skippedwere followed by aworkflow_runbreaker run within five seconds; 17 of those breaker runs executed and the rest were replaced while waiting, as designed.So
workflow_rundoes fire for a triggering run that concludedskipped. What this does not show is a paused run: no pause has been set, so these were runs skipped by their event filter. A paused Dev-Lead Agent run ends the same way (every job skipped at the job level), which is why this is the relevant observation, but the first real pause is still the first direct one.Also since the last status here: the early exit's slot is ten minutes (#2307), the trusted-records filter and the ledger fix have merged (#2305, #2311), and on 13 October the breaker ran 16 times in the day, against 711 on the 11th, because fleet activity itself fell from 2,500 Dev-Lead Agent runs to 8.
don-petry commentedon Oct 14, 2026
The dry-run budget poller (
budget-poller.yml) is disabled (disabled_manually); its last run was 2026-10-14 07:39 UTC. Under the reactive design it could only ever recorddegraded(permission_error, the CI credential cannot read usage). Its two readers already fail open on a missing log: the retry sweep's budget hold logsbudget hold fail-open (no readable budget-poller-log artifact), and the fleet monitor warns that no OK poll exists, as it did before. To bring it back:gh workflow enable budget-poller.yml -R petry-projects/.github-private.