To Data & Beyond

To Data & Beyond

Engineering Reliable Coding Agent Loops: Control Flow, Verification, Retries, and Stop Conditions

How to supervise long-running coding work with explicit contracts, verification, retries, and stopping conditions.

Youssef Hosni's avatar
Youssef Hosni
Jul 29, 2026
∙ Paid

Get 50% off for 1 year

Coding agents such as Claude Code and Codex already contain an internal execution loop that can inspect files, call tools, modify code, and respond to intermediate results. That native loop is useful for completing a single run, but it does not provide the full control needed for long-running or production-oriented coding workflows. A supervisory runtime must decide when another run should begin, what action the worker is allowed to perform, how repository changes are isolated, which evidence is required, when a failure should be retried or replanned, and what conditions justify stopping.

This article develops a practical architecture for building that outer control loop around coding agents. It introduces typed task contracts, durable plan ledgers, bounded worker handoffs, provider adapters, normalized event streams, independent Git diff collection, verification and completion gates, failure classification, no-progress detection, cross-run budgets, Git worktree isolation, trigger admission, cancellation handling, and terminal artifact retention.

The concepts are developed through one parser failure that persists across the full article. The controller reproduces the error, delegates a bounded patch attempt, rejects an implementation that preserves the same failure fingerprint, revises the plan, verifies the corrected patch, and records completion against the resulting repository state. Runnable Python examples demonstrate the supervisory components, while concrete Claude Code and Codex CLI integrations show how the provider-independent controller can invoke native coding-agent workers.

The result is a reference design for coding-agent systems in which model-driven execution remains flexible, while workflow transitions, permissions, budgets, verification, recovery, and termination remain deterministic and auditable.

Get All my 9 AI Courses with 60% off

Get All My 8 Books With 60% Off

Table of Contents:

1. The Two Loops: Native Agent Execution and Supervisory Control

  • The inner coding-agent loop

  • The outer supervisory loop

  • Model policy versus runtime control

  • Why native completion is insufficient

  • The parser failure used throughout the article

2. The Loop Contract: Guidance, Permissions, and Controller State

  • Defining the task contract

  • Separating guidance from enforcement

  • Native permissions and tool boundaries

  • Workflow state versus evidence state

  • Legal state transitions and terminal outcomes

3. Planning Across Runs and Selecting One Bounded Action

  • Persistent plans and plan revisions

  • The plan ledger

  • Selecting the next ready step

  • Constructing a bounded worker handoff

  • Revising a failed implementation hypothesis

4. Invoking Claude Code or Codex as a Controlled Worker

  • The provider-adapter boundary

  • Building bounded native CLI invocations

  • Running Claude Code in non-interactive mode

  • Running Codex through codex exec

  • Capturing native event streams

  • Normalizing provider-specific results

  • Process timeouts, termination, and session resumption

5. Evidence, Verification, and Completion Gates

  • Collecting the Git diff independently

  • Enforcing file and scope policy

  • Running deterministic verification

  • Layered acceptance predicates

  • Separating worker completion from task completion

  • Binding evidence to the verified repository state

6. Failure Classification, Retry, Replanning, and No-Progress Detection

  • Classifying infrastructure, execution, and verification failures

  • Retrying transient failures

  • Replanning failed hypotheses

  • Comparing failure fingerprints

  • Detecting repeated actions and unchanged evidence

  • Escalating when progress stalls

7. Isolation, Budgets, Triggers, and Safe Stopping

  • Admitting a new iteration

  • Isolating mutating attempts with Git worktrees

  • Reserving and aggregating cross-run budgets

  • Normalizing manual, scheduled, and event triggers

  • Persisting cancellation intent

  • Handling local and external side effects

  • Routing terminal artifacts by outcome

