Skip to content

Halt Reasons

A loop runs until an external check passes, a guard trips, a budget is spent, or a human is required. Never because an agent reports it is done. Every halt emits a computed reason - not a flag - and a replayable receipt.

This page is the taxonomy as doctrine. It is the page behind the claim: our loops stop, and explain why.

A halt is not one fact. It is three, and they are deliberately separated.

  1. The halt_reason string is the integration contract. It is stable, precise, and the unit other tools key on. One of a closed set of named reasons. The string MUST NOT change meaning within a major spec version.
  2. The exit code is a coarse CI class. A small integer CI can branch on without parsing JSON. Many strings collapse into one code. The code answers “what kind of stop”; the string answers “which one.”
  3. Every reason is computed from observed state. A reason is the output of a detector reading durable state - the failing-test trajectory, a fingerprint hash, a coverage delta, a manifest hash, a spend ledger, a model-identity tuple, a sequential flake bound. It is never an argument you pass in.

Keep these three straight and the rest of the page follows.

The halt_reason string is the stable integration contract; the exit code is a coarse class. Existing codes 0/10/11/12/20/30/40/50 predate this taxonomy; 13-19 are new classes. The map below is the full normative taxonomy; which strings emit today versus stay reserved is tracked in SPEC section 11 - today classes 13, 14, 15, 16, 17, 18, and 19 emit their core reasons (class 18 via inspect-cost, class 15 via the doctor --mutate-cmd adequacy canary), while only class 11’s task-list reasons are reserved. Every reason MUST be computed from observed state - --halt-reason is a hidden test fixture only.

exitclasshalt_reason stringsowner
0nominal(loop ran, no halt)LoopExec
10convergedsuccess_condition_metLoopExec
11terminal-blockedno_actionable_tasks, human_requiredhuman
12iteration-capmax_iterations_reachedLoopExec
13integrityblocked_path_modified, reward_hacking_detected, metric_integrity_violation, credential_scope_invalid, objective_unverifiedLoopExec (det.)
14oracle-untrustedcheck_flaky, check_has_side_effects, check_not_hermetic, hermeticity_violationLoopExec (det.)
15check-inadequatecheck_inadequateLoopExec (det.)
16resumable-judgmentescalation_pending, reviewer_rejectedMusketeer / human
17no-convergenceno_progress_detected, same_failure_repeated, oscillation_detected, same_test_regressed, unsatisfiable_constraints, infeasible_suspectedLoopExec (det.)
18budgetbudget_exceeded, cost_anomalyLoopExec (det.)
19liveness/driftheartbeat_stale, model_drift_detected, comprehension_debt_exceeded, context_budget_unsatisfiableLoopExec (det.)
20invariantinvariant_failedLoopExec
30workspaceworkspace_invalid, isolation_unsatisfiableLoopExec
40executionexecution_failureLoopExec
50internalinternal_errorLoopExec

The (det.) marker means deterministic: LoopExec-owned, reproducible offline, the part of a halt that a replayable verdict can re-verify without re-running the agent.

The split is intentional, and it earns its keep at 3am.

A CI pipeline does not want to parse JSON to decide whether to page. It reads the exit code. Exit 17 means no-convergence - alert the owner, do not auto-retry blindly. Exit 14 means the oracle itself is untrusted - quarantine the check before believing any verdict. Exit 18 means budget - the loop stopped because it was told to, not because it failed.

The string is for the integration that wants precision. Exit 17 covers six distinct strings. oscillation_detected and infeasible_suspected share a class but demand opposite responses: one is a thrashing loop you can sometimes break by widening context; the other is a target with no reachable green, where more budget burns forever. The exit code routes; the string decides.

The single most important property of this taxonomy: no halt reason is a flag.

In a stub, a loop can be told how it ended. The contract forbids it. --halt-reason is demoted to a hidden test fixture - present so the receipt schema and exit-code map can be exercised, never a path a real run takes. A conformant run reaches a halt reason exactly one way: a detector observes state and computes it.

  • no_progress_detected is computed from the failing-test-ID set F_i showing no strict decrease over K iterations - not from an agent saying it is stuck.
  • metric_integrity_violation is computed from the collected-test-set losing a member, or an assertion count dropping, against an immutable baseline t0 - not from a diff against HEAD~1.
  • check_flaky is computed from a sequential Wilson lower bound on the check’s stability falling below confidence_target - a maintained confidence bound, not a one-time probe.
  • model_drift_detected is computed from the observed model-identity tuple no longer matching the pinned one.

This is what makes “explain why” honest. A computed reason can be re-derived from the receipt offline. A declared one is just a label the loop chose to print.

Three owners, drawn along one line: what a number can decide, what a number cannot, and what only a person can.

