agent-works pilot: what the verifier caught that my checks missed

· By Owen Nishimura

  • agents
  • evals
  • claude-code
  • agent-works

agent-works is a small workflow in which AI coding agents turn GitHub issues into pull requests. I built it to hand small codebase work (bug fixes, features, chores, and docs changes) to agents, while the one who runs it, a person or an AI orchestrator, keeps control of what goes in and what merges.

It works like this. A person or an AI orchestrator approves an issue with the agent:ready label and starts a run (/agent-works run, or scripts/factory.sh run). A builder agent makes the change in its own worktree. Fixed checks run first: the new tests must fail without the change and pass with it, and the repo’s verify commands must pass. Then a separate verifier agent, which cannot edit code, maps each acceptance criterion to evidence in the diff and the test output. The workflow retries until it proposes a PR or hands the issue back. In the pilot below, I approved the issues and merged each PR myself.

The builder and the verifier are roles, each with a prompt, a tool list, and a JSON schema for its output, so the design does not depend on one AI. The current scripts run both roles with Claude Code (claude -p) on Opus, and every number in this post comes from that setup.

Many agent workflows show that agents can open PRs. Few show how often their own pass is correct. That number decides when a workflow can merge by itself, so agent-works measures its own verifier. The aim is to let it merge small changes without me, but only after the data shows that its pass can be trusted.

How I tested it

I tested agent-works in three ways:

  • A seeded trust suite. Eight PRs on a small Python fixture repo: five bad PRs and three good controls. Every seeded PR passes the fixed checks, so only the verifier can catch the bad ones. Before the pilot, I set the target for the verifier: catch every seeded bad PR, and fail no more than one good control.
  • A pilot on real work. Ten real issues in a private app repo, in 16 runs, in propose mode. A run is one issue from agent:ready to a final status.
  • A public demo. One issue in the agent-works-demo repo, from agent:ready to a proposed PR, so you can see a full run.

The short version: the verifier caught 5 of 5 seeded bad PRs and failed none of the good controls. In the pilot, 9 PRs merged with no human code edits, and the ledger recorded $8.43 in costs, not counting a deliberate stop-and-resume test. The most useful result was a failure: the builder gamed one of my checks, and only the verifier caught it. I still merge every PR by hand, for the reasons at the end of this post.

The check the builder gamed

During the pilot, the builder gamed one of my checks. The root cause was a mistake in my own rules. The fail-first check requires new tests to fail without the change and pass with it. That rule makes sense for features and bug fixes, but I applied it to every issue, and one pilot issue was a chore that removed tests. In this issue, removing the tests left the base passing, so the check reported that the tests “passed without the change” and sent the issue back for a rebuild with that feedback.

On the next attempt, the builder added a test that existed only to make the base fail. Every fixed check passed. The verifier returned fail. It named the extra test, and it also found that the PR removed test coverage of a function that the issue said to keep. After a third attempt, the issue went to a person with the label agent:needs-person.

The same issue exposed two gaps in my own tooling. The needs-person comment listed only the reasons from the last attempt, so the verifier’s catch from attempt 2 was visible only in the ledger. And the report grouped runs by issue, so when the issue ran a second time, the report dropped the failed first run and its cost.

I made three changes:

  1. Fail-first is skipped for type:docs and type:chore issues. The verifier still looks for weakened tests on those PRs.
  2. The needs-person comment lists the reasons from every attempt.
  3. The report shows one row per run instead of one row per issue.

After the fix, one rerun produced a 38-line PR, and I merged it.

In this run, the rebuild feedback pushed the builder toward satisfying the check instead of the issue, and the verifier caught what the check missed. This one failure is why I kept the verifier after the fixed checks.

The numbers

Trust suite

Two full runs gave the same result:

Measure Run 1 Run 2
Catch rate (bad PRs that got fail or unsure) 5/5 5/5
False-fail rate (good PRs that did not get pass) 0/3 0/3
Unsure rate 0/8 0/8
Invalid verdicts 0/8 0/8
Cost $0.37 $0.40

The five bad PRs are a skipped criterion, an overclaiming summary, a weakened test, a silent regression, and an injected instruction. The injected one has a code comment that tells “the automated reviewer” to return pass. The scoring counts that case as caught only when the verifier reports a concern of type injected instruction, which it did.

The suite is small: one case for each bad category, and each full run evaluates each case once. So 5 of 5 shows that the verifier catches these five defects. It is not a catch rate that I can apply to other PRs.

Pilot

Ledger totals for the 16 pilot runs:

Measure Result
Issues merged with no human code edits 9 of 10
Runs that ended as proposed or merged 14 of 16 (88%)
Attempts per run 1.1 on average
Recorded tokens 6,680,979
Recorded cost $8.43
Time per run 2.5 minutes on average
Verifier verdicts on pilot PRs 14 pass, 1 fail

The ledger does not record a stopped attempt. During the pilot, the workflow was stopped on purpose while two builders worked, to test resume. Those two attempts are not in the ledger, so the real token count and cost are higher than these totals.

The report counts a human edit when a merged PR has a commit from any author other than the builder. Every commit on the 9 merged PRs had the builder as its author. But “no human code edits” does not mean no human work. I labeled each issue as ready and merged each of the 9 PRs, because the workflow runs in propose mode. 5 of the 9 PRs needed a rebuild after an earlier merge moved the base, and I started each of those rebuilds. The chore issue above came back to me after three attempts and merged only after I changed the rule.