8. Reference Implementation: From Parser Failure to Verified Patch

  • Defining the integrated runtime boundary

  • Reproducing the baseline parser failure

  • Executing the first bounded hypothesis

  • Classifying the unchanged failure

  • Replanning and applying the corrected patch

  • Passing verification and the completion gate

  • Replacing fixtures with native Claude Code and Codex adapters

  • Persisting the final run record

Get All my 9 AI Courses with 60% off


I’m hosting a 3-hour live workshop on Hands-On Loop Engineering with Claude.

We will build Claude loops with persistent state, verification, scheduling, and safer permissions, then apply the pattern to real coding, research, and automation workflows.

Reserve Your Seat


1. The Two Loops: Native Agent Execution and Supervisory Control

Get 50% off for 1 year

Claude Code and Codex already run an agent loop when they receive a coding task. They inspect relevant files, select tools, execute commands, interpret the output, and continue until the current run reaches a result. Anthropic describes its Agent SDK as exposing the same tools, agent loop, and context management used by Claude Code. Codex provides a comparable programmable surface through codex exec, which can run inside scripts and emit machine-readable events.

This blog adds a second loop around that native behavior. The supervisory loop decides when to start an agent run, what contract applies to it, whether the produced evidence represents progress, and whether another run is justified. It also owns cross-run limits and terminal outcomes, which remain stable when the underlying coding agent, model, or invocation method changes.

Get All my 9 AI Courses with 60% off

Figure 1. Claude Code or Codex runs the inner tool loop, while an external controller supervises progress across runs.

Claude Code includes several native controls that can participate in this design. /goal evaluates a condition after every turn using a separate evaluator, while Stop hooks can run deterministic scripts or model-evaluated checks. In non-interactive mode, — max-turns and — max-budget-usd bound an individual invocation.

Codex exposes related controls at a different layer. codex exec is intended for scripts, CI jobs, and scheduled workflows, supports explicit sandbox settings, and can emit a JSONL event stream containing thread, turn, item, and error events. Codex sandbox modes determine whether a run is read-only, workspace-writable, or unrestricted.

These native mechanisms remain inside one invocation or product session. The supervisory loop maintains the task-level record across invocations: the original acceptance criteria, earlier failures, approved scope, unresolved hypotheses, and the reason the overall task eventually stopped.

A. Start with a reproducible engineering failure

Get 50% off for 1 year

The running example is a small header parser that fails on an HTTP-style blank separator line. Every later section will advance this same task rather than replacing it with isolated examples.

The parser should accept:

Host: example.com
Accept: application/json

and produce two headers. Its current implementation attempts to split every line around a colon, including the empty line.

This goes in the parser example used throughout the post.

def parse_headers(block: str) -> dict[str, str]:
    headers: dict[str, str] = {}
    for line in block.splitlines():
            name, value = line.split(”:”, 1)
            headers[name.lower()] = value.strip()
        return headers
    
sample = “Host: example.com\n\nAccept: application/json”
    
try:
    parse_headers(sample)
except Exception as exc:
    print(”reproduction_status=failed”)
    print(f”error_type={type(exc).__name__}”)
    print(f”error={exc}”)

Output:

reproduction_status=failed
error_type=ValueError
error=not enough values to unpack (expected 2, got 1)
Figure 2. The supervisory task begins with a reproducible parser failure and a stable error signature.

Get All My 8 Books With 60% Off

The process itself exits normally because the exception is caught, while the parser operation has failed. A controller that records only subprocess exit status would miss this distinction. The outer loop therefore needs task-specific evidence in addition to process metadata, including the failed input, exception class, test identity, and any artifacts produced by the run.

This reproduction will become the first durable observation in the task state. Later attempts can compare their result against the same input and determine whether the failure disappeared, changed shape, or moved elsewhere. A repeated run that produces the same exception without modifying the hypothesis or environment has not generated new information.

Key concept: Native agent activity produces observations. The supervisory loop determines whether those observations represent progress against the task’s acceptance criteria.

Get All my 9 AI Courses with 60% off

Figure 3. The first outer-loop iteration converts a local failure into durable task state.

