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.
Copy & share
Loading sections…
Connect MCP or install the Empryo skillSection exports contain only that heading’s content. Markdown and text links fetch the selected content directly, without the rest of the page.
/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 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 turnAttach 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 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=Nper run,goalLoop.maxIterationsas 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
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
{
"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). 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` |
/goal escalates models when it stalls; /review pass keeps judging the same way until the findings are gone.