Skip to main content
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.
A review loop that cannot converge keeps spending tokens until a person notices. 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.
No limit is on by default. Set one with 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 with override-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 the state.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 checked and 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-tree state.json seen from a linked worktree, a failing ledger read). The ledger side and the token signal read not-read with the reason, tracking is unknown where 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 failing certify 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-review still 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 guard section, only while a limit is configured.

Set the limits

Configure the limits spend-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 a spend-guard STOP, and re-arm the guard from the current count.
An override appends {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 tokens override 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 resolved review model for rounds and implement for 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 user is 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 --preauthorize does not reach this verb.
  • A later pass spends a rounds override. 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 with revokedAt and revokeReason, keeps the entry, and re-arms the guard at once: the baseline falls back to the previous live override, or to 0. --escalated-to is refused alongside --revoke, and a revoke with nothing live exits non-zero. The record is never deleted.