B. Treat Claude Code and Codex as provider-specific workers

Get 50% off for 1 year

The controller needs a narrow interface for starting either coding agent. Claude Code print mode can return JSON and apply invocation-level turn and budget limits. Codex can run non-interactively with JSON events and an explicit sandbox.

The first provider adapter only builds the commands. Subprocess execution, event parsing, timeout handling, and session continuation belong in Section 4. Keeping the builder runnable allows us to verify the exact arguments that the later executor will receive.

This goes in the provider adapter.

from dataclasses import dataclass
from shlex import join
from typing import Literal


Provider = Literal[”claude”, “codex”]


@dataclass(frozen=True)
class InvocationPolicy:
    max_turns: int = 6
    max_budget_usd: float = 3.00
    sandbox: str = “workspace-write”


def build_agent_command(
    provider: Provider,
    prompt: str,
    policy: InvocationPolicy,
) -> list[str]:
    if provider == “claude”:
        return [
            “claude”,
            “-p”,
            “--output-format”,
            “json”,
            “--max-turns”,
            str(policy.max_turns),
            “--max-budget-usd”,
            f”{policy.max_budget_usd:.2f}”,
            “--allowedTools”,
            “Read”,
            “Edit”,
            “Bash”,
            prompt,
        ]

    if provider == “codex”:
        return [
            “codex”,
            “exec”,
            “--json”,
            “--sandbox”,
            policy.sandbox,
            prompt,
        ]

    raise ValueError(f”Unsupported provider: {provider}”)


policy = InvocationPolicy()
prompt = (
    “Reproduce the parser bug. Do not claim completion. “
    “Return the failing command and evidence.”
)

for provider in (”claude”, “codex”):
    command = build_agent_command(provider, prompt, policy)
    print(f”{provider}_command={join(command)}”)

Output

claude_command=claude -p --output-format json --max-turns 6 --max-budget-usd 3.00 --allowedTools Read Edit Bash ‘Reproduce the parser bug. Do not claim completion. Return the failing command and evidence.’
codex_command=codex exec --json --sandbox workspace-write ‘Reproduce the parser bug. Do not claim completion. Return the failing command and evidence.’
Figure 3. The provider adapter translates one supervisory prompt into native Claude Code and Codex invocations.

Get All My 8 Books With 60% Off

The Claude invocation bounds the work performed inside one native run. Its turn and spending limits do not represent the full task budget because the outer controller may start another run after verification fails. The Codex invocation similarly defines the sandbox for one worker execution, while the supervisory contract may impose narrower path rules and a smaller permitted diff.

Provider adapters also create a compatibility cost. Claude and Codex expose different event schemas, session models, budget controls, and permission semantics. The controller needs a normalized result type so later logic operates on changed_files, evidence, usage, and stop_reason rather than provider-specific payloads.

Get All my 9 AI Courses with 60% off

Figure 4. Provider adapters isolate native command and event differences from the supervisory controller.

C. Keep the completion decision outside the worker

Get 50% off for 1 year

A coding agent can propose that the task is complete, request more context, or report that it cannot continue. The controller evaluates that proposal against repository state and independent checks. A completion message becomes one input to the decision rather than the terminal event itself.

The example below simulates a Codex run that reports completion without changing files. The controller executes the parser acceptance check and rejects the claim because the original failure remains reproducible.

This goes in the supervisory controller.

from dataclasses import dataclass
from enum import Enum

class SupervisorDecision(str, Enum):
    VERIFY = “verify”
    REPLAN = “replan”
    SUCCEED = “succeed”
    ESCALATE = “escalate”

@dataclass(frozen=True)
class AgentRunResult:
    provider: str
    claimed_complete: bool
    changed_files: tuple[str, ...]
    summary: str

def parse_headers(block: str) -> dict[str, str]:
    headers: dict[str, str] = {}
    for line in block.splitlines():
        name, value = line.split(”:”, 1)
        headers[name.lower()] = value.strip()
    return headers

