Skip to content
164 changes: 163 additions & 1 deletion scripts/agent-rate-limit-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
# dispatch step on it. DRY_RUN / DEV_LEAD_DRY_RUN log every intended decision and
# mutate nothing (AC #7).
#
# Org-wide token-budget breaker (#1155): in addition to the per-agent-type checks
# above, the orchestrator also consults the org-wide Claude token-budget breaker
# (the `session` 5-hour and `weekly_all` 7-day account windows; `weekly_scoped` is
# never consulted — ADR §2.5). A `defer` from EITHER half defers the dispatch. The
# whole consultation is behind AGENT_TOKEN_BUDGET_ENABLED: unset (the default) ⇒
# skipped entirely, so the output is byte-identical to before this change; it is
# inert until a maintainer arms the flag AND wires the private telemetry seam
# (petry-projects/.github-private#1565). Even armed, the weekly glide half stays
# inert until org_wide.token_budget.limits.weekly_all.enabled is flipped in config.
#
# Usage:
# agent-rate-limit-gate.sh <agent_type> [options]
# --mode enforce|log-only default: log-only (only initiative-driver enforces)
Expand All @@ -48,7 +58,10 @@
#
# Env: SOURCE_NOW (epoch override, testability), DRY_RUN / DEV_LEAD_DRY_RUN,
# AGENT_RATE_LIMITS_CONFIG (library override), ARGATE_LIB_ONLY (source
# without running main — for unit tests).
# without running main — for unit tests), AGENT_TOKEN_BUDGET_ENABLED (arm the
# org-wide token-budget consultation; unset ⇒ skipped entirely),
# AGENT_TOKEN_BUDGET_TELEMETRY_CMD / AGENT_TOKEN_BUDGET_TELEMETRY_FILE (the
# private telemetry adapter seam the library reads the usage envelope from).

# Source the co-located pure gate library.
_ARGATE_HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
Expand Down Expand Up @@ -241,6 +254,128 @@ argate_escalate() {
fi
}

# ---------------------------------------------------------------------------
# argate_token_escalate <window> <tracking_repo> <tracking_issue> — post the open
# token-budget-breaker marker for <window> to the tracking issue ONCE and apply
# the human-clearable label (the same needs-human-review gate the per-agent
# breaker uses). Guarded by arl_token_should_escalate for dedup; the marker string
# is read from the library (arl_token_breaker_marker) and the label from
# arl_breaker_label — never restated. Best-effort and disclosed: with no tracking
# issue configured it logs the marker (observable) and skips the gh mutation
# rather than failing. Mirrors argate_escalate for the org-wide token windows so a
# tripped budget is human-clearable and deduplicated, not merely logged (#1155).
# ---------------------------------------------------------------------------
argate_token_escalate() {
local window="$1" tracking_repo="$2" tracking_issue="$3"
local marker label body
marker="$(arl_token_breaker_marker "$window")"
label="$(arl_breaker_label)"

if [ -z "$tracking_issue" ]; then
argate_log "token-budget[${window}] breaker OPEN but no --tracking-issue configured — not posting; marker would be: ${marker}"
return 0
fi

local repo_args=()
[ -n "$tracking_repo" ] && repo_args=(--repo "$tracking_repo")

body="$(gh issue view "$tracking_issue" "${repo_args[@]}" \
--json body,comments \
--jq '[.body, (.comments[]?.body)] | join("\n")' \
2>/dev/null || printf '')"
if ! arl_token_should_escalate "$body" "$window"; then
argate_log "token-budget[${window}] breaker OPEN — marker already present on ${tracking_repo:-current}#${tracking_issue}; deduped, not re-posting"
return 0
fi

local comment
comment="$(printf '%s\n\n⛔ The org-wide **%s** Claude token-budget breaker is OPEN — account-wide usage is at/over the pause threshold and new agent dispatch is deferred. A human clears this by removing the `%s` label and deleting this comment once usage has recovered.' \
"$marker" "$window" "$label")"

if gh issue comment "$tracking_issue" "${repo_args[@]}" --body "$comment" >/dev/null 2>&1; then
argate_log "token-budget[${window}] breaker OPEN — posted escalation marker to ${tracking_repo:-current}#${tracking_issue}"
else
argate_log "warning: failed to post token-budget escalation comment to ${tracking_repo:-current}#${tracking_issue}"
fi
if gh issue edit "$tracking_issue" "${repo_args[@]}" --add-label "$label" >/dev/null 2>&1; then
argate_log "applied '${label}' to ${tracking_repo:-current}#${tracking_issue}"
else
argate_log "warning: failed to apply '${label}' to ${tracking_repo:-current}#${tracking_issue} (does the label exist?)"
fi
}

