<!-- Markdown mirror. Canonical: https://empryo.com/docs/agents/goal-loop -->

# The goal loop

> State a goal, and Empryo runs coder and reviewer rounds until a fresh judge returns PASS: escalation ladder, stuck detection, stand-in, and CI exit codes.

![A round: the coder works, typecheck and tests run for free, a fresh reviewer returns the verdict. A stalled loop escalates a different variable each round.](https://empryo.com/media/docs-goal-loop.svg)

*A round: the coder works, typecheck and tests run for free, a fresh reviewer returns the verdict. A stalled loop escalates a different variable each round.*

`/review` judges work you already did. The goal loop judges work it is still doing, and keeps going until the judge agrees.

You state the goal once. The coder works it. A [reviewer](/docs/agents/review) with clean context checks the repo against that goal, returns `PASS`, `FAIL` or `PARTIAL`, and the loop either stops or feeds the reason back for another round.

```
/goal the auth middleware must reject expired tokens, with a test that proves it
/goal fix the flaky upload test --max=8
/loop migrate the config loader to zod        # alias
/goal clear                                    # drop the loop, keep this turn
```

Attach images to a goal the same way you attach them to a prompt: they ride along to the coder.

## A round

| Step | What runs | Cost |
|------|-----------|------|
| 1 | The coder turn — a real turn in your lane, with your tools | Model |
| 2 | Typecheck, then the test suite | No tokens |
| 3 | `project run` when `goalLoop.runtimeProbe` is on | No tokens |
| 4 | The reviewer reads the repo against the goal and the verdict history | Model |

Steps 2 and 3 run before any model sees the round. A typecheck or test failure is an automatic `FAIL`, so the loop never spends a reviewer call to learn the build is broken.

The reviewer gets fresh context every round, its own model slot, and its own prompt-cache lane. It judges the current state of the repo, not the diff narrative, and it re-checks that nothing it passed earlier has regressed.

## When it stalls, it changes something

Feeding the same failure to the same model is how loops burn tokens. Each round changes one variable instead:

| Round | What changes |
|-------|--------------|
| 1–2 | Nothing. The reviewer's reason goes back to the coder as the next prompt |
| 3 | The **reviewer** escalates — a weak sensor reads like a stubborn bug |
| 4 | The **coder** escalates |
| 5 | Re-plan. The coder is told to stop patching and re-derive its approach |
| Cap | Stuck. The loop stops and reports why |

Escalation resolves against your [task router](/docs/recipes/task-router) and fallback chains, so the ladder spends your configured models rather than picking new ones.

## When it stops

- **PASS** — the reviewer verified the goal. The loop closes with its summary.
- **Oscillation** — the same failure reason twice in a row. Two identical verdicts mean the coder is not moving, and a third round would not move it either.
- **Round cap** — 5 by default, `--max=N` per run, `goalLoop.maxIterations` as your default.
- **Token cap** — `goalLoop.tokenCap`, counted across coder and reviewer rounds.
- **Ctrl+X** — kills the loop in any phase, mid-code or mid-review.

Only a stuck loop asks you a question. It is never a routine gate between rounds.

## Steering a running loop

Type while it runs. Your line folds into the next round's coder prompt instead of interrupting the turn, and prior verdicts stay in the prompt so the coder can see what it already tried. `/goal clear` removes the loop without killing the turn in flight.

## The stand-in

A loop that blocks on `ask_user` at 3am is a loop that achieved nothing by morning. When you are away, the stand-in answers in your place from a fresh context, logs the decision with its rationale, and falls back to the safe path when it is not confident.

| `goalLoop.standIn` | Behavior |
|--------------------|----------|
| `off` | Never decides. The normal blocking prompts run |
| `conservative` | Multiple-choice questions only (default) |
| `full` | Also decides web-search and fetch-page approvals |

Override a stand-in decision later and the correction is remembered, so it converges toward your judgment across sessions.

Headless runs treat you as away by definition: there is no keypress to detect, so the dial alone gates it.

## Where you watch it

| Surface | What you see |
|---------|--------------|
| Desktop | The goal strip above the composer: one pip per round tinted by its verdict, elapsed time, tokens spent, and a cancel button |
| TUI | The goal banner and per-round verdict rows in the transcript |
| Headless | Round progress on stderr, verdicts in the `--events` stream |

An unbounded loop shows an ∞ marker instead of a row of empty pips.

## In CI

```bash
empryo --headless --loop "the migration must be reversible and covered by a test"
empryo --headless --loop "fix the failing suite" --max-iterations 8 --json
```

| Exit code | Meaning |
|-----------|---------|
| `0` | PASS |
| `1` | Stuck — oscillation, round cap, or token cap |
| `2` | Timeout (`--timeout`) |
| `130` | Aborted |

## Configuration

```json
{
  "goalLoop": {
    "maxIterations": 5,
    "tokenCap": 2000000,
    "standIn": "conservative",
    "runtimeProbe": false
  }
}
```

Every round is a real coder turn plus a real reviewer run, priced into the tab's totals like any other work ([cost tracking](/docs/concepts/cost-tracking)). A loop that runs its cap is five judges and four coder turns, so state a goal specific enough to converge on: "rejects expired tokens, proven by a test" converges, "improve auth" does not.

## Goal loop or review-until-pass

| You have | Use |
|----------|-----|
| A goal, and no code yet | `/goal` |
| Code that exists, and no goal but "hold up under a fresh reading" | [`/review pass`](/docs/agents/review) |

`/goal` escalates models when it stalls; `/review pass` keeps judging the same way until the findings are gone.

## Next

#### The reviewer

The judge behind every round: scope, verdicts, and acting on findings.

#### Steering

Redirect a running turn or a running loop by typing.