def verify_parser_behavior() -> tuple[bool, str]:
    sample = “Host: example.com\n\nAccept: application/json”
    try:
        parsed = parse_headers(sample)
    except ValueError as exc:
        return False, f”ValueError:{exc}”
    expected = {
        “host”: “example.com”,
        “accept”: “application/json”,
    }
    return parsed == expected, f”parsed={parsed}”

def supervise(
    result: AgentRunResult,
) -> tuple[SupervisorDecision, str]:
    if result.changed_files:
        return (
            SupervisorDecision.VERIFY,
            “repository changed; verification is required”,
        )
    passed, evidence = verify_parser_behavior()
    if result.claimed_complete and not passed:
        return (
            SupervisorDecision.REPLAN,
            f”completion claim rejected; {evidence}”,
        )
    if passed:
        return SupervisorDecision.SUCCEED, evidence
    return SupervisorDecision.REPLAN, evidence

result = AgentRunResult(
    provider=”codex”,
    claimed_complete=True,
    changed_files=(),
    summary=”The parser task appears complete.”,
)
decision, reason = supervise(result)
print(f”provider={result.provider}”)
print(f”agent_claimed_complete={result.claimed_complete}”)
print(f”supervisor_decision={decision.value}”)
print(f”reason={reason}”)

Output:

provider=codex
agent_claimed_complete=True
supervisor_decision=replan
reason=completion claim rejected; ValueError:not enough values to unpack (expected 2, got 1)
Figure 5. Completion is a controller decision derived from evidence and policy, rather than a field copied from the worker result.

The printed trace preserves the disagreement between the worker and the controller. That disagreement is operationally useful: it records that the agent believed the task was complete while the deterministic check still reproduced the failure. A later failure-analysis stage can classify this as unsupported completion rather than a tool failure or an implementation regression.

The same rule applies when native completion helpers are enabled. Claude Code’s /goal uses a separate evaluator after every turn, which provides stronger separation than self-evaluation inside the working turn. An external controller may still retain the final authority when completion depends on repository policies, cross-run budgets, or evidence stored outside the agent session.

Takeaway: Native goal evaluators can decide whether another native turn should begin. The supervisory controller decides whether the overall engineering task has reached an accepted terminal state.

Get All my 9 AI Courses with 60% off

Figure 6. Coding-agent products and external controllers make completion decisions at different scopes.

The supervisory layer adds latency because evidence may be checked again after the native agent has already run tests. It also adds storage and integration code for normalized events, task state, and provider adapters. That duplication becomes useful when the same contract must work across Claude Code and Codex or when a task continues across multiple sessions, workers, or scheduled runs.


2. The Loop Contract: Guidance, Native Permissions, and Controller State

Get 50% off for 1 year

The parser task now has one failed observation and a replan decision. Before another coding-agent run begins, the controller needs a contract that defines the accepted result, authorized scope, required checks, and outer-loop limits. Part of that contract can be translated into native Claude Code or Codex configuration, while the remaining fields stay in the external controller.

The contract has three layers:

→ Durable project guidance
→ Native execution boundary
→ Supervisory task policy

Each layer has a different enforcement model. Mixing them into one Markdown file makes it difficult to determine which rules are contextual instructions, which are enforced by the coding-agent runtime, and which are evaluated by the outer controller.

Get All my 9 AI Courses with 60% off

Figure 7. A loop contract spans guidance, native enforcement, and external task control.

A. Separate guidance from enforcement

Get 50% off for 1 year

Claude Code loads CLAUDE.md files as persistent project instructions, while Codex reads AGENTS.md before beginning work. Both mechanisms are suitable for repository conventions, build commands, testing instructions, and directory-specific guidance. Claude’s documentation explicitly describes CLAUDE.md as context rather than enforced configuration, and also supports importing an existing AGENTS.md from CLAUDE.md.

