> ## Documentation Index
> Fetch the complete documentation index at: https://pm-plugin.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# /pm:spend-guard — Stuck-Loop and Token-Ceiling Guard

> Check whether an epic is stuck in a review loop or past its token ceiling, set the limits, and record the escalation decision. No limit is on by default.

<Info>
  **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`.
</Info>

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.

```bash theme={null}
conductor.mjs spend-guard <epicId> [--json] [--no-ingest]
```

No limit is on by default. Set one with `set-spend-guard` (see [Set the limits](#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

| Signal | Counted from | Trips when |
| - | - | - |
| `rounds` | the larger of the epic's highest recorded **fail streak**, and (tracking on) the distinct `round` values on its attributed `phase=review` ledger rows since the epic's last recorded pass, minus one | the count, minus the live override baseline, reaches `maxFailedRounds` |
| `tokens` | the epic's **attributed** total from the spend ledger (the figure `spend` reports) | the total, minus the live override baseline, reaches `tokenCeiling`; it warns from `warnAt` percent (default 80) |

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.

```bash theme={null}
conductor.mjs set-spend-guard [--epic <id> | --lane <lane>] [--max-failed-rounds <n>] [--tokens <n>] [--warn-at <pct>] [--unset max-failed-rounds|tokens|warn-at|all]
```

| Flag | Meaning |
| - | - |
| `--max-failed-rounds <n>` | a positive whole number. The epic is stuck when its rounds count reaches it. |
| `--tokens <n>` | a positive whole number of tokens. The epic is stuck when its attributed spend reaches it. |
| `--warn-at <pct>` | a whole number 1 to 99: the share of the ceiling at which the report warns. Default 80, a convention for when to start warning, not a measured threshold. |
| `--unset <field>` | removes that field from this layer only. Repeatable. `all` clears the layer. |

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.

```bash theme={null}
conductor.mjs override-spend-guard <epicId> --signal rounds|tokens --escalated-to user|fable|opus|sonnet|haiku --reason "<why>"
conductor.mjs override-spend-guard <epicId> --signal rounds|tokens --revoke --reason "<why>"
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.