New in 0.52.0. Opt-in: no limit is on by default. The gate-round counter is always recorded. This page covers
spend-guard, set-spend-guard and override-spend-guard.spend-guard is the
check. It counts an epic’s failed review rounds, compares its attributed token spend with a ceiling, and
exits 2 while the epic is stuck, printing a STOP block that names who to hand the failure to. It
dispatches nothing and kills nothing: pm is an instruction layer, and the agent or the user acts on the
STOP.
set-spend-guard (see Set the limits below); until then
the guard reports counts and never trips. The gate-round counter itself is always recorded, with or without
a limit: every accepted record-gate-review updates epic.gateRounds.gate<N> in the same write as the
verdict.
What it reports
The report prints each signal’s count, limit and the layer each limit came from (
epic, lane:<lane> or
project), which of the two rounds counts decided, the live overrides, and the escalation target for each
signal. --json prints one object: epic, stuck, signals.rounds and signals.tokens (each with
state, count, limit, source and baseline), overrides, escalation, tracking, coverage,
notes, and stop when stuck.
Exit codes. 0: no signal is tripped. 2: a signal is tripped (the STOP block is printed first). 1: wrong
arguments or an unknown epic, and nothing else. Exit 2 is also Claude Code’s hook “block” code, so
never wire spend-guard into a hook; the rules block asks the agent to run it before each review or fix
dispatch instead.
The STOP
A STOP means: stop dispatching same-tier retries for the epic, hand the failure evidence (the last failing findings or tests, the verdicts recorded, the diff) to the escalation target and ask “what is wrong and how do we resolve it?”, then record the decision withoverride-spend-guard. Epic-level autonomy does not
bypass a STOP, and no autonomy grant covers continuing past it.
The escalation target is the next model above the resolved model of the role tied to the signal (review
for rounds, implement for tokens) in the order haiku < sonnet < opus < fable; the user is always also
valid. The resolved model counts whatever its source, a built-in default included, and the block names the
source. Only fable, a model outside that order, or no model at all gives user.
Which tree it reads
Limits, tallies and overrides come from thestate.json of the tree the verb runs in, the file
record-gate-review, set-spend-guard and override-spend-guard write there. Only the tracking switch and
the ledger come from the main working tree, as spend reads them.
It degrades, and says so
- Tracking off. The token ceiling is
not checkedand the rounds signal uses recorded verdicts only. The report says ledger review rounds were not read because tracking is off. No spend or transcript path is opened. - Ingest cannot run (another ingest holds
ingest.lock, the main tree’s HEAD is detached, or--no-ingest). The existing ledger is read, both ledger-fed signals are computed from it and may trip, and the report says the ledger was not refreshed, why, and that counts may be stale. - The ledger cannot be read (an unreadable
enabledAt, an unreadable main-treestate.jsonseen from a linked worktree, a failing ledger read). The ledger side and the token signal readnot-readwith the reason, tracking isunknownwhere it could not be told (never “off”), the recorded-verdict check still applies, and the exit code is 0 or 2, never 1. - A withdrawn verdict. Withdrawing a gate verdict does not un-count the round. When a gate’s latest verdict is withdrawn the report says so, and that a withdrawn pass still anchors the ledger side and override liveness.
What it does not detect
Repeated failingcertify runs (a certify run is bound to the index, not to an epic, so there is nothing to
count against), per-test failures, and review rounds nobody marked: with tracking on a dispatch must begin
with pm-spend: epic=<id> role=review phase=review round=<n>, or its tokens are unattributed and its round is
not seen. The brief’s STOP line (below) is computed from recorded verdicts alone, because no hook reads the
ledger, so it can miss a ledger-only trip; the per-dispatch spend-guard call covers that.
Where else it speaks
record-gate-reviewstill records every verdict, however many have failed. When the accepted verdict leaves the recorded rounds signal tripped, the same invocation prints the STOP block on stderr after the success line and exits 0.- The SessionStart and PreCompact brief carries one
STOP: spend guard —line for a stuck active epic. - The managed rules block carries a
## Spend guardsection, only while a limit is configured.
Set the limits
Configure the limitsspend-guard checks. No limit is set by default: a repo that never runs this
sees no new behaviour except the always-on gate-round counter. init and upgrade never set one;
upgrade lists the guard as NOT SET: spendGuard until a limit exists.
With neither
--epic nor --lane it writes the project layer; --lane writes one lane’s; --epic
one epic’s. The two are refused together. Each field resolves independently, epic first, then lane, then
project, and a detour epic inherits nothing from the epic it paused. A stored value that fails its check
is ignored and reported, and resolution falls through. This is a separate cascade, not a fourth execution
profile field, so profile does not show it; spend-guard does, with each limit’s source.
Every set has its inverse. A layer left with no field is removed from the record, so it is
indistinguishable from one never configured. Unsetting a field that is not set exits 0, says nothing
changed and writes nothing. A call that names nothing to set, sets and unsets the same field, or gives a
value out of range, is refused and writes nothing.
--tokens is only checked while spend tracking is on. With tracking off the value is recorded and the
output says it will not be checked until set-spend-tracking on; the guard counts rounds only.
When the first limit appears (a maxFailedRounds or tokenCeiling at the project, at any lane, or on any
epic that is not archived) the managed rules block gains a ## Spend guard section; when the last goes it
is removed. A warnAt alone is not a limit. A suggested starting point for a repo that has seen a loop is
set-spend-guard --max-failed-rounds 2.
Record the escalation decision
Record who took the decision at aspend-guard STOP, and re-arm the guard from the current count.
{signal, baseline, escalatedTo, reason, at} to epic.spendGuardOverrides. The
baseline is the signal’s count at that moment, so the guard trips again after limit more rounds, or
after another ceiling’s worth of tokens.
- Only while the signal is tripped. An override given in advance would be a pre-authorization to loop,
which is what the guard exists to prevent. It is refused, writing nothing, when the signal is not tripped
now. A
tokensoverride also needs spend tracking on: with it off there is no count to baseline. - It must name who decided, and the model must rank at or above the computed escalation target
(
haiku < sonnet < opus < fable, from the resolvedreviewmodel for rounds andimplementfor tokens). The model that failed cannot override itself. After one live model override for a signal, the next escalation is the user’s and only--escalated-to useris accepted, so a trip, an override and a trip cannot repeat without a person. The engine records the claim and checks its rank; it cannot know which model actually ran. - No autonomy grant covers it. The override is the escalation decision, and pre-authorizing it would
remove the stop.
set-autonomy --preauthorizedoes not reach this verb. - A later pass spends a
roundsoverride. A pass resets the streak the baseline was measured against, so the baseline no longer applies once a pass is recorded after the override. - Revoke (
--revoke --reason) marks the latest live override for that signal withrevokedAtandrevokeReason, keeps the entry, and re-arms the guard at once: the baseline falls back to the previous live override, or to 0.--escalated-tois refused alongside--revoke, and a revoke with nothing live exits non-zero. The record is never deleted.