Native execution controls provide the next layer. Claude Code permissions determine which tools, files, and domains can be accessed, while Codex sandbox and approval modes determine filesystem, network, and command boundaries.

The supervisory contract owns task-specific conditions that should remain stable across providers. These include acceptance predicates, total outer-loop iterations, permitted diff size, repeated-failure limits, and the mapping from failure conditions to terminal outcomes.

Key concept: CLAUDE.md and AGENTS.md provide durable context. Permissions, sandboxes, hooks, and the external controller provide enforcement.

Get All my 9 AI Courses with 60% off

Figure 8. Contract fields are distributed according to which layer can enforce them reliably.

B. Define one provider-independent contract

Get 50% off for 1 year

The contract below contains the fields needed by the external loop and renders the repository guidance shared with coding agents. The generated Markdown can be stored in AGENTS.md; a Claude project can import it from CLAUDE.md, avoiding duplicated instructions across tools.

The contract deliberately keeps outer-loop limits outside the rendered guidance. Telling an agent that it has four attempts does not prevent a wrapper from starting a fifth run. The controller must enforce that number before spawning another process.

This goes in the supervisory contract module.

from dataclasses import dataclass

@dataclass(frozen=True)
class AcceptanceCriterion:
    name: str
    command: str

@dataclass(frozen=True)
class LoopContract:
    objective: str
    writable_paths: tuple[str, ...]
    protected_paths: tuple[str, ...]
    criteria: tuple[AcceptanceCriterion, ...]
    max_outer_iterations: int
    max_changed_files: int

def validate_contract(contract: LoopContract) -> list[str]:
    errors: list[str] = []
    if not contract.objective.strip():
        errors.append(”objective_empty”)
    if not contract.writable_paths:
        errors.append(”writable_paths_empty”)
    if not contract.criteria:
        errors.append(”criteria_empty”)
    if contract.max_outer_iterations < 1:
        errors.append(”invalid_iteration_limit”)
    return errors

def render_shared_guidance(contract: LoopContract) -> str:
    checks = “\n”.join(
        f”- `{item.command}` ({item.name})”
        for item in contract.criteria
    )
    writable = “\n”.join(
        f”- `{path}`” for path in contract.writable_paths
    )
    protected = “\n”.join(
        f”- `{path}`” for path in contract.protected_paths
    )
    return f”“”# Parser maintenance rules
## Objective
{contract.objective}
## Writable scope
{writable}
## Protected scope
{protected}
## Required checks
{checks}
“”“

