Specification
Documentation source v0.5.0-rc.1. Published releases
Status: normative. This is the single source of truth for the loopexec contract. The binary (cmd/loopexec) and the docs site (loopexec.dev) MUST conform to this document. Where docs/loop-contract.md and this file disagree, this file wins; loop-contract.md is the task_list mode profile of this spec.
Spec version: 2.0.0 | Conformance keywords: MUST / MUST NOT / SHOULD / MAY per RFC 2119.
1. What loopexec is
loopexec is a deterministic runtime for loop engineering: it runs bounded, stateless, auditable work loops that progress through an external check, durable state, isolated execution, and explicit stop conditions.
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 (never a flag) and a replayable receipt.
loopexec is not an agent, a model, or a task runner. It composes with those by governing how work is executed, recorded, and resumed.
2. Topology - the two machines, made explicit
loopexec hosts two control topologies. The mode is explicit and recorded in every receipt.
loop.mode | Stop condition | Gate | Profile |
|---|---|---|---|
check_fixpoint | external check returns success_exit_code | the application oracle (test/build/lint/score) | this document, section 4 |
task_list | all SMALL plan tasks completed | small check --strict (state hygiene) + a per-task acceptance oracle | docs/loop-contract.md |
Invariant T1. check_fixpoint is the degenerate one-task plan whose acceptance criterion is the external check. task_list MUST attach a real per-task acceptance oracle; state-hygiene validation alone is NOT an acceptance oracle (it cannot decide application correctness). A task_list loop without a per-task oracle MUST refuse to mark tasks completed and MUST halt check_inadequate.
Invariant T2. The receipt MUST record, per iteration, which oracle was evaluated at which commit. A receipt that cannot answer “which oracle, at which commit, under which pinned model, having spent how much, with which guards intact” is not a halt record.
3. The oracle contract (the check)
O1 - External. The check MUST be an external process returning an exit code, independent of the agent. The agent’s self-assessment is not a check.
O2 - Determinism as a maintained confidence bound (not a one-time probe).
loopexec probe-checkMUST report an achieved confidence bound on the check’s flake rate, not a pass/fail of N runs. The run count is derived from a targetmax_flake_rate, not a magic constant.- During a loop, each in-loop check invocation is treated as a free Bernoulli sample; the runtime maintains a sequential (Wilson) lower bound on stability and MUST halt
check_not_deterministicwhen it drops belowconfidence_target. - Probing MUST be adversarial where configured (test order, seed, clock, concurrency, load) and MUST report which dimension broke.
O3 - Hermeticity. A check declared deterministic MUST be hermetic: frozen clock/seed/TZ/locale, OS-assigned (:0) ports, ephemeral fixtures, pinned tool versions. A non-hermetic check MUST be rejected by doctor (check_not_hermetic), not silently looped on.
O4 - Adequacy != determinism. A check that returns 0 regardless of the diff is deterministic and useless. doctor MUST verify adequacy: every changed/added line is exercised by the check (coverage delta), and a mutation canary injected into changed lines MUST turn the check red. Failure => check_inadequate.
O5 - Determinism != idempotency. A deterministic but side-effecting check (passes once, fails on rerun in the same workspace) is distinct from a flaky one; it MUST be reported check_has_side_effects and cured by reset, not by editing the check.
O6 - Single capture. The runtime MUST run the check once per iteration and derive both the green/red verdict and the failing-test set from that one captured artifact {exit_code, stdout, stderr}. It MUST NOT re-invoke the suite to scrape failure text.
4. Iteration lifecycle - check_fixpoint
Each iteration MUST execute as one ordered transaction:
- Build context - a narrow, budgeted slice (
build-context). Token budget is a true ceiling, measured with a code-calibrated estimator; a slice that cannot fit even after widening tiers MUST emitcontext_budget_unsatisfiable, never silently evict relevant files. - Execute the agent in the agent zone (section 7).
- Run the check once (O6); capture the artifact; parse the failing-test set
F_i. - Guards dominate success (G-before-S). Evaluate guards (section 6) BEFORE any green branch can fire. A check that went green because a guard was violated (e.g., a weakened test) MUST classify as the guard reason, NOT
success_condition_met. - Acceptance (anti-regression ratchet). Accept the iteration’s diff only if the passing-test set is a superset of the last accepted set (primary guarantee). Optionally enforce a numeric
score >= best_so_farratchet with an acceptance band. On violation, revert to the last accepted commit and re-prompt. - Progress / feasibility. Update
F_i. Halt on no strict decrease in|F_i|over K iterations, on a previously-passing test re-enteringF(oscillation), or on a contradictory pair across >=2 cycles. Distinguish “still improving at cutoff” (raise the limit) from “stalled/infeasible” (do not retry). - Checkpoint the receipt (section 8). Every iteration MUST append a typed JSONL event and update durable state.
- Continue or halt. Halt only on a computed condition from section 5; otherwise iterate. Before declaring
success_condition_met, the full check phase MUST pass (a targeted subset MAY drive intermediate iterations).
5. Halt reasons -> exit codes (canonical map)
The halt_reason string is the stable integration contract; the exit code is a coarse class CI can branch on without parsing JSON. Existing codes 0/10/11/12/20/30/40/50 predate this spec; 13-19 are new classes introduced here. The table below is the full normative taxonomy; which halt_reason strings the binary emits today versus holds reserved is tracked in section 11 - as of v0.2.0, every class 13-19 emits (their core reasons; 18 via inspect-cost, 15 via doctor --mutate-cmd), while only class 11’s task-list reasons (the task_list topology) stay reserved until their detectors ship. Every reason MUST be computed from observed state - --halt-reason is a hidden test fixture only.
| exit | class | halt_reason strings | owner |
|---|---|---|---|
| 0 | nominal | (loop ran, no halt) | LoopExec |
| 10 | converged | success_condition_met | LoopExec |
| 11 | terminal-blocked | no_actionable_tasks, human_required | human |
| 12 | iteration-cap | max_iterations_reached | LoopExec |
| 13 | integrity | blocked_path_modified, reward_hacking_detected, metric_integrity_violation, credential_scope_invalid, objective_unverified | LoopExec (det.) |
| 14 | oracle-untrusted | check_flaky, check_has_side_effects, check_not_hermetic, hermeticity_violation | LoopExec (det.) |
| 15 | check-inadequate | check_inadequate | LoopExec (det.) |
| 16 | resumable-judgment | escalation_pending, reviewer_rejected | Musketeer / human |
| 17 | no-convergence | no_progress_detected, same_failure_repeated, oscillation_detected, same_test_regressed, unsatisfiable_constraints, infeasible_suspected | LoopExec (det.) |
| 18 | budget | budget_exceeded, cost_anomaly | LoopExec (det.) |
| 19 | liveness/drift | heartbeat_stale, model_drift_detected, comprehension_debt_exceeded, context_budget_unsatisfiable | LoopExec (det.) |
| 20 | invariant | invariant_failed | LoopExec |
| 30 | workspace | workspace_invalid, isolation_unsatisfiable | LoopExec |
| 40 | execution | execution_failure | LoopExec |
| 50 | internal | internal_error | LoopExec |
Ownership stance. LoopExec owns everything a number can decide (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 what a number cannot express - reward_hacking_detected, reviewer_rejected - layered on top of the deterministic metric_integrity_violation, never replacing it. Humans own the irreducible decision and the ack. Judgment is never the stop oracle: it can veto a green loop or escalate, but it cannot promote a red one to green.
Migration. The legacy bare "blocked" string (one word, three meanings) is split: no_actionable_tasks (11), blocked_path_modified (13), human_required (11).
6. Guards & metric integrity
Guards are evaluated against an immutable baseline t0 captured at loop start, BEFORE any green declaration (G-before-S, section 4.4), covering working-tree + staged + committed + untracked changes.
blocked_paths- globs the loop MUST NOT modify (default includestest/,infra/,.env,.git/). Violation =>blocked_path_modified.- Metric integrity (supersedes
git diff --quiet -- test/): the collected-test-set MUST NOT lose a member; test-count and AST-level assertion-count MUST NOT decrease; a protected-manifest hash over the full test-determining surface (runner/coverage config, CI yaml, test-dep lockfile) MUST be stable; coverage MUST NOT drop below thet0floor. Tests SHOULD run from a read-only mount frozen att0. Violation =>metric_integrity_violation. - Judge (advisory). A cross-model, cross-lab judge MAY veto a green iteration (
reviewer_rejected/reward_hacking_detected). It receives a behavioral oracle (check log, result delta, collected-set delta, coverage/mutation signal), treats the diff as untrusted data, and never replaces the deterministic gate.
7. Isolation & secrets (two zones)
A single --network none container cannot both run a cloud agent and isolate untrusted code. Isolation MUST be two zones sharing only a work volume that is a detached clone (a git worktree is ergonomic, NOT a security boundary - it shares the object DB/refs/remotes/hooks/credentials).
- Agent zone - reasoning surface. Network: default-deny + egress allowlist to the model endpoint only, via an auditing proxy. Credentials: exactly one per-run minted, short-TTL, spend-capped key (never a
~/.model-homebind-mount). Filesystem: RW/work, RO elsewhere, no$HOME. - Exec zone - untrusted code execution (check/build/test). Network:
none. Hermetic (O3). Ephemeral: reset each run. doctorpreconditions (fail-closed): rejectnetwork: nonefor a cloud/local-host model (isolation_unsatisfiable); reject any~/.model-homemount (credential_scope_invalid); rejectexec_zone.network != none; require an egress allowlist; warn onworktree+ credentialed origin.
8. Receipts, replay, and state
Receipt (per run). MUST pin everything that determines output, or fail closed (workspace_invalid): the model-identity tuple {provider, model_id, version/build, quantization, runtime, endpoint}, recorded sampling params {temperature, top_p, seed, max_tokens}, a context manifest [{path, content_sha256}], parsed cost actuals {prompt_tokens, completion_tokens, usd}, the per-iteration check command + exit code + commit, the computed halt_reason, and the achieved determinism confidence. Receipts SHOULD be signed (attest).
Replay vs re-execute (MUST NOT be conflated):
replay= VERIFY. Re-run the deterministic check against the recorded end-state and confirm the fingerprint matches the receipt. Agent-free, budget-free, deterministic. Answers “does this receipt’s verdict hold?”reexecute= RE-RUN. Run the live agent loop again. Non-deterministic (the LLM samples); reports a statistical match, never byte identity. Budget-burning;--confirm-gated.
A live-LLM trajectory is not reproducible; only the verdict is. Marketing copy MUST say “replayable verdicts,” not “replayable runs.”
State (durable, resumable). The implemented snapshot is .loopexec/state.json with per-run snapshots. The target full-state profile MUST carry at least: phase, iteration, last_green_commit, open_failures, cumulative_usd, baseline (Step-1 measurement), determinism_probe result, metric_integrity snapshot, model_pin, and escalation state. In the full-state profile, the runtime MUST revalidate determinism_probe and metric_integrity before the first agent call and halt on drift.
9. Cost & liveness
- Budget: strict monetary caps require run-total accounting plus credible per-call upper bounds enforced by the invoking adapter/provider (section 12). Observed metering is explicitly post-call and MUST NOT claim a preventive cap. Unmetered usage reports unknown dollars. Parsed actuals include builder, critic, and explicitly priced tool/test calls.
cost_anomalyremains distinct frombudget_exceeded. A historical quantile alone is not a hard reservation bound. - Liveness. The heartbeat MUST be read by an external watchdog (
loopexec watch) that times out a wedged agent call and emitsheartbeat_stale. Every exit - including a non-zero agent exit - MUST pass through the typed logger; aset -e-style silent death is non-conformant. - Comprehension.
diffs_merged_unreadMUST be tracked; the loop SHOULD haltcomprehension_debt_exceededafter a configured threshold, cleared by a signedack. (A forcing/visibility gate, not proof of comprehension.)
10. Command surface
| Command | Purpose | Exit semantics |
|---|---|---|
init | create .loopexec/ runtime directory | 0 / workspace_invalid |
run | the real iterating loop (section 4); computed halt | per section 5 |
run --once | single iteration (debug); absorbs legacy step | per section 5 |
probe-check | determinism as a confidence bound (O2); agent-free | 0 / class 14 |
build-context | bounded relevant slice; budget-unsatisfiable handling | 0 / context_budget_unsatisfiable |
doctor | precondition gate: determinism + hermeticity + adequacy + isolation | 0 / class 14 |
check | SMALL/state-hygiene validation only (NOT the application oracle) | 0 / invariant_failed |
ratchet | inspect/advance the monotonic acceptance frontier | 0 |
replay | VERIFY a recorded receipt (agent-free, budget-free) | 0 / class 13 |
reexecute | live re-run; statistical match; --confirm | 0 |
report / status | render receipt / live tail | 0 |
inspect-cost | composite cost model + caps + anomaly | 0 |
explain-halt | human rationale; separates “raise the limit” from “never retry” | 0 |
escalate / watch | structured human handoff + external watchdog | escalation_pending / heartbeat_stale |
attest / ack | sign a receipt / signed comprehension ack | 0 |
JSON contract. Every command in --json mode emits exactly one JSON object to stdout; human logs and errors go to stderr. The object includes at least {tool, version, status, errors[]} and, where applicable, {run_id, iteration, halt_reason}. The schema is additive: new fields MAY be added; existing field meanings MUST NOT change within a major spec version.
11. Capability status (normative source for the docs matrix)
This matrix describes current source. Publication status comes from GitHub Releases; new v0.4.1 report behavior requires a matching public binary.
Each capability is Shipped (in cmd/loopexec with tests), In progress (on a feature branch), or Planned (specified here, not yet built). The docs site MUST render this distinction and MUST NOT present Planned capability as Shipped.
| Capability | Status |
|---|---|
CLI contract: --json, stable exit codes, deterministic output | Shipped |
init / status / check / step stubs | Shipped |
run as a real iterating loop (section 4) | Shipped |
Computed halt reasons (section 5) replacing --halt-reason | Shipped |
| Typed JSONL receipt + durable state (section 8) | Shipped |
probe-check confidence bound (O2) | Shipped (core; adversarial perturbation + in-loop sequential monitor Planned) |
doctor precondition gate (O3-O5, section 7) | Shipped (determinism + isolation preflight + adequacy via mutation canary --mutate-cmd; hermeticity and the coverage-delta tier reported as planned) |
| Set-based progress + no-regression ratchet: oscillation / no-progress / regression halts (section 3.2) | Shipped (legacy --failures-cmd; opt-in manifest snapshots and best-candidate restoration via --workflow) |
explain-halt: raise-the-limit vs do-not-retry (feasibility) | Shipped |
| Metric-integrity gate: guards dominate success (section 6) | Shipped (fail-closed collected-set monotonicity and configured protected-file hashes; assertion-count / coverage-floor Planned) |
doctor isolation preflight: credential-mount + exec-network fail-closed (section 7) | Shipped |
| Receipt pinning: model-identity tuple + sampling + context manifest (sha256) + cost + check fingerprint (section 8) | Shipped (legacy flags record metadata; workflow usage hooks support exact in-loop strict/observed/unmetered accounting) |
replay: verify a receipt offline by re-running the check and matching the fingerprint (section 8) | Shipped |
attest: HMAC-sign a receipt and --verify it (section 8) | Shipped |
report: read-only event/failure/missing-evidence reporting | Shipped (v0.4.1 source; signature presence is unverified, candidate retention is not acceptance) |
reexecute: live re-run of the recorded config N times, halt-reason distribution (section 8) | Shipped |
escalate / watch / ack + comprehension gate (section 9) | Shipped (file/stdout channels, heartbeat + staleness detection, comprehension --comprehension-every; github/slack channels + kill-the-PID actuator Planned) |
build-context: budgeted relevant-file slice with workdir-confined, symlink-safe file resolution (section 4) | Shipped (stacktrace + last-diff + untracked relevance; import_closure / dep_graph tiers Planned) |
Two-zone isolation orchestration (isolate): detached-clone sandbox + per-run minted/revoked credential (0600 env-file, never on the argv) + rendered/launched exec-zone (network:none) and agent-zone (egress-allowlist) (section 7) | Shipped (orchestration; the container engine, the auditing egress proxy, and the provider key API are operator-provided hooks: --runtime, --egress-proxy, --mint-cmd/--revoke-cmd) |
| Shared command lifecycle: total/per-command deadlines, process-group cancellation, bounded output | Shipped (macOS/Linux groups; other platforms direct child only) |
| Numeric score/patience and candidate-bound acceptance guards | Shipped (opt-in workflow policy) |
| Shared scoped model-call budget | Shipped (opt-in call-count gate across run IDs; independent of monetary metering) |
| Offline one-attempt Go creative adapter with signed synthetic review evidence | Shipped (fixtures only; live engines/models are operator integrations) |
This table is the contract between the binary and the site. When a capability moves status, update it here first; the binary tests and the docs matrix both reference this section.
Every row above has a Shipped core. The remaining work is named, inline sub-parts (the operator-provided infra hooks, provider-specific metering wrappers, the deeper metric-integrity layers, the import_closure/dep_graph context tiers, github/slack escalation channels, the kill-the-PID watchdog actuator) — not whole capabilities.
Additional integration and topology status:
| Capability | Status |
|---|---|
| SMALL CLI validation and v1/v2 compatibility proofs | Shipped (repository/adapter integration; SMALL remains an optional external CLI) |
Full SMALL-driven task_list topology | Planned |
| Provider-specific live usage wrappers | Planned (adapters must supply provider enforcement for strict monetary caps) |
| Container/Nix/remote substrates and multi-worker orchestration | Planned (existing isolation hooks compose with operator infrastructure) |
12. Unattended workflow extension
Implementation status: shipped, with offline unit, subprocess, and CLI tests. The following extension is opt-in; legacy set-based progression remains the default. No model-provider, Blender, Godot, or Python orchestration dependency belongs in the governor.
All run hooks share —timeout (total), —command-timeout, and —terminate-grace. Zero deadlines preserve legacy unlimited time. Linux/macOS own a process group, TERM on deadline/cancellation, KILL after grace, wait, and close bounded pipes. Other platforms bound only the direct child; process-tree containment is not claimed there. SIGINT/SIGTERM yield a final atomic state; SIGKILL recovery is limited to journaled state, never an assertion that interrupted work completed. Command events identify phase, duration, exit, and cause, never raw tool output. Output capture is bounded to 1 MiB; separate 16 KiB tails are retained in private run-owned diagnostic files, never printed or stored in receipts. There are no default full logs. Collector stdout is separate from stderr. Collector stderr, nonzero, truncated, or malformed structured output fails closed. Identity collectors accept legacy lines or {“ids”:[]}; valid empty stdout with no stderr remains an empty set. All configured guards run before the success branch (external check 0, CLI 10).
The —workflow JSON contract pins metering, numeric score policy, and constrained candidate management before the first attempt. Monetary strict mode requires per-call upper bounds AND adapter/provider enforcement. Observed mode reports post-call totals without claiming a preventive cap. Unmetered usage has unknown monetary cost, with iteration/time/call/token bounds where supplied. Money uses integer micro-USD (six decimal places), never float accumulation. A nonzero —budget-usd without a metering contract is invalid. Reservations and actuals use stable run/iteration/phase/call IDs; unresolved reservations block restart work. A separate opt-in scoped call budget lets adapters reserve individual child model calls across run IDs before launch. Its absolute shared store pins the scope policy, counts crash/failed-launch reservations conservatively, and refuses changed limits or damaged records. It caps call count by scope and phase, not tokens or dollars; it never substitutes for provider-enforced monetary limits.
Numeric progression compares domain-checked scores with distinct acceptance tolerance and meaningful-improvement thresholds. Best accepted score and last patience-resetting score are separate. Only reviewed technical successes consume visual patience. Technical failures consume attempts but are unreviewed. Stable failure IDs with improving scores do not cause oscillation in numeric mode.
Candidates are confined to a configured manifest under a dedicated candidate root, with runtime journals/evidence outside it. The governor snapshots binary and untracked files as well as text, pins protected policy/config hashes, and restores the best technically valid candidate after rejection. There is no reset/clean of the user’s checkout. No valid baseline means best is absent until a technically valid candidate is accepted. Promotion is journaled before an atomic best-pointer update; interrupted promotion is not accepted. Failed restore is terminal and invalidates exposure of the best candidate. Filesystem naming and hashes are not an adversarial security boundary: production builders must run with OS-enforced write access only to candidate content, with verifier, policy, controller state and critic evidence inaccessible for modification.
The exact workflow schemas, comparison boundaries, restart behavior, trust boundary, and offline commands are normative in docs/workflows.md.
13. Documentation and acceptance continuity
Implementation changes MUST ship with matching canonical documentation and release notes. CLI command/flag/default/help and JSON field contracts MUST pass the generated reference comparison. Release archives MUST include their source-bound public docs snapshot. Automated publication MUST fail closed until the official docs site serves the matching manifest, source files and homepage build marker. Semantic review and account-level release protections remain required; automated provenance is not a claim that every sentence or rendered page is correct. See the normative continuity policy for the gates and rollout limits.
LoopExec is a general-purpose governor. Domain-specific targets and oracle policies belong to integrations. Candidate retention, convergence under the recorded run contract, explicit human acceptance and publication MUST remain distinct. Owner adjustments MUST preserve original receipts and desired objectives, with separate decision evidence; they MUST NOT silently reset budgets or authorize new work. See acceptance policy. Native cross-run decision history is not implemented; these records currently belong to the adapter/operator.
14. Explicit model assignments
The optional schema-1 workflow model_policy defines named profiles, a selected
task and per-role assignments with declared capability requirements and rationale.
models plan MUST perform no work or spending. Advisory mode MUST leave adapter
invocation unchanged. Explicit mode MUST bind every assigned model and nonempty
reasoning value through whole argv arguments, reject conflicting exec overrides,
and require matching pre-call declarations and adapter-reported usage identity.
Valid incurred usage MUST remain recorded even when identity drift halts the run.
Model policy is hashed for resume and included in attestation; legacy records
without it remain compatible. Operator declarations and trusted adapter reports
MUST NOT be represented as independent provider identity/capability attestation.
Assignments MUST NOT lower acceptance targets, renew allowances or create a
second retry controller. Automatic routing and escalation are not implemented.
See model assignments for the normative adapter
contract, validation boundaries and offline proof.