A definition-of-done engine for coding agents

Write the definition of done.
Any agent does the work.
You get the receipt.

You write the checks; Claude, Grok, Copilot, or a local model works until they pass; and the run ends in a report you can hand to a reviewer — not the agent's word that it's finished.

# offline demo — no API key needed
$ npm run loopgen -- run examples/building-blocks/mock-demo.loop.yaml

[loopgen] running baseline evaluation (no agent) — disable with limits.baseline: false
[loopgen:iter0] starting iteration 1/5
  iter 1: retry — agent ok — ✗ answer-check · 1 file(s)
[loopgen:iter0] result: not yet — failing: answer-check
[loopgen:iter1] starting iteration 2/5
  iter 2: PASS — agent ok — ✓ answer-check · 1 file(s)
[loopgen:iter1] result: PASS — all checks passed

✓ SUCCESS — mock-demo
outcome: success — all checks passed
baseline: checks fail as expected — failing: answer-check
iterations: 2, time: 0.0s
changed: 1 file(s)

Abridged. The real run also warns that its workspace isn't a git repo, so change detection fell back to content hashes and there's no unified diff. That caveat is part of the receipt — a thinner receipt says so. Run it a second time and it fails baseline-vacuous after zero agent iterations: the answer is already there, so there's nothing to earn.

Start where you are

Three ways in

Platform & senior engineers

Run it where nobody is watching

Overnight, in CI, across a fleet. Every run ends in a report with one of nine outcomes, each iteration's results, and the diff of the real work — and streams the agent's trajectory as JSONL or standard OTLP spans to whatever you already use.

What the report can tell you →
Observing runs →

Solo builders

Describe the app. Wake up to one that works.

Cut a product into slices a check can observe, give each its own red-then-green checks, and run the whole graph as one batch. A ten-slice coaching portal built this way came back 10/10 for about $1.77 — and the report showed the work, not just the "done".

From one loop to an app →
Run it on a local model →

Engineering leaders

Agent work you can review, audit, and budget

The loop is a versioned file your team code-reviews, not a script on someone's laptop. Spend is capped per run. The report is an artifact you can keep — the difference between a party trick and a process.

What should a leader actually do about agent loops? →

How it works

The checks are the contract

One .loop.yaml describes the task, the workspace the agent may edit, and the checks that are red today and go green only when the work is really finished. The engine drives the agent, feeds every failure back, and repeats — until the checks pass or the budget runs out.

.loop.yamltask + checks
drive agentany backend
auditdiff + guards
evaluatechecks that say no
feedbackfailures + last diff
reportthe verdict

You write what done means

If you can't write a check that's red before the work and green only when the requirement is met, the work isn't ready for an agent yet. loopgen lint and generate --verify tell you that before a token is spent.

Any agent grinds toward it

The agent is a plug-in — claude-agent-sdk, grok, github-copilot, opencode with a local model, or a scripted mock — held to the same contract whichever you pick. Each round it sees what still fails and what it changed last time.

You get the report

What the agent changed, which checks it passed, what it cost, and whether anything it wasn't allowed to touch was touched. If the green wasn't earned, the report says so in one word — and says why.

Why the green is earned

What the report can tell you

Reward hacking isn't hypothetical — frontier coding models have been caught special-casing tests, hard-coding expected values, and editing the very test files that grade them. Most loop runners take the agent's word for it. This one treats every green as a claim to be checked — and the report tells you what it checked. All of it is on by default; the one thing you add yourself is a spend ceiling, because only you know the number.

Real files changed

Every iteration is diffed with build and runtime artifacts excluded. A green run that changed no source files is flagged as likely vacuous, not celebrated.

Always on → warning on a green run

The agent actually finished

Drivers report an honest stopReason. An agent that errored or ran out of turns on an otherwise-green run puts a caveat in report.warnings — a warning on a green run is a finding.

Always on → warning on a green run

The tests weren't edited

The test files a check runs are the contract. They're hash-watched across iterations — detected from the command, plus any explicit guard: paths.

Default (evaluatorGuard: error) → run fails evaluator-tampered

The success criteria weren't rewritten

The .loop.yaml is excluded from the work diff and hash-watched when it lives in the workspace. A mid-run edit isn't a clever fix; it's a failed run.

Default (specGuard: error) → run fails spec-tampered

The checks were red before the work

Baseline evaluation runs every check before the agent's first turn. Already passing? Then the checks don't measure the requirement, and no agent turn is spent finding out. Set baseline: false only for checks whose side effects must not run twice.

Default (baseline: strict) → run fails baseline-vacuous

Spend stayed under a ceiling you set

maxCostUsd and maxTokens cap cumulative spend. A satisfied iteration always reports success first — only further spend is refused.

Opt-in: set a ceiling → run stops budget-exceeded

Every way a run can end

success — checks pass, guards quiet
spec-tampered · evaluator-tampered
baseline-vacuous — checks never went red
budget-exceeded · max-iterations
preflight-failed · aborted · error

No outcome is inferred from the agent's self-report. "Done" is a rule over your check results — never the model's opinion.

And a linter that runs before a single token is spent