contract = LoopContract(
    objective=(
        “Fix blank-line handling in parse_headers “
        “and add a regression test.”
    ),
    writable_paths=(
        “src/parser/**”,
        “tests/parser/**”,
    ),
    protected_paths=(
        “migrations/**”,
        “infrastructure/**”,
    ),
    criteria=(
        AcceptanceCriterion(
            “targeted_regression”,
            “pytest tests/parser/test_headers.py -q”,
        ),
        AcceptanceCriterion(
            “static_analysis”,
            “python -m compileall -q src”,
        ),
    ),
    max_outer_iterations=4,
    max_changed_files=4,
)
errors = validate_contract(contract)
guidance = render_shared_guidance(contract)
print(f”contract_valid={not errors}”)
print(f”outer_iteration_limit={contract.max_outer_iterations}”)
print(f”required_checks={len(contract.criteria)}”)
print(”guidance_preview=”)
print(”\n”.join(guidance.splitlines()[:8]))

Output:

contract_valid=True
outer_iteration_limit=4
required_checks=2
guidance_preview=

# Parser maintenance rules

## Objective
Fix blank-line handling in parse_headers and add a regression test.
## Writable scope
- `src/parser/**`
- `tests/parser/**`
Figure 9. One typed contract produces both external policy and shared coding-agent guidance.

Get All My 8 Books With 60% Off

The contract validator runs before either provider is invoked. A missing objective prevents useful planning, while missing acceptance criteria prevent evidence-based completion. Invalid outer limits are configuration errors and should move the run into a setup failure before model usage begins.

Rendering guidance from the same source reduces drift between the external contract and repository instructions. It does not guarantee that the agent will follow every instruction, so the controller still validates the resulting diff and verification evidence. The generated file should remain concise because both Claude and Codex load project guidance into their working context.

The contract introduces schema-management costs. Persisted tasks need a contract version, and resumed runs need compatibility checks when fields or criterion formats change. Repository guidance may also require provider-specific additions, such as Claude hooks or Codex rules, while retaining one shared task definition.

C. Make acceptance criteria executable

Get 50% off for 1 year

The contract’s objective remains prose because it needs to communicate intent. Completion depends on executable predicates. For the parser task, those predicates cover the original blank-line behavior, ordinary header parsing, and the existence of a targeted regression test.

The test-discovery check remains an in-memory stub in this section. Section 5 will replace it with repository inspection and captured test-run evidence.

This goes in the acceptance evaluator.

from dataclasses import dataclass
from typing import Callable

@dataclass(frozen=True)
class CheckResult:
    name: str
    passed: bool
    evidence: str

def parse_headers(block: str) -> dict[str, str]:
    headers: dict[str, str] = {}
    for line in block.splitlines():
        name, value = line.split(”:”, 1)
        headers[name.lower()] = value.strip()
    return headers

def check_blank_line() -> CheckResult:
    sample = “Host: example.com\n\nAccept: application/json”
    try:
        parsed = parse_headers(sample)
    except ValueError as exc:
        return CheckResult(
            “blank_line_is_ignored”,
            False,
            f”ValueError:{exc}”,
        )
    return CheckResult(
        “blank_line_is_ignored”,
        parsed.get(”accept”) == “application/json”,
        f”parsed={parsed}”,
    )

def check_standard_headers() -> CheckResult:
    sample = “Host: example.com\nAccept: application/json”
    parsed = parse_headers(sample)
    expected = {
        “host”: “example.com”,
        “accept”: “application/json”,
    }
    return CheckResult(
        “standard_headers_preserved”,
        parsed == expected,
        f”parsed={parsed}”,
    )

def check_regression_test() -> CheckResult:
    discovered_tests = {”test_standard_headers”}
    required = “test_blank_line_is_ignored”
    return CheckResult(
        “regression_test_exists”,
        required in discovered_tests,
        f”required={required}”,
    )

checks: tuple[Callable[[], CheckResult], ...] = (
    check_blank_line,
    check_standard_headers,
    check_regression_test,
)
results = [check() for check in checks]
for result in results:
    print(
        f”{result.name}:”
        f”passed={result.passed}:”
        f”evidence={result.evidence}”
    )
print(f”task_complete={all(item.passed for item in results)}”)

Output:

blank_line_is_ignored:passed=False:evidence=ValueError:not enough values to unpack (expected 2, got 1)
standard_headers_preserved:passed=True:evidence=parsed={’host’: ‘example.com’, ‘accept’: ‘application/json’}
regression_test_exists:passed=False:evidence=required=test_blank_line_is_ignored
task_complete=False
Figure 10. One typed contract produces both external policy and shared coding-agent guidance.

The evaluator records partial progress. Existing header behavior still passes, while the target input and regression-test requirement fail. A later patch can therefore fix the blank-line case while the same evaluator checks whether ordinary parsing has regressed.

Each result carries compact evidence. A production implementation should replace long inline values with artifact identifiers, test node IDs, commands, exit codes, and hashes. The controller uses the boolean result for transitions, while operators and verifier agents use the evidence when auditing the decision.

Takeaway: Acceptance predicates preserve both success and failure. A passing check remains visible during an unsuccessful run and becomes a regression guard during later attempts.

Get 50% off for 1 year

User's avatar

Continue reading this post for free, courtesy of Youssef Hosni.

Or purchase a paid subscription.
© 2026 Youssef Hosni · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture