Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions campaign-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ KEEP_RUNS=2000 # retained per-run traces (~1.8MB each → ~4GB/~
# The gate runs in-process in pr-review-report, so its config must reach the binary as ENV.
# cron.env is sourced above, which makes these shell-local; export them explicitly. Exporting a
# name that cron.env never set is a no-op — bash does not put unset names in the child's env — so
# an unset var stays absent rather than arriving as an empty string.
export USAGE_CEILING_PCT USAGE_SLACK_PCT USAGE_USED_PCT USAGE_RESET_AT USAGE_URL CLAUDE_CREDENTIALS
# an unset var stays absent rather than arriving as an empty string. USAGE_SLACK_PCT is RETIRED
# (#158) but still exported: the gate REFUSES it when set, and dropping it from this list would
# hide a stale cron.env from that guard instead of surfacing it.
export USAGE_CEILING_PCT USAGE_HEADROOM_PCT USAGE_SLACK_PCT USAGE_USED_PCT USAGE_RESET_AT USAGE_URL CLAUDE_CREDENTIALS


# --- org scope: single source = cron.env ORGS; derive owner-flags + prose, export for pr-review-report ---
Expand All @@ -82,12 +84,21 @@ if [ -f "$DIR/DISABLED" ]; then
exit 0
fi

# --- weekly-budget pace gate: skip this tick when usage is over the ceiling or running ahead of a
# linear burn toward the reset. `usage-gate` reads /api/oauth/usage itself; exit 10 means PAUSE.
# It is INERT when it cannot read usage and no fallback is set — it prints OK and we run. ---
_ug="$(pr-review-report usage-gate)"; _ugrc=$?
# --- weekly-budget pace gate: skip this tick when usage is over the ceiling or inside the BAU
# headroom band under the linear burn toward the reset — the crons hold ~USAGE_HEADROOM_PCT points
# BEHIND pace so interactive work keeps standing budget (#158). `usage-gate` reads
# /api/oauth/usage itself; exit 10 means PAUSE (log it, exit 0). It is INERT when it cannot read
# usage and no fallback is set — it prints OK and we run. Any OTHER non-zero exit is a config
# REFUSAL (the retired USAGE_SLACK_PCT still set: exit 2, reason on stderr, captured into the
# log): the tick must not run on config the gate refused to read, so propagate the failure — a
# refusal is neither a run nor a pause. ---
_ug="$(pr-review-report usage-gate 2>&1)"; _ugrc=$?
echo "$(date -u +%FT%TZ) usage-gate: $_ug" >> "$LOG"
[ "$_ugrc" -eq 10 ] && exit 0
if [ "$_ugrc" -ne 0 ]; then
echo "$(date -u +%FT%TZ) campaign run ABORTED: usage-gate refused its config (exit $_ugrc) — fix cron.env" >> "$LOG"
exit "$_ugrc"
fi

# --- single-run lock (non-blocking: skip this tick if a prior run is still going) ---
exec 9>"$LOCK"
Expand Down
21 changes: 14 additions & 7 deletions cron.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,23 @@ PR_ASSIGNEE="your-github-handle"
# and is the only way it can delete a work clone at all: `rm -rf <clone>` is denied by a
# prefix-matched rule (#56).

# Weekly-budget PACE GATE (`pr-review-report usage-gate`) — pause a tick when we're burning the
# Claude plan's weekly budget faster than a steady pace. Usage IS machine-readable: the gate reads
# /api/oauth/usage with the OAuth credential and takes `seven_day.utilization` and
# `seven_day.resets_at` from it, so there is no human checkpoint to go stale. Everything below is
# OPTIONAL.
# Weekly-budget PACE GATE (`pr-review-report usage-gate`) — pause a tick unless usage is holding
# comfortably BEHIND a steady burn of the Claude plan's weekly budget, so interactive/BAU work
# always has standing headroom and the deferrable consumer (the cron, which re-ticks every 4h) is
# the one that waits (#158). Usage IS machine-readable: the gate reads /api/oauth/usage with the
# OAuth credential and takes `seven_day.utilization` and `seven_day.resets_at` from it, so there
# is no human checkpoint to go stale. Everything below is OPTIONAL.
#
# Two checks, in order: CEILING (pause at/over USAGE_CEILING_PCT, whatever the pace), then PACE
# (pause when usage runs more than USAGE_SLACK_PCT ahead of a linear burn toward the reset).
# (pause while usage is inside the last USAGE_HEADROOM_PCT points UNDER a linear burn toward the
# reset — i.e. the crons run only while at least that many points remain under the pace line, and
# they idle at each week's start until the pace line clears the headroom).
# USAGE_CEILING_PCT="90" # hard ceiling regardless of pace (default 90)
# USAGE_SLACK_PCT="5" # points over linear pace tolerated before pausing (default 5)
# USAGE_HEADROOM_PCT="5" # points the crons stay behind linear pace (default 5; positive = behind)
#
# USAGE_SLACK_PCT is RETIRED (#158): the pace comparison inverted, so a value written for the old
# name cannot be honoured or ignored silently — the gate REFUSES to gate while it is set (exit 2;
# the runner logs the reason and aborts the tick). Delete it rather than carrying its value over.
#
# FALLBACK ONLY — used when the endpoint is unreachable, e.g. the token expired or the API moved.
# Not the normal path. With neither the endpoint nor a fallback the gate is INERT and the crons
Expand Down
Loading
Loading