# ---------------------------------------------------------------------------
# argate_token_budget — consult the org-wide token-budget breaker across the two
# pause-worthy account-wide windows and echo `allow` or `defer` on stdout (#1155).
#
# The telemetry envelope is fetched ONCE (through the library's private adapter
# seam) and fed to BOTH library gates: arl_token_budget_gate for the 5-hour
# `session` window and arl_token_weekly_glide_gate for the 7-day `weekly_all`
# window. `weekly_scoped` is deliberately NOT consulted — per-model exhaustion is
# the engine's model-fallback chain, never a fleet pause (ADR §2.5, AC #4).
#
# Observability (AC #5): every window read, its percent, and its threshold are
# logged, and — critically — whether telemetry was ACTUALLY obtained (a 200
# envelope) is logged distinctly, so a fail-open on absent/degraded telemetry is
# visibly different from a genuine under-threshold allow. On a trip the window's
# human-clearable escalation marker is logged too.
#
# Guard-only: reads telemetry, never mutates. Returns 0 always. The decision
# rides on stdout as space-separated fields: the first is `allow`/`defer`, and any
# remaining fields name the windows that tripped (`session`, `weekly_all`) so the
# enforcing caller can escalate each one (dedup marker + needs-human-review label).
# ---------------------------------------------------------------------------
argate_token_budget() {
local envelope status body decision="allow"
local -a tripped=()
envelope="$(arl_token_fetch_envelope)"
status="$(arl_sanitize_int "$(jq -r '.status? // 0' <<<"$envelope" 2>/dev/null || printf '0')")"
body="$(jq -c '.body? // {}' <<<"$envelope" 2>/dev/null || printf '{}')"

# Extract both pause-worthy window percents up front so the OBTAINED/DEGRADED
# headline reflects whether the body actually carried usable window data. A 200
# status with a missing/empty/malformed body fails BOTH gates open, so it is a
# DEGRADED fail-open — not a genuine OBTAINED read — and must be logged as such
# (AC #5 precision): the headline gates on the presence of a usable percent, not
# on the HTTP status alone.
local s_percent w_percent
s_percent="$(arl_token_extract_percent "$body" session)"
w_percent="$(arl_token_extract_percent "$body" weekly_all)"

if [ "$status" -eq 200 ] && { [ -n "$s_percent" ] || [ -n "$w_percent" ]; }; then
argate_log "token-budget: telemetry OBTAINED (status=200) — evaluating pause-worthy account windows (session, weekly_all)"
elif [ "$status" -eq 200 ]; then
argate_log "token-budget: telemetry status=200 but NO usable window data (empty/malformed body) — the breaker fails open; any allow below is DEGRADED, not a genuine under-threshold allow"
else
argate_log "token-budget: telemetry NOT obtained (status=${status}) — the breaker fails open; any allow below is DEGRADED, not a genuine under-threshold allow"
fi

# session (5-hour, static config threshold).
local s_threshold s_decision
s_threshold="$(arl_token_pause_threshold session)"
argate_log "token-budget[session]: percent=${s_percent:-<none>} threshold=${s_threshold:-<none>}%"
s_decision="$(arl_token_budget_gate session "$envelope")" || true
if [ "$s_decision" = "decision=defer" ]; then
decision="defer"
tripped+=(session)
argate_log "token-budget[session] TRIP — escalation marker: $(arl_token_breaker_marker session)"
Comment thread
don-petry marked this conversation as resolved.
fi

# weekly_all (7-day, time-varying glide-path threshold). Inert until
# weekly_all.enabled is set in config, even when the env flag is armed.
local w_decision
argate_log "token-budget[weekly_all]: percent=${w_percent:-<none>} threshold=glide-path (config-enabled=$(arl_token_glide_enabled))"
w_decision="$(arl_token_weekly_glide_gate "$envelope")" || true
if [ "$w_decision" = "decision=defer" ]; then
decision="defer"
tripped+=(weekly_all)
argate_log "token-budget[weekly_all] TRIP — escalation marker: $(arl_token_breaker_marker weekly_all)"
fi

printf '%s' "$decision"
[ "${#tripped[@]}" -gt 0 ] && printf ' %s' "${tripped[@]}"
}

