Loop protocol / The toolkit

Every tool in the loop, and what it's for

What I run and why: the harness and plugin, the board, the task surface, the gates, three reviewers, code search, docs lookup, the transcript catalogue and the rules.

Nothing here is required to run a loop except Claude Code, the plugin and a Backlog.md board. Everything else earns its place by removing a failure I kept seeing. Each entry says what the tool does in a loop, how I use it, and how to get it or what to use instead.

The harness

Claude Code and the agent-workflows plugin

The root is one long Claude Code session on Opus 5.5 at medium effort. Lanes are subagents from the plugin, each with its model and effort pinned in its definition: worker (Sonnet 5.5, high) writes one task, senior (Opus 5.5, medium) takes judgement calls and composes landings, senior-escalation (Opus 5.5, xhigh) takes a task that has failed its repairs, reviewer (Opus 5.5, high) reports and never edits, and scout (Sonnet 5.5, medium) maps and sweeps. A model written in a brief does nothing, and a model passed at dispatch overrides the pin, so the root passes neither.

Lanes on routine work don't get an Opus reviewer by default: each runs dsreview, a DeepSeek Flash reviewer, on its own patch, and the reviewer lane is kept for guarded work and for the cases dsreview can't settle.

The plugin also carries the loop skill that prepares a run, a Stop hook that re-prompts a root ending its turn before its closeout exists, a hook that refuses a lane brief without the protocol's header line, and loop-lint.

Get it: rknightion/agent-workflows. The routes are on the Claude Code harness page.

pi and Codex, for OpenAI models

When I want OpenAI models on a loop I use pi with my loop-pi extensions, which add event-driven waits and a per-request time limit. Codex runs a loop only when I name it, because its root can't wait cheaply on a long gate. I ran Claude Code on GPT models through a LiteLLM gateway for a few weeks and retired it in October 2026. If you only use Claude, skip this entry.

Read: Why I don't use Codex for long-running agent loops, Bounding a model request on pi.

Planning and state

Backlog.md

Every task lives on a Backlog.md board in the repository, as Markdown files the CLI owns. The root admits work from the board, a lane's acceptance criteria come from its task, and a task is Done only once its change is pushed. Agents use the CLI, never the files, and a few of its flags silently replace another session's writes.

Read: Setting up Backlog.md for agents.

LOOP.md, the charter and the run directory

LOOP.md holds the repository's standing facts: tier, gate command, required CI checks and lane rules. Each run gets a frozen charter of at most 8 KB and a run directory under codex/ (never committed) holding the queue, the lanes, decisions and every lane's patch and return. The closeout is the only thing the next run needs.

Read: the contract and the charter template.

Proving a change

just

Every repository has a top-level justfile and nothing else as its task surface. Seven recipes are mandatory: default, setup, fmt, fmt-check, lint, test and check. just check is the gate a lane must pass and the gate CI runs, so an agent never has to guess how to prove a change.

Here is the gate from the repository in In practice, a Go API and worker with a TypeScript front end. check times itself and runs one private graph, so a dependency two legs share runs once:

# THE GATE: generated-code drift, Go and frontend checks, builds, and race tests
check:
    node internal/devtools/ci-timing.mjs run check -- just _check < /dev/null

[private]
_check: integration-shard-coverage check-gen check-vocabulary check-analyzer-fingerprint \
        check-go check-connection-budget check-migrations scan-go-vulnerabilities \
        web-audit test-devtools