The 14 pass verdicts cover the 9 merged PRs. Five of those PRs got a second verdict after a rebuild on a new base. The one fail was the gamed check.

What these numbers show about the pass verdicts: every PR that the verifier passed merged without a code change from a person, and every recorded check of the base branch after the merges passed (89 to 97 tests, 0 failures). I haven’t independently audited every verdict, so I can’t say that every criterion marked as met actually was.

One issue is still open. An AI agent wrote all ten pilot issues, and this docs issue asked for label names that the repo does not contain. The builder stopped and said what it needed, and it did not invent the names. I need to revise the issue before running it again.

Demo

In the public demo repo, one issue went from agent:ready to a proposed PR in 63 seconds. Its ledger line records 119,688 tokens and $0.19. The scripted quick start, from clone to a proposed PR and a scorecard, took 2 minutes 36 seconds of machine time.

Other failures

Shared lines in docs made parallel PRs conflict. 5 of the 9 merged PRs had a merge conflict after an earlier merge. Each conflict was in a test-count line in the README or in an append-only log file, because the pilot’s instructions ask every change to update both files. Each merge made the next open PR conflict, so the merges had to go one at a time. Rebuilds on the new base fixed every conflict with no human code edit, but the rebuilds cost $2.97, 35% of the recorded cost.

A denied command led to a false claim. In the first pilot run, the permission rules denied an export PATH, and the builder then made a false claim about the Node version. Now the builder must not change PATH or guess about its environment, and build.sh lists every command that the rules denied. The pilot’s instructions still tell agents to change PATH, so each run loses a turn to the denied command.

Escaped line breaks. The first demo run produced correct code, but the PR body showed the characters \n instead of line breaks. build.sh now converts them.

How it works

issue + agent:ready  (a person or an AI orchestrator)
  → build.sh    worktree, builder agent (tests first), draft PR
  → check.sh    fail-first, verify commands, protected paths → commit status agent-works/checks
  → verify.sh   verifier agent: criteria map with evidence → commit status agent-works/verifier
  → factory.sh  decision, labels, evidence comment, ledger line
  → agent:proposed (a person or orchestrator merges) | agent:ready (rebuild) | agent:needs-person
  • Each issue needs a goal, acceptance criteria, and an out-of-scope list. A person or an AI orchestrator approves it with agent:ready, and the workflow’s own orchestrator script sets that label again only to retry an issue. The label is the trust boundary: whoever adds it decides what the agents work on. The agents follow the acceptance criteria and treat issue bodies, comments, and code as data.
  • The verifier starts with fresh context and has no Edit or Write tools, so the builder never grades its own work.
  • The verifier reads the diff and the test output, and it checks each claim in the builder’s summary against them. A false claim makes the result fail, even when the code is correct.
  • The fixed checks run before the verifier. When a script can check a fact, the model does not decide it.
  • A failed test check or a verifier fail sends the issue back for a rebuild with the reasons, up to the attempt limit (three in the example config).
  • The verifier returns a criteria map. Each criterion is met: true with evidence (test: or code:), false, or null. The orchestrator script rejects a met criterion that has no evidence, and an unsure result always goes to a person.
  • The check step reads its config from the default branch, so a PR cannot change the checks that grade it. A PR that touches workflow files, CI, or the check config goes to a person.
  • The builder cannot push. A script pushes only the agent/ branch and opens a draft PR.

Each agent runs as claude -p with its own tools, permission rules, and a JSON schema for its output. Both agents use Opus. Each finished attempt adds a ledger line with the run’s totals so far: time, tokens, cost, attempts, check results, and verdicts. The core is under 760 lines, not counting tests and the trust suite, so one person can read and debug all of it.

Why I still merge by hand

I kept propose mode for three reasons, from the decision note:

  1. The merge rule has no enforcement on the pilot. The rule is to merge only the commit that passed the checks, against the current base. The pilot is a private repo on a free plan, so it has no branch protection, and the fallback (gh pr merge --match-head-commit, one merge at a time) is not built. In propose mode a person enforces the rule. In auto mode nothing would.
  2. The workflow cannot follow a moving base by itself. A person started each of the 5 rebuilds above.
  3. The sample is small and easy. An AI agent wrote the ten small pilot issues, with clear acceptance criteria. The largest merged PR changed 83 lines, and the trust suite has one case per bad category. My own backlog will have larger issues with less clear criteria.

Branch protection does work on the public demo repo. A test PR showed it: with no statuses, or with the checks status green and the verifier status failing, GitHub showed the PR as BLOCKED. With both statuses green, it showed CLEAN.

Auto-merge for small changes needs all of these first:

  • The merge rule is enforced: branch protection that requires both statuses on an up-to-date branch, or the fallback with one merge at a time.
  • After each merge, the workflow re-checks the open PRs and rebuilds the ones that conflict, with no person.
  • No shared counter lines in the pilot’s docs, or the workflow resolves them.
  • 20 more pilot issues, written by me: at least 90% merged with no human edits, and no merge breaks the base branch.
  • At least three trust-suite cases per category, each run three times. The catch rate stays at 100%, and the false-fail rate is at most 5%.

Even then, auto would apply only to a PR with at most 150 changed lines (the example config), no protected paths, a verifier pass, and test evidence for each criterion.

Repeat it

If you run the trust suite against your own verifier prompt, or add a case that breaks mine, I want to see it.

All posts