# ---------------------------------------------------------------------------
# argate_emit <decision> — print `decision=<decision>` to stdout and, when
# running under GitHub Actions, append it to $GITHUB_OUTPUT so the caller can
Expand Down Expand Up @@ -343,6 +478,26 @@ argate_gate() {
[ "$admission" != "decision=allow" ] && decision="defer"
fi

# Org-wide token-budget breaker (#1155). INERT unless AGENT_TOKEN_BUDGET_ENABLED
# is set: unset ⇒ this block is skipped entirely, so the output stays
# byte-identical to the pre-#1155 per-agent-only behaviour (AC #2/#6). When
# armed, a `defer` from the token-budget half defers dispatch in addition to the
# per-agent-type checks above — the account-wide budget is consulted regardless
# of whether a per-agent limit already deferred, so the observability log always
# records the budget state (AC #1/#5).
local -a tripped_token_windows=()
if [ "${AGENT_TOKEN_BUDGET_ENABLED:-false}" = "true" ]; then
local -a token_fields=()
local token_decision
read -r -a token_fields < <(argate_token_budget)
token_decision="${token_fields[0]:-allow}"
tripped_token_windows=("${token_fields[@]:1}")
if [ "$token_decision" = "defer" ]; then
argate_log "token-budget breaker deferred dispatch for '${agent_type}' (org-wide account budget over threshold)"
decision="defer"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi
fi

# DRY_RUN: log the intended decision, mutate nothing, do not block (AC #7).
if arl_is_dry_run; then
argate_log "DRY_RUN — computed decision=${decision} for '${agent_type}'; emitting allow, no side effects"
Expand All @@ -363,6 +518,13 @@ argate_gate() {
if [ "$breaker_open" -eq 1 ]; then
argate_escalate "$agent_type" "$tracking_repo" "$tracking_issue"
fi
# Escalate each tripped org-wide token-budget window the same way (dedup marker +
# needs-human-review label), so an armed budget trip is human-clearable and
# deduplicated through the tracking issue rather than merely logged (#1155).
local token_window
for token_window in "${tripped_token_windows[@]}"; do
argate_token_escalate "$token_window" "$tracking_repo" "$tracking_issue"
done
argate_log "enforce mode for '${agent_type}' — decision=${decision}"
argate_emit "$decision"
return 0
Expand Down
2 changes: 1 addition & 1 deletion standards/agent-rate-limits.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"status": "signed-off",
"_note": "Human sign-off 2026-09-01 (epic #636 gate; maintainer sign-off comment on #636). This is the machine-readable single source of truth for per-agent-type rate limits, circuit breakers, and the org-wide Claude token-budget breaker. Decision record: docs/initiatives/agent-rate-limits-adr.md (#637). The per-agent-type limits and consecutive-failure breakers are now CONSUMED at dispatch time by scripts/agent-rate-limit-gate.sh (Phase 4, #640): initiative-driver ENFORCES, every other agent type runs log-only. org_wide.token_budget stays out of the live path (AGENT_TOKEN_BUDGET_ENABLED is unset) and org_wide.limits.weekly_all.enabled stays false — arming the weekly glide-path breaker is a separate, deliberately-out-of-scope change (#994). Consumers read values from this single file, never hardcoded.",
"_note": "Human sign-off 2026-09-01 (epic #636 gate; maintainer sign-off comment on #636). This is the machine-readable single source of truth for per-agent-type rate limits, circuit breakers, and the org-wide Claude token-budget breaker. Decision record: docs/initiatives/agent-rate-limits-adr.md (#637). The per-agent-type limits and consecutive-failure breakers are now CONSUMED at dispatch time by scripts/agent-rate-limit-gate.sh (Phase 4, #640): initiative-driver ENFORCES, every other agent type runs log-only. org_wide.token_budget is now wired into that same dispatch path but DISARMED (#1155): scripts/agent-rate-limit-gate.sh consults the session + weekly_all windows, yet the consultation is gated on AGENT_TOKEN_BUDGET_ENABLED which stays unset, and org_wide.token_budget.limits.weekly_all.enabled stays false — so the breaker is inert (byte-identical to per-agent-only behaviour). Arming it (setting the flag with the private telemetry seam, and flipping weekly_all.enabled for the glide-path half) is a separate, deliberately-out-of-scope change (#994). Consumers read values from this single file, never hardcoded.",
"_schema_version": 1,
"agent_types": {
"dev-lead": {
Expand Down
22 changes: 17 additions & 5 deletions standards/agent-rate-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,23 @@ the gate, mirroring the PR-Limits sign-off exactly (ADR §7,
Phase-4 orchestrator
[`scripts/agent-rate-limit-gate.sh`](../scripts/agent-rate-limit-gate.sh)
([#640](https://github.com/petry-projects/.github/issues/640)), which wraps the
pure gate library. The org-wide token-budget breaker (§4) stays out of the live
path — `AGENT_TOKEN_BUDGET_ENABLED` is unset and
`org_wide.token_budget.limits.weekly_all.enabled` stays `false` — so arming the
weekly glide-path breaker remains a separate, deliberately out-of-scope change
([#994](https://github.com/petry-projects/.github/issues/994)).
pure gate library.

The org-wide token-budget breaker (§4) is now **wired into that same orchestrator's
decision path but disarmed** ([#1155](https://github.com/petry-projects/.github/issues/1155)):
`scripts/agent-rate-limit-gate.sh` consults both account-wide windows (`session`
and `weekly_all`; `weekly_scoped` is never consulted, ADR §2.5) alongside the
per-agent-type checks, and a `defer` from the token-budget half defers dispatch —
but the whole consultation is gated on `AGENT_TOKEN_BUDGET_ENABLED`, which stays
**unset**, so the breaker is inert and the gate's output is byte-identical to the
per-agent-only behaviour. It is therefore **wired-but-disarmed**, not
signed-off-and-armed: arming it is a deliberate maintainer change that must (a)
set `AGENT_TOKEN_BUDGET_ENABLED` with the private telemetry seam wired
(petry-projects/.github-private#1565),
and (b) flip `org_wide.token_budget.limits.weekly_all.enabled` from `false` to arm
the weekly glide-path half — each a separate, deliberately out-of-scope change
([#994](https://github.com/petry-projects/.github/issues/994)). See §4.2 for the
staged rollout.

### 3.1 Canary scope — enforcing vs log-only, and the promotion path (AC #5)

Expand Down
Loading
Loading