LegWhat has to be true for it to go green
integration-shard-coverageEvery package with integration tests is assigned to a CI shard. It runs first, so a new package that CI would silently skip fails in seconds.
check-genRegenerating the sqlc database code, the OpenAPI server, the TypeScript client and the parser changes nothing; the OpenAPI spec lints clean and has no breaking change against main; the workflow audit (zizmor) passes; every table, API operation and checked acceptance criterion maps to a recorded decision.
check-vocabularyThe front end passes Prettier, ESLint with TypeScript, its component tests and a production build, loads no external assets, and its user-visible copy uses the product's agreed words.
check-analyzer-fingerprintGenerated fingerprints of three entry points' code are current, so a stored result can't be reused after the code that made it changed.
check-gogofmt and goimports, go vet, golangci-lint, go mod tidy -diff, go test ./... -race, a second pass under the production build tag, a scoring corpus, an import-boundary check, k6 inspect on generated load scripts and go build ./....
check-connection-budgetThe Postgres pool sizes in the Helm chart fit the server's connection ceiling with a reserve.
check-migrationsNo migration takes a heavy lock unless a reviewed allowlist names it.
scan-go-vulnerabilities, web-auditgovulncheck and npm audit are clean.
test-devtoolsThe repository's own tooling scripts pass their tests.

The integration suites (fourteen Postgres-backed shards in containers) and the Playwright browser suite aren't in check, because they need Docker. A lane runs the shards its packages touch; the composed gate before a push runs all of them. How a change is proven has the legs.

Three habits make the gate trustworthy for agents:

  • Every tool is pinned in the justfile, with a # renovate: comment so Renovate bumps it. A recipe uses the binary on PATH only when its version matches the pin, and otherwise runs go run tool@<pin>, so a lane and CI run the same version.
  • Lint findings are excepted one at a time. golangci-lint runs the standard set plus gosec, and each exception names the file, the linter, the message and, where it can, the exact source line. A new instance of an excepted finding still fails.
  • Every recipe reads stdin from /dev/null, so nothing in the gate can wait for input an agent will never type.

The repository's Backlog config makes just check the definition of done, so every task's acceptance criteria include it.

Get it: casey/just. Rule: justfile task surface.

Remote gate runners

Fifteen lanes each running a race-enabled Go suite and a Postgres container will flatten a laptop. My heavy gates run on three EC2 hosts instead, and a lane calls them with one command:

gaterun-slot <lane> -- gaterun --log <lane dir>/gate.log -- just check
  • The hosts are Graviton m9g.8xlarge instances (32 vCPU, 128 GiB) on Ubuntu 24.04 arm64, each with a 500 GiB gp3 volume for caches and twelve slots: 36 in all. One repository can hold at most ten slots on a host, so one loop can't starve the others.
  • Nothing gets in. The hosts have no inbound ports: SSH arrives over a private mesh VPN. Outbound they reach the internet for images and modules but not private address ranges, and a gate container holds no credentials. A gate that needs a secret or a private network runs locally.
  • A run is one container. The client pushes the base commit to the host's git cache and uploads the uncommitted and untracked changes with a checksum. The host takes a free slot, resets that slot's worktree to the base, applies the change and starts the gate in a container from a pinned image (the CI runner image plus Go, Node, ripgrep and the release tools). Go, npm and Playwright caches are shared per user across runs. When the client dies, the gate is killed; when the gate ends, its containers and test databases are swept.
  • No slot is not a failure. The client tries the next host, then waits up to four hours. The exit status is the gate's own; 70 means the run couldn't be prepared and 124 means no slot came free. Only then does a lane run the gate locally, under a wrapper that allows two heavy runs on the machine at once at low priority, and it says so in its return.
  • Pacing is by slot, not lane count. gaterun-slot spreads a machine's runs across the hosts. The hosts export busy slots and queue wait as metrics, and the root admits more work when fewer than fifteen of the repository's slots have been busy for twenty minutes.

Across four loops in October 2026 the hosts ran 4,915 gates for the repository in In practice, timed by the runner from start to end:

Gate (passing runs)RunsMedian90th percentile
Full just check91015.8 min18.6 min
One integration shard2,2616.1 min15.6 min
Browser suite22011.3 min12.4 min