loopgen lint catches the misconfigurations that fake results upstream — self-fulfilling smoke tests, wrong workspace, missing binaries, destructive checks — in milliseconds. When you don't trust the verdict, the trace shows the agent's every turn.

Every guard and rule, documented →

From one loop to a whole app

The same discipline, at the size of a product

One loop delivers one checkable outcome. An application is a graph of them. Cut it into the smallest slices a check can observe, order them by what each needs built first, give every slice its own red checks, and run the graph as a .batch.yaml — dependency-ordered, concurrency capped, never two loops in one workspace at once.

The authoring pipeline, as Claude Code skills

frame-app turns a product spec into a dependency graph of slices. frame-checks turns one slice into acceptance checks that are red now, for the right reason, and hard to fake. author-loop writes the spec and proves it lints clean and starts red. debug-loop reads a failed or suspicious run and finds the fix without spending agent budget. They load when you open the repo in Claude Code.

Agent-assisted authoring →

Run end to end

A ten-slice leadership-coaching portal — sign-in, clients, surveys, notes, a client portal with role separation — built from a plan, ten check files, and one batch manifest. Result: 10/10 trustworthy greens under a strict baseline with the evaluator guard armed, about $1.77 and nine minutes of agent time. The write-up also records what the experiment didn't show.

Read the run →

The spec

The whole contract in one file

Version it, review it, reuse it. loopgen generate -i --verify scaffolds one interactively and proves it's lint-clean and starts RED.

name: add-retry-to-fetchUser
task:
  type: function
workspace:
  dir: ./target
requirements: |
  Add exponential backoff (max 3 retries) to fetchUser(). Keep the signature.
driver:
  uses: claude-agent-sdk
evaluators:
  - uses: command
    as: tests
    options: { command: npm test }
  - uses: command
    as: typecheck
    options: { command: npx tsc --noEmit }
success:
  type: all-pass
limits:
  maxIterations: 6
  baseline: strict      # fail fast if the checks were already green
  specGuard: error      # editing the spec fails the run
  evaluatorGuard: error # editing the tests fails the run
  maxCostUsd: 5.0
observability:
  observers:
    - uses: otlp           # stream the run as spans to any OTLP collector
      options: { endpoint: http://localhost:4318/v1/traces }

The guards are three lines of YAML. Everything after that is the engine's problem — including proving it to whoever reads the report.

Any agent

The harness isn't sold by the agent's vendor

AgentDriver

One interface, five backends

claude-agent-sdk, grok, github-copilot, opencode — including LM Studio and Ollama models through it — or a scripted mock for offline runs. Swap with --driver; the spec doesn't change.

Evaluator

Checks that can say no

command covers anything with a CLI and an exit code — tests, linters, type checkers, builds. experiment compares a numeric metric against thresholds or baselines. Or register your own in ~15 lines.

Conformance

Even the drivers are audited

New backend? loopgen verify-driver runs it against a behavioral contract — reports a name, does requested work, applies feedback, handles aborts — before it's trusted inside a loop.

Straight talk

Where this pays off — and where it can't

GOOD FIT  Mechanically checkable work

Overnight and CI runs nobody is watching. A failing test that encodes the new behavior. A metric to move: p95 under X ms, coverage ≥ 90%, bundle under Y KB. Repo-wide remediation against a concrete check. Fleets of agents whose word you'd rather not take.

POOR FIT  Judgment-heavy work

"Improve the architecture." "Make the UI delightful." No exit-zero definition of done means the loop can only tell you the checks were the wrong contract. A passing check doesn't make subjective work good.

And honesty about the guards themselves: they make mis-specified checks, gamed metrics, and context drift visible — they don't eliminate them. If you can't write a check that's RED before the work and GREEN only when the requirement is met, the task isn't ready for a loop. How to prove that up front →

Extending it

Four plug-in points. Zero engine edits.

Drivers, evaluators, task types, and observers are typed registries — register your own and pass them in. The engine, and its guards, never change.

// a custom evaluator, complete
import { type Evaluator } from "loop-generator";

export const coverage: Evaluator = {
  type: "coverage",
  async evaluate(ctx) {
    const pct = await measure(ctx.workdir);
    return {
      passed: pct >= 0.9,
      score: pct,
      feedback: `coverage ${(pct*100).toFixed(1)}% (need ≥ 90%)`,
    };
  },
};
// a custom observer: ship the evidence anywhere
import { type Observer } from "loop-generator";

export const slackObserver: Observer = {
  name: "slack",
  begin({ runId, spec }) {
    return {
      onRunEnd: (report) =>
        postToSlack(
          `${spec.name}: ${report.outcome} ` +
          `in ${report.iterations.length} iterations`,
        ),
    };
  },
};

Embedding it? The engine is a library too — new LoopEngine(createDefaultRegistries()), with an onAgentEvent callback for streaming the trajectory yourself.

Learn it

The workshop runs the real engine in your browser

Seven modules, no API key. Step through scripted runs — the happy path, a vacuous baseline, a spec tamper, a blown budget. Build the exact prompt the agent sees. Pick an attack and watch which guard catches it, and which two don't yet. The demos import the engine's own functions, so what you see is what ships.

Green is a claim. Get the receipt.

Write the checks once. Let any agent grind toward them. Trust the report because it shows the work — not because the agent said so.