# The agent loop protocol

> How I run long agent loops on pi, Codex and Claude Code: a short frozen charter, a living queue from the board, lanes that hand back uncommitted candidates and a root that alone pushes.

- Canonical page: https://rob-knight.com/articles/the-agent-loop-protocol/
- Kind: Article
- Published: 2026-09-26
- Updated: 2026-10-21
- Topics: agents

A loop is one long-lived agent session, the root, working a living queue of Backlog tasks through parallel lanes. It starts from a short frozen charter, keeps its queue fed from the board, gives each task to one lane in its own worktree and checks what comes back. Accepted work lands in batches: the root composes the candidates, gates that exact tree and pushes it. Only the root pushes. It keeps going while ready work remains, asks me questions without stopping to wait for the answers, and writes a closeout at the end that the next loop starts from.

This is the contract every root follows, whichever harness runs it. pi is the default for OpenAI-model loops, through a launcher called [`loop-pi`](https://github.com/rknightion/loop-pi). Claude Code runs a loop on Claude models, and Codex runs one only when I name it; [Why I don't use Codex for long-running agent loops](/articles/why-i-dont-use-codex-for-long-running-agent-loops/) has the reasons. [The machinery around the loop](/articles/the-machinery-around-the-loop/) covers the tools that enforce what's described here.

<figure class="diagram figure-breakout">
  <img
    class="on-light"
    src="/diagrams/43-tldr-the-agent-loop-protocol-v2-light.svg"
    alt="At a glance: a loop is one long-lived root session working a living queue of Backlog tasks through parallel lanes, from a frozen charter to a closeout the next loop starts from. A petrol line runs left to right through three acts, each leaving files. Before launch, charter-&lt;date&gt;-loop&lt;N&gt;.md: a frozen charter, at most 8 KB; every choice the first wave needs, asked once; a first wave of up to about 20 small tasks. While it runs, QUEUE.md, LANES.md and DECISIONS.md: one lane, one task, one worktree; lanes hand back uncommitted candidates; highlighted, only the root pushes, after a green composed gate. At close, closeout-&lt;date&gt;-loop&lt;N&gt;.md with the sections Landed, Parked, For ratification and Notes; the next loop starts from Parked and For ratification. Along the bottom, the owner: questions never stop the run, and overnight mode asks nothing."
    width="1440"
    height="790"
    loading="lazy"
    decoding="async"
  />
  <img
    class="on-dark"
    src="/diagrams/43-tldr-the-agent-loop-protocol-v2-dark.svg"
    alt=""
    aria-hidden="true"
    width="1440"
    height="790"
    loading="lazy"
    decoding="async"
  />
  <figcaption>
    Each act leaves files the next one reads, and the closeout's Parked and For ratification
    sections are where the next charter starts.
  </figcaption>
</figure>

## What each reader sees

At launch the root reads its charter, the [contract](/reference/loop-contract/) and the harness file for whatever it's running on, all in full, and the repository's `LOOP.md` for facts. A lane reads only its brief and the run's `LANE.md`. No lane ever reads the contract. Where these disagree, a message from me in the run wins, then the charter, then the `Standing:` file, then `LOOP.md`, then the contract.

Every file has a size cap that something enforces. Tests in the protocol repository fail a change that takes the contract over 16 KB or a harness file over 6 KB. `loop-lint` refuses a charter over 8 KB and a `LOOP.md` over 6 KB, and `LOOP.md` holds nothing dated. The charter states intent and authority, the board holds the tasks and `LOOP.md` holds the repository facts, so none of them restates another.

<figure class="diagram figure-breakout">
  <img
    class="on-light"
    src="/diagrams/13-one-loop-its-files-v4-light.svg"
    alt="Two reading sets and one run directory. The root reads its charter, the contract and its harness file in full, and LOOP.md for repository facts. The harness file is one of four, pi, Claude Code, Claude Code on GPT or Codex, and a root reads the one it runs on, never all four; on GPT it also reads the Claude Code file. The root also holds the Standing file: approved authority, checked by sha256. A lane reads only its brief and LANE.md, never the contract. Precedence: owner message, then charter, then Standing, then LOOP.md, then contract. The run directory under codex/ holds, from preparation, charter-&lt;date&gt;-loop&lt;N&gt;.md, launch-&lt;date&gt;-loop&lt;N&gt;.txt and loop&lt;N&gt;/LANE.md; written by the root while running, QUEUE.md, LANES.md, DECISIONS.md, QUESTIONS.md and BATCH&lt;k&gt;.md in loop&lt;N&gt;/; from each lane, its candidate.patch and return.md; and the terminal closeout-&lt;date&gt;-loop&lt;N&gt;.md, after which only codex-reconcile and the reply follow."
    width="1440"
    height="1106"
    loading="lazy"
    decoding="async"
  />
  <img
    class="on-dark"
    src="/diagrams/13-one-loop-its-files-v4-dark.svg"
    alt=""
    aria-hidden="true"
    width="1440"
    height="1106"
    loading="lazy"
    decoding="async"
  />
  <figcaption>
    A lane never sees the contract or the charter. Everything it needs reaches it through its brief
    and the run's <code>LANE.md</code>.
  </figcaption>
</figure>

## Preparation

Preparation runs through the `loop` skill in my `agent-workflows` plugin, and should take minutes. It reconciles the repository's `codex/` directory across my Macs, then reads the last closeout's `## Parked` and `## For ratification` sections: my answers to those become decisions in the new charter. It reads the board, ranks ready work by my priorities and excludes, by name, any task that needs a choice from me nobody has made.

The first wave is up to about 20 rows, each a task, its files and a route. Preparation prefers many small tasks to a few large ones. Two tasks that share a file go in anyway, with the overlap noted, because the landing step merges overlaps. Migration numbers and registry IDs are assigned here, so two lanes never pick the same one.

Every choice the first wave needs from me goes into one batched prompt, each with a recommendation, and the answers are frozen into the charter. The planner also runs the gate and the board's Definition of Done on the base commit and records anything already red, so a lane isn't blamed for it later. It reads the gate route from the repository's `AGENTS.md`: a remote gate runner where the repository has one, with the workstation as the fallback.

Then it writes the charter, `LANE.md` and the launch file, and runs `loop-lint charter` and `loop-lint queue` until both are clean. I launch the loop myself.

## The charter

The charter is a short Markdown file of keys, then the first wave and the decisions already made. The [template](/reference/loop-charter/) is published:

```
# Charter: <repo> loop<N>

harness:    claude | codex | pi
root:       <model> / <effort>
aim:        <one or two sentences: what this run is for>
priorities: <ordered board selectors>
exclude:    <task IDs and labels never to admit>
lanes:      <cap on live implementation lanes>
gate:       <exact gate command>
gate-route: <remote runner, then the fallback>
standing:   <abs path to the Standing file> sha256=<hex>
overnight:  <the mandate when I say "overnight mode">

## First wave
## Decisions already made
```

That is trimmed: the template also has an optional host pin, extra guarded paths, extra authority lines, what the root may read live and an optional wall-time end. The charter carries no protocol text, because the root reads the contract anyway.

Once a root adopts a charter nobody edits it. An amendment goes in the run's `DECISIONS.md`, or in an addendum file beside the charter that I've approved.

The launch file's first line is the command, and the rest is the message I paste:

```
cmd: loop-pi
You are the root. Read <contract>, <harness file> and <abs charter path> in full.
Write <abs closeout path> last, then reconcile and reply (contract section 12).
Standing: <abs path> sha256=<hex>
```

"You are the root" and the closeout path are how every hook, watchdog and continuation recognises a run, so both stay exact. The `Standing:` file is my approved standing authority for the repository, checked against its sha256 at launch. A mismatch grants nothing.

## The run directory

Each repository driven this way has a gitignored `codex/` directory. Run N leaves these in it:

```
codex/charter-<date>-loop<N>.md      the frozen charter
codex/launch-<date>-loop<N>.txt      the launch message
codex/loop<N>/LANE.md                the rules every lane reads
codex/loop<N>/QUEUE.md               Ready, Hold and Landing
codex/loop<N>/LANES.md               one line per live lane
codex/loop<N>/DECISIONS.md           rulings, amendments, parks; append-only
codex/loop<N>/QUESTIONS.md           open questions for me, each with a recommendation
codex/loop<N>/BATCH<k>.md            one landing batch
codex/loop<N>/<lane>/                the lane's patch, return and evidence
codex/closeout-<date>-loop<N>.md     the terminal closeout
```

The root writes the queue, lane and decision files as it goes, and re-reads them after a compaction or a provider error to recover. On pi the harness also keeps an append-only event log, and writes its dispatches, returns, watchers and heartbeats itself.

## The queue

The board is the queue. The root starts from the first wave and keeps `QUEUE.md` fed from the board in the charter's priority order. Before each wave, `loop-lint queue` checks that nothing on it is excluded, already Done or labelled `needs-owner`. A finding becomes a Backlog task at once and joins the queue when it serves the aim.

The root doesn't stop for lack of listed rows while ready, unexcluded work remains on the board. It also never invents work outside the aim.

## Dispatch

One lane takes one task, in its own worktree from current main. The root keeps live implementation lanes at the charter's `lanes:` cap, counted from the harness's own listing rather than memory, and every wake schedules every ready task before anything else.

A lane gets this brief and nothing more:

[Live from the loop contract: 3-dispatch](https://rob-knight.com/reference/loop-contract/#3-dispatch)

Scope is not a lock. A brief names the files other live lanes are in, and the landing step merges overlaps. The exception is generated and shared integration files, such as an API spec or a registry. Those belong to the root, and a lane returns a hunk for them.

The root also does every live read. It queries the database, the logs or the provider API and puts the exact cause and response shapes in the brief, so lanes stay credential-free and work against fixtures.

A task routes to a `worker` by default. A `senior` takes it only when the brief says why: an unknown cause, a frozen seam or shared contract, a data-model call or composing a batch. Read-only mapping, sweeps and log reduction go to a `scout`. A discovery brief tells a lane to find code by meaning: the language server for symbols and [`jg`](/articles/jev-as-a-decision-layer/) for how something works.

## Returns, repairs and the tripwire

A lane never commits to main. It hands back an uncommitted candidate: a binary patch, its sha256, the base SHA and the worktree path, in a `lane-return` block the [lane brief](/reference/loop-lane/) defines.

A lane saying it finished is a claim. Before accepting, the root reads the return, the predicate that decides the task and the test diff, and checks the claims against git and the gate output in proportion to the risk. Repairs go back to the same live lane with the new evidence, so it keeps its context.

Repairs stop at a tripwire. When a task has had two failed rounds that each opened a new class of finding, or four hours have passed since its first dispatch, the root stops repairing and records one of four outcomes: a named new approach, a split, an escalation to `senior-escalation`, or a park with options.

A task is Done only once it has landed in a pushed batch whose composed gate was green.

## Review

Every task is `routine` or `guarded`. A task is guarded when its files touch auth, secrets, credentials, privacy or redaction code, tenant or authorisation scope, migrations, destructive SQL, IaC, Kubernetes, Helm, workflow files, release-please files, deploy configuration, `LOOP.md` or the charter's own guarded paths, or when it makes a live change. A lane that finds it needs a guarded path its brief didn't name stops and returns.

Routine work gets the lane's gate, CodeRabbit and `dsreview`, a DeepSeek Flash reviewer the lane runs on its own patch, in place of a `reviewer` lane. A `reviewer` is mandatory when dsreview gives no verdict, when the lane disputes one of its major findings, on the second repair round, or when CodeRabbit didn't run, and it runs after landing. Guarded work always gets a reviewer with a security brief before it lands, then CodeRabbit, plus the lane's dsreview; any blocker blocks. [How a change is proven](/loop/review/) has the evidence for replacing the routine reviewer.

A change after a verdict invalidates it, and only the delta is reviewed again. Reviewers report and never edit. CodeRabbit runs once per final candidate, every critical and major finding is fixed, and exit 0 isn't a clean review: a run with no `complete` line failed.

## Landing

Accepted candidates land in batches:

[Live from the loop contract: 6-landing](https://rob-knight.com/reference/loop-contract/#6-landing)

The composed gate runs on the integrated tree, which no lane's gate ever sees, and that exact tree is what gets pushed. Gating once per pair of tree and base means an unchanged rerun is a loop, not a retry. A red that also fails on base isn't the candidate's, and a repository-wide check that was already red at base doesn't block landing once both logs are recorded. A task's own `Done when:` check never qualifies for that: it has to be red at base and green on the candidate.

<figure class="diagram figure-breakout">
  <img
    class="on-light"
    src="/diagrams/24-gate-land-fix-forward-v2-light.svg"
    alt="Three lane candidates, A, B and C, each passed its lane gate on its own tree. The root accepts them and composes them on current main, one commit per task with a Task: trailer, then runs the composed gate on this exact tree. On FAIL the batch goes to repair; a red that also fails on base is not the candidate's. On OK the root alone pushes the tree unchanged to main. Hosted CI then runs on main, watched in the background rather than blocking the push: OK means done, FAIL means fix forward at once, and a revert happens only on a recorded decision."
    width="1440"
    height="986"
    loading="lazy"
    decoding="async"
  />
  <img
    class="on-dark"
    src="/diagrams/24-gate-land-fix-forward-v2-dark.svg"
    alt=""
    aria-hidden="true"
    width="1440"
    height="986"
    loading="lazy"
    decoding="async"
  />
  <figcaption>
    No lane gate ever sees the integrated tree. The composed gate is the first check of it, which is
    why that exact tree is the one the root pushes.
  </figcaption>
</figure>

Proof has its own rules. Name the check, quote its output and give the SHA it ran on. Never weaken, skip or special-case a test to get green. A local check proves its tree, the composed gate the integrated tree, CI its exact SHA and live evidence only the environment it observed.

## The owner

The contract assumes I'm around unless I say otherwise:

[Live from the loop contract: 7-the-owner](https://rob-knight.com/reference/loop-contract/#7-the-owner)

A question never stops the run. It goes in chat with a recommendation and into `QUESTIONS.md`, and the root keeps working on everything that doesn't depend on it. A blocking question tool is allowed only once every queued return is processed and every ready task dispatched, so it can't stand in for a wait. In overnight mode nothing is asked at all: questions are held, small calls are ruled and marked for ratification, and the closeout carries them all.

## Authority, parks and fences

The `Standing:` file and the charter's `authority:` lines are a root's grants, and live work they cover stays with the root on every harness. The contract adds one standing line for every loop: read-only access everywhere, OpenBao login with ordinary reads and writes, IaC and CLI provisioning, changes to existing infrastructure, and any deploy, rollout or release that serves the aim, including production, fixing forward. A customer's tenant stays per charter.

The root has four repair powers. It can record an amendment that fixes a charter defect, settle a design fork the evidence clearly decides, implement a small prerequisite outside any live lane's scope, and amend a seam it froze in this run if nothing has accepted against it yet. No root decision supplies my consent, removes a fence or rotates a credential.

A permission or classifier denial doesn't stop the run. The root records it verbatim, does the other ready work and asks me for the exact action by name. It never reaches the denied effect by another tool, command or lane.

A park names the blocker, what's missing and the resume condition, with one reason: `owner`, `authority`, `dependency`, `defect` or `evidence-later`. The last is for proof only time can produce, such as a scheduled job firing. The code lands with a `verify:<date>` label and a `just verify-<name>` recipe, and a daily verifier reopens the task if the check fails. Proof obtainable now is never deferred.

Then the fences, which the contract lists as having no hook to enforce them (on pi the guard happens to back the force-push one):

- Never kill a mutation in flight, and no history rewrite or force push.
- Irreversible data loss only on a target the charter names, with my confirmation.
- Nothing outward-facing: no email, chat posts, third-party issues or pull requests, and nothing made public. My own release-please releases don't count.
- Never print a secret. Apply a targeted IaC change only after saving and reading its plan.
- Before pushing anything that deploys, check what the deploy will interrupt.
- A new billable resource only under about $50 a month, with the estimate recorded.
- Never touch a customer tenant the charter doesn't name.
- After an ambiguous external write, re-read the target, and roll back only a verified change.

## Waiting and recovery

The rule is wait by push, never by polling. A lane's completion wakes the root, and a wait the root owns, such as main's CI or a release, is a background watcher that exits on every terminal state. A waiting turn ends with `WAITING: <what> until <time>`. A wait on nothing but me has a deadline too: if no answer, ready task or live lane has turned up by then, the root parks what's waiting on me as `owner` and closes out.

Each harness does this its own way. Claude Code wakes the root with a task notification for a lane and runs the root's own waits as one background command. pi starts a new root turn when a lane returns, a `watch_start` watcher exits or a `wake_at` timer fires. Codex has neither, so a Codex root holds its turn on `wait_agent` and hands process waits to a scout.

After a compaction or a provider error the root recovers in the same session: it re-reads its queue, lane and decision files and the harness's live listing, reconciles them and carries on. It never starts a fresh root. On pi, recovery is automatic: the harness resumes a root after a provider error, a stalled request or a request-ceiling abort, with backoff, and stops the run with an incident on an authentication, quota, policy or context-length error.

A root never ends a turn while work is owed. Each harness file ends with a block naming the endings that stop a run early: a summary that announces the next step without taking it, an offer to carry on, a list of decisions none of which blocks the remaining work, and deciding this is a good place to stop. A hook or extension re-prompts a root that stops any other way, up to three times.

## Closeout

A loop closes when I say `close out`, on an emergency stop, or when no ready, unexcluded work is left and no lane or watcher is live. Then the root drains in-flight mutations, marks each landed task Done, stops its watchers and lanes, and writes the closeout. Its first line is `# Closeout: <repo> loop<N> · Charter: <charter sha256>`, followed by four sections:

- `## Landed`: a bullet per task with its SHA.
- `## Parked`: a bullet per task with its reason and resume condition.
- `## For ratification`: every ruling and open question, each with a recommendation.
- `## Notes`: the root's session id, a usage block from [my session catalogue](/articles/building-an-agent-transcript-indexer/) and anything else I should know.

Then it runs `codex-reconcile` once, so the closeout reaches my other Mac, replies with the closeout's path and stops. The next preparation starts from the Parked and For ratification sections, so nothing a loop left open is lost between runs.

<figure class="diagram figure-breakout">
  <img
    class="on-light"
    src="/diagrams/14-the-loop-lifecycle-v4-light.svg"
    alt="The root's scheduling cycle: feed queue from board, dispatch, return, accept or repair, compose and land, and back to feeding the queue. Dispatch gives one task its own worktree, up to the lanes cap. Accept or repair has a side exit, the tripwire: a new approach, a split, an escalation or a park. From anywhere in the cycle the root can end a turn on WAITING: &lt;what&gt; until &lt;time&gt;, which returns to the cycle on a lane return, a watcher or a timer, or on PAUSED: &lt;reason&gt;, which returns on resume. When no ready work is left and no lane or watcher is live, or on close out or an emergency stop, the closeout runs in order: drain mutations, mark landed tasks Done, stop watchers and lanes, write closeout, codex-reconcile, reply. Nothing follows the reply."
    width="1440"
    height="838"
    loading="lazy"
    decoding="async"
  />
  <img
    class="on-dark"
    src="/diagrams/14-the-loop-lifecycle-v4-dark.svg"
    alt=""
    aria-hidden="true"
    width="1440"
    height="838"
    loading="lazy"
    decoding="async"
  />
  <figcaption>
    Waiting and pausing are exits from the cycle rather than steps in it. Once the closeout file is
    written, only <code>codex-reconcile</code> and the reply follow.
  </figcaption>
</figure>

## Why it's built this way

Text enforces nothing, so the prose stays as short as possible. The charter states intent and authority in a few keys, the board holds the tasks, and the contract is short enough to read in full. Changes to the protocol follow one rule: delete before adding. A change removes text or moves a rule into a tool, and the size caps are tests, so a file that grows past its cap fails.

A rule that matters lives where no agent can skim past it: in `loop-lint`, a Stop hook, the pi guard or a test. The fences above are the exception, and the contract says plainly that no hook enforces them.

Every claim needs an observable check. A brief's `Done when:` is red at base and green on the candidate, a lane's status is never acceptance, and Done means landed in a pushed batch whose composed gate was green.

Only the root pushes, so one session owns main, and the tree that gets gated is the tree that gets pushed.

## Routes and harnesses

The contract names no model. Each harness maps five roles, `worker`, `senior`, `senior-escalation`, `reviewer` and `scout`, to agents with a fixed model and effort, on the [pi](/reference/loop-harness-pi/#routes), [Claude Code](/reference/loop-harness-claude/#routes) and [Codex](/reference/loop-harness-codex/#routes) pages. A Claude root runs Opus 5.5 at medium; a Codex root runs `gpt-6.1-sol` at high; a pi root runs `gpt-6.1-sol` at high from the launcher's root route, and the pi harness allows only `gpt-6.1-sol` and `gpt-6-astra`.

The pinned agents carry their own model and effort, so a root never passes a model override to one. pi gives a root 19 async slots at depth 2, Claude Code allows 30 running subagents and Codex 19 child threads. pi also has a fast launcher variant I add at launch for priority processing; it changes speed, never authority, and the planner never writes it into a launch file.

## Where to read it

The contract is at [/reference/loop-contract/](/reference/loop-contract/), the charter template at [/reference/loop-charter/](/reference/loop-charter/), the lane brief at [/reference/loop-lane/](/reference/loop-lane/), and the harness files at [/reference/loop-harness-pi/](/reference/loop-harness-pi/), [/reference/loop-harness-claude/](/reference/loop-harness-claude/) and [/reference/loop-harness-codex/](/reference/loop-harness-codex/). They are synced from my private source with private names replaced by neutral ones. The preparation steps and the standing-authority registry aren't published, so the preparation section above is a paraphrase.
