Work within a spend limit

A hard dollar ceiling, and what stella does as it approaches one — where the abort lands and what the scope gate stops before you spend anything.

You have a number in mind and you do not want to find out you passed it afterwards. This guide is about the mechanisms that hold a ceiling, in the order they fire — one of them before any money is spent at all.

The two places a run is stoppeda plannothing writtenscope reviewsteps · files · costthe stepswhere the money goessettledcost in the receiptover a thresholdstopped$0 — nothing editedover the ceilingabortexit 1 — work on disk staysthe meter is read between steps, never inside a tool call — an abort leaves work, not wreckage

The flag

stella --spend-limit 2.00 run "port the utils module to the new API"

--spend-limit is a global flag, so it can go on either side of the subcommand — but the habit of putting it first is a good one, because it reads as a property of the whole invocation, which is what it is.

Observed — no --spend-limit

The default. Spend is metered for the end-of-run cost summary and the local store, but never blocks. Use it to find out what a class of task actually costs before picking a ceiling for it.

Enforced — --spend-limit <usd>

A hard cap on the whole run or session — not per turn, not per round. Must be positive and finite.

Set it once for a shell or a CI job instead:

export STELLA_SPEND_LIMIT=5

Where the abort lands

This is the part worth trusting, because it determines whether a tripped spend limit leaves you with work or with wreckage.

The meter is checked between steps and stages — never in the middle of a tool call. An abort therefore never leaves a half-applied edit, a partially written file, or a tool whose side effects landed but whose result never made it back into the transcript. Work already committed to disk stays; nothing is rolled back.

The exit code is 1, the same as any other failure. If you need to distinguish the spend limit stopped this from this failed, read the JSON:

stella --spend-limit 2.00 run --output-format json "…" | jq '{status, cost_usd, verdict}'

--spend-limit is metered against catalog prices. Local and air-gapped models have no catalog price, so the dollar columns read zero and a spend limit has nothing to meter against. Bound those runs with --test-command and goal-mode round caps instead — see Local and air-gapped.

The gate that fires before you spend

A spend limit stops a run partway. The scope review stops it before the first edit, which is usually the outcome you actually wanted.

When a plan's blast radius crosses any of three thresholds, stella pauses and shows you the plan:

max_steps

More than this many steps in the plan.

Default 5

max_files

Estimated files-touched count above this.

Default 8

max_cost_usd

Estimated cost above this.

Default 1.00

Every comparison is a strict >, so a plan sitting exactly on a threshold passes without a gate. At the card, a approves, t trims to the largest prefix that would not have tripped review, x aborts — and anything longer than one of those words is read as what you want changed, sending the plan back to the planner with your note attached.

Headless runs must opt in to bypassing this gate explicitly — agent_engine_config.headless_scope_bypass: "on". Without it there is no silent auto-approve: a plan over the thresholds ends the run. That is deliberate, because a scope gate that quietly approves itself in CI is not a scope gate.

What the money is actually spent on

Before tuning anything, know the shape of the bill — most attempts to economize target the wrong stage.

StageModel callsCost share
Triage1negligible
Plan1 (+1 repair, rarely)small
Witness~1 engine turnonly when you pass no --test-command
Execute5–40+ stepsmost of the bill
Verify0free — the deterministic ladder
Verifier0–1 per verificationcents

Three consequences follow, and they are most of what you need:

  1. The worker's output price dominates. Moving the worker to a cheaper model changes the bill several-fold. Moving the verifier barely changes it at all.
  2. A flagship verifier is cheap insurance. One verifier call is a few thousand input tokens and under a thousand out. Keep it in a different family than the worker.
  3. --test-command is the strongest cost flag on the CLI. It arms the deterministic flip oracle, so most runs finish on evidence alone with the verifier skipped entirely — and it skips the witness-author call too.
# Cheapest correct shape: deterministic oracle armed, hard ceiling on top.
stella --spend-limit 1.50 run --test-command "cargo test -p api" \
  "add cursor pagination to /orders"

The multipliers

A ceiling only helps if you know what is pushing against it.

Goal mode

Repeats judged rounds until the objective is met, up to 8. Each round is a full working turn. stella monitor is goal mode, so it has the same shape.

Fleets

One pipeline per task. Worker spend meters into the parent spend limit, so --spend-limit is a single shared ceiling across the whole fan-out rather than a per-worker allowance — and the run stops early once it trips.

Best-of-N

Generates N candidate executions and keeps the best by verification score, at N× the execution cost. Strictly opt-in, for exactly this reason.

Revisions

A failed verification sends the worker back with the evidence, bounded at 2 revision turns by default.

When the transcript outgrows the window

A long run's cost is not only what it does — it is how much conversation it carries while doing it. When the transcript approaches the compaction budget (150,000 tokens by default), stella compacts it and keeps going, rather than failing or silently truncating.

Compaction applies four mechanisms, least-lossy first:

  1. Dedup. A byte-identical tool output appearing more than once keeps only its earliest copy; later ones become a pointer to it.
  2. Supersession. When the same call ran twice with identical input, only the latest result reflects current state — the older ones are stale by construction and get stubbed.
  3. Aging. Still over budget, large old outputs are middle-out truncated to head plus tail. The head carries the tool's framing (a PASSED/FAILED line, file headers) and the tail carries the errors, so what survives is the part worth reading.
  4. Eviction. Only then are the oldest large tool outputs replaced with a stub — and a result whose call is still the most recent one is never evicted.

The system message and the latest user message are never touched.

Dedup keeps the earliest copy on purpose. Byte-identical content is position-independent, so stubbing the later one puts the change in the newest part of the conversation and leaves the provider's prompt-cache prefix byte-identical. Compacting the other way around would save tokens and then lose far more money to a cold cache than it saved.

If a conversation is still over budget after all four passes, an overflow summarizer runs — a real extra model call, recorded at --call-seq 1 for that step. Seeing those in stella inspect is the sign a run's transcript, not its work, is what got expensive.

Picking the number

Set the spend limit by observation, not by intuition. Run the class of task once unbounded, read what it cost, then set a ceiling with headroom:

stella run "the representative task"           # observed mode
stella stats --format text                    # what it actually cost
stella --spend-limit 2.00 run "the next one"   # now with a ceiling

The number to steer on over time is $/resolved, not sticker price — a cheap model that needs three attempts is not cheap. The Observatory tracks it per model, and Examples & recipes has complete configurations at four different price points.

Next