Shards range from about a minute to about twenty, so the composed gate takes as long as its slowest shard. Busiest day: 1,576 runs, peaking at 30 at once on 36 slots. 22 per cent of runs waited for a slot, a median of 36 seconds and a 90th percentile of about four minutes; none hit the four-hour limit, and lanes fell back to a local run at most twice in about 1,230 lanes. Half the failing just check runs ended within about two minutes. I have no like-for-like local timing: the full gate never runs on the laptop.

Each repository declares what its gate needs in a short contract:

# .gaterunner.yaml
env:
  APP_TOOL_CACHE: "${GR_TOOL_CACHE}"   # tool downloads persist across runs
artifacts: [web/test-results, web/playwright-report]   # kept with the run for reading after a failure
deprioritise: ["^app-.*e2e"]          # the first containers the host may OOM-kill
browser_instance_env: APP_E2E_INSTANCE  # parallel browser suites don't share ports

The tool is private because it's tied to my network. Instead: any machine you can run a container on, or CI on a pushed branch. The ideas that matter are a slot limit, so lanes queue for the gate instead of fighting over CPU, and running exactly the tree the lane has, committed or not. Rule: gate runner.

Reviewing a change

Three reviewers with different strengths: CodeRabbit on every candidate, a DeepSeek Flash reviewer (dsreview) on every candidate, and an Opus reviewer lane on guarded work and disputes. How a change is proven has the order and the evidence for each.

CodeRabbit CLI

Each lane runs coderabbit review --agent once on its final candidate and fixes every critical and major finding. My own repositories commit straight to main, so this is the code review those commits get. Exit 0 isn't a clean review: a run with no completeline failed.

Get it: CodeRabbit CLI. Rule: CodeRabbit is the review gate.

dsreview, a DeepSeek Flash reviewer

A single-file Python tool that gives DeepSeek Flash the lane's brief, the candidate diff and read-only tools (read a file, grep, list files) over the candidate tree, and returns a PASS or FAIL verdict with findings as JSON. On 59 replayed reviews it caught the same number of defective changes as an Opus reviewer lane, with no false blocks, at about a twentieth of the cost. It replaces the Opus reviewer on routine work.

Get it: tools/dsreview in the agent-workflows plugin. It calls DeepSeek through a Cloudflare AI Gateway and refuses any repository whose owner you haven't allowed.

The Opus reviewer lane

A read-only reviewer subagent on Opus 5.5 at high effort. It runs on every guarded change before it lands, with a security brief, and on routine work when dsreview gives no verdict, when a lane disputes a dsreview finding, on a second repair round, or when CodeRabbit didn't run.

Finding code and facts

A language server

Claude Code's LSP tool answers definition, reference, implementation and call-hierarchy questions exactly, so an agent trusts its answer instead of grepping and re-reading files to confirm one. I give gopls my test build tags so tagged test files resolve.

jg

jevgrep answers "where does this behaviour live?" in unfamiliar code when there's no symbol or string to grep for. It sends the searched source to a model provider, so it is scoped to one package and skipped when the brief already names the files.

Rule: code navigation. Read: Jev as a decision layer.

Firecrawl for library docs

Agents look up a library's current API instead of answering from training data. Firecrawl's developer search covers issues, merged pull requests and docs, which is where version-specific behaviour actually lives.

Rule: Firecrawl.

Memory and retrospectives

agent-history

Every Claude Code, Codex and pi transcript loads into one Postgres catalogue: sessions, tool calls, lanes, loop runs, commits and cost. It answers "what did we decide about this last week?" and "how did that loop actually go?" with a query instead of a guess. In practice is built from it.

Get it: rknightion/agent-history. Read: Building an agent-transcript indexer.

Rules, hooks and handoffs

Every session starts with the same short rules: an operating model, how to dispatch a subagent, when to run CodeRabbit, how to commit without taking another session's staged files. Hooks enforce the few that matter most, such as refusing git add -A. Rules are written as instructions, not explanations, because every word is paid for on every turn.

Read: The rules my agents run with, and the published rules under Reference.