ownerdecidesexample reasons
LoopExeceverything a number can decide, reproducible offlinesuccess_condition_met, no_progress_detected, metric_integrity_violation, budget_exceeded, check_flaky
Musketeerwhat a number cannot express, layered on top of the deterministic gatereward_hacking_detected, reviewer_rejected
humanthe irreducible decision and the ackhuman_required, resolving escalation_pending

LoopExec owns the exit codes, the F_i trajectory, a fingerprint hash, a coverage/mutation delta, a manifest hash, a spend ledger, a model-identity tuple, a sequential flake bound. Musketeer owns only the judgments a number cannot capture - semantic reward hacking, a reviewer’s veto - and these sit on top of the deterministic metric_integrity_violation, never in place of it. Humans own the one thing neither layer can: the decision to proceed, and the signed ack that records it.

This is why reward_hacking_detected shares exit class 13 (integrity) with the deterministic metric_integrity_violation. The number-decidable violation fires first and stands on its own; the judge’s semantic veto is an additional, advisory layer over the same class - it cannot rescue a loop the deterministic gate already condemned, and it is not required for that gate to halt.

How to read the receipt, the string, and the exit code together. JSON below follows the --json contract - one object, documented fields - with the process exit code noted alongside.

1. The green that wasn’t - guards dominate success

Section titled “1. The green that wasn’t - guards dominate success”

A check goes green at iteration 7. The loop does not halt success_condition_met.

{
"tool": "loopexec",
"version": "0.2.0",
"status": "halted",
"run_id": "fix-auth-suite",
"iteration": 7,
"halt_reason": "metric_integrity_violation",
"errors": ["collected-test-set lost member: test_token_refresh_expiry"]
}

Exit code: 13 (integrity).

How to read it. Guards are evaluated against the immutable baseline t0 before any green branch can fire - guards dominate success (G-before-S). The suite passed, but it passed because a test was deleted. The collected-test-set lost a member, so the integrity gate computed metric_integrity_violation and the green was never allowed to become success_condition_met. The exit code routes CI to “integrity - do not ship.” The string names the exact invariant. The errors line names the lost test. You revert to t0; you do not trust the green.

2. Out of iterations - raise the limit, or never retry

Section titled “2. Out of iterations - raise the limit, or never retry”

Two runs both stop at the iteration cap. They are not the same halt.

{ "halt_reason": "max_iterations_reached", "iteration": 20, "status": "halted" }

Exit code: 12 (iteration-cap). |F_i| was still strictly decreasing at the cutoff.

{ "halt_reason": "infeasible_suspected", "iteration": 14, "status": "halted" }

Exit code: 17 (no-convergence). The failing-test set stopped shrinking, with no reachable green in evidence.

How to read it. max_iterations_reached while progress was still being made means the loop ran out of budget, not road - raise the limit and retry is rational. infeasible_suspected means there may be no reachable green; retrying burns budget forever. explain-halt is specified to draw exactly this line, so the operator who reads the halt at 3am knows whether more budget is a fix or a waste. Same family of “it stopped without converging,” opposite correct response - and the exit code (12 vs 17) already separates them before you read a word.

The loop halts not on the work, but on the instrument.

{
"tool": "loopexec",
"version": "0.2.0",
"status": "halted",
"run_id": "payments-refactor",
"iteration": 4,
"halt_reason": "check_flaky",
"errors": ["stability lower bound 0.91 < confidence_target 0.99 (dimension: test_order)"]
}

Exit code: 14 (oracle-untrusted).

How to read it. Each in-loop check run is a free Bernoulli sample. The runtime maintains a sequential lower bound on the check’s stability; here it crossed below confidence_target, and the adversarial dimension that broke was test order. The loop refuses to treat an unstable check as a stop condition - a flaky oracle cannot certify convergence. Note the neighbor in the same class: check_has_side_effects is a deterministic-but-non-idempotent check, cured by reset, not by editing the check. Exit 14 says, uniformly, “fix the instrument before believing any verdict it produced.”

The legacy bare "blocked" string was one word with three meanings. The taxonomy splits it, and the split is not cosmetic - each shard has a different owner and a different exit class.

wasbecomesexitmeaning
blockedno_actionable_tasks11nothing left to select; terminal, human-owned
blockedblocked_path_modified13a guard tripped; integrity, LoopExec-owned
blockedhuman_required11a person must decide; terminal, human-owned
  • Determinism - the maintained confidence bound behind check_flaky, and why only verdicts replay.
  • Loop Engineering - the discipline these halts enforce: bounded, stateless, auditable loops.
  • CLI Reference - the --json contract, the exit-code list, and the capability-status matrix.