AI

Four ways my agent team's code could report a success that never happened

A code audit of the multi-agent team I run on one machine found four gates that turned a failure into a pass: a failed commit, a rejection read as approval, empty worker output and a CI check that never ran.

For agentsView .mdOpen in ClaudeOpen in ChatGPT
For agentsView .mdOpen in ClaudeOpen in ChatGPT
Contents
Four gates, four green results that were not trueA hardcoded committed flag gated PASS on an empty or broken workspace; the word approve in “Cannot approve - SQL injection remains” counted as approval; an empty worker output was stored as done; and an exit code of 0 from a check command made CI green with zero checks.The code readIt concludedWhat was true1the commit function’s dict:committed is Truegate PASSthe workspace was emptyor broken2the word “approve” in thereview textapproved, ship activated“Cannot approve - SQLinjection remains”3an empty worker outputstatus donenothing had been written4the exit code of gh prchecks: 0CI greenzero checks existed
Four gates, four green results that were not trueA hardcoded committed flag gated PASS on an empty or broken workspace; the word approve in “Cannot approve - SQL injection remains” counted as approval; an empty worker output was stored as done; and an exit code of 0 from a check command made CI green with zero checks.1readthe commit function’s dict: committed is Truesaidgate PASStruethe workspace was empty or broken2readthe word “approve” in the review textsaidapproved, ship activatedtrue“Cannot approve - SQL injection remains”3readan empty worker outputsaidstatus donetruenothing had been written4readthe exit code of gh pr checks: 0saidCI greentruezero checks existed
Each gate read a proxy the code itself produced, and concluded something the facts did not support.Source: my 2026-08-05 code audit of the team, read from the code.

On 2026-08-05 I finished a line-by-line audit of the multi-agent team I run on one machine: about 40k lines of real code across 60-plus modules, six parallel deep analyses with the critical findings verified file and line. The architecture is honest work. A planning agent turns a request into a spec, an architect lays out a DAG, coding agents run in parallel, up to three nodes at a time, then integration, tests, review, ship, with a fix loop back from failing tests. The workers are driven through a CLI coding agent launched with its approvals and sandbox flags bypassed and no timeout at all. On the happy path it really delivers, which is part of why the failure mode below stayed hidden.

The audit’s verdict on reliability was different. The team reports success that did not happen. I grouped the 5 audit findings in this class by area: 3 fake-done findings in the project runner, 1 in the delivery chain and 1 in the approval layer. Each one shares a shape: the gate asked a question the code never actually answered.

Where the four false successes sat in the pipelineThe team’s pipeline: a planning agent writes a spec, an architect lays out a DAG, workers run up to three at once, then tests, review and ship, with a fix loop from failing tests back to the workers; the delivery chain runs a CI check, a merge and a deploy. The four failures sit at the ship commit, the review, the workers’ output and the CI check.Specplanning agentDAGarchitectWorkersup to 3 at once3TestsReview2Shipgit commit1fix loop on failing testsCI check4MergeDeploydelivery chain
Where the four false successes sat in the pipelineThe team’s pipeline: a planning agent writes a spec, an architect lays out a DAG, workers run up to three at once, then tests, review and ship, with a fix loop from failing tests back to the workers; the delivery chain runs a CI check, a merge and a deploy. The four failures sit at the ship commit, the review, the workers’ output and the CI check.Specplanning agentDAGarchitectWorkersup to 3 at once3TestsReview2Shipgit commit1CI check4MergeDeploy
Numbered as in the text: 1 the commit at ship, 2 the review verdict, 3 the workers’ output, 4 the CI check.Source: my 2026-08-05 code audit of the team, read from the code.

A failed commit, gated as PASS

The shipper ran git commands and ignored their return codes. The result dict hardcoded {"committed": True}, so gate="PASS" was written even when the workspace was empty or broken. Nothing between that flag and the report asked whether a commit object existed.

The fix is one line: test commit.returncode before declaring the gate, and assert a commit SHA exists. When I later designed the permanent fix, it was a receipt at the end of every project, a receipt.json written once the run is over: commit SHA, number of passing tests, produced files with line counts, a hash of the sandbox log. A PASS that cannot point at a receipt is a claim, not a fact.

“Cannot approve” read as approval

The review gate ran a regex over the reviewer’s free text: re.search(r"\bapprove\b", ...). A review that said “Cannot approve - SQL injection remains” contains the word approve. It counted as approved, and ship activated. The reviewer had done its job correctly; the parser threw the negation away.

Two neighbours made it worse. The verdict default in the delivery path was the string APPROVE: if the review function was missing or raised, the verdict stayed APPROVE, and a green CI was enough to squash-merge without any review. In local mode every merge was “auto-approved”. And a second engine, kept for demos rather than the live path, matched “PASS” inside “password” and approved.

The fix is a parsing contract, not a better regex: the reviewer must emit its decision as the first token of its reply, APPROVE or REJECT, and anything that fails that contract resolves to a rejection. The fix list puts it the same way: a first-token contract with negation rejected, and the substring match removed outright. A verdict you grep out of prose is a sentiment, not a decision.

Empty output marked done

Workers were judged by a small set of error markers: only 3 of them counted as failure, an error marker, a timeout marker or a missing marker. A worker that returned nothing wrote nothing and was stored with status="done". The files a worker claimed to produce were never checked against the filesystem. The delivery path had the same shape further out: with no deploy function wired, deploy returned {"ok": True} and the run reported “deployed”.

The missing check is existence: after each worker, verify the reported files exist and are non-empty, with content that came from this run. “Done” should be a property of the filesystem, not of a status field. A status field describes what the agent intended; only the filesystem knows what happened.

A CI gate that went green without running anything

The CI gate ran gh pr checks --watch and read success as the exit code: ci_ok = returncode == 0. In a repository where GitHub Actions had never been activated, there are zero checks to watch, the command returns 0, and CI is green without having run a single check. The merge then proceeds on an unrun test suite, and the run’s report carries a green CI it never earned. I read this mechanism from the code; the record I have does not include a surviving run log of an actual zero-check merge, and I did not isolate which repos had Actions switched off.

The same file ran the pytest gate directly on the host rather than through the sandbox module, which quietly bypassed the sandbox module’s entire purpose. The checks the team trusted had, in two places, been pointed at the machine they were supposed to be guarded from.

The fix is a count, not an exit code: require at least one completed check with a name and a conclusion, and refuse to merge when the check list is empty. Zero checks is not green; it is missing data, and the fix list says to count the checks before believing the exit code.

Two more, because the pattern is not four accidents

The planning agent’s dependency graph could contain an A-requires-B, B-requires-A cycle. No node ever became ready, the graph drained its pending queue, and the finaliser’s else branch wrote status="done". Unreachable, therefore complete. The fix list has both halves: detect cycles when the graph is built, and make a drain that leaves pending nodes write stalled, a status the report has to render, instead of falling into the else of the finaliser.

The approval layer contributes 1 finding to the count, on the path where it never waited for a human. The max-iteration path logged the interrupt call, skipped the actual wait-for-approval call, and finalised the result anyway, so a runaway run reported a finished result rather than a question. The checkpoint module’s approve and deny functions had zero call sites, which made the pause-before-deploy entry in the team’s YAML config a decoration. And the webhook route that marks tasks done or rejected on a review event read the raw request body with no signature verification, even though two signature-verifying webhook implementations existed in the codebase and had never been mounted. Anyone who could reach the port could mark a task done. I did not test this from the network; the audit read it from the code.

What I take from it

Every one of these reports was true from inside its own function. The commit function’s dict said committed. The regex said approve. The status field said done. The exit code said zero. Each gate trusted a proxy that the code itself produced, and the one artefact that could have contradicted the proxy, the commit object, the reviewer’s intent, the files, the check list, was never consulted. The public role-based frameworks this space grew out of (MetaGPT, ChatDev, AutoGen, CrewAI) all put an agent in front of the verdict; I have not read their gate code, but in all of them the verdict reaches the pipeline as language. MetaGPT encodes standard operating procedures into role sequences, ChatDev chains chats in a fixed waterfall, AutoGen wires conversations, CrewAI assigns crew roles.

What each gate trusted, and what it should have asked forThe commit gate trusted the commit function’s dict and should have asked for a commit SHA that exists; the review gate trusted a word in the review text and should have required APPROVE or REJECT as the first token; the worker gate trusted a status field and should have checked that files exist, are non-empty and came from this run; the CI gate trusted an exit code and should have required at least one completed check.The gate trustedIt should have asked for1the commit function’s dicta commit SHA that exists, in a receipt2a word found in the review textAPPROVE or REJECT as the first token;anything else rejects3a status fieldfiles that exist, are non-empty and came fromthis run4the exit code of a commandat least one completed check with a name anda conclusion
What each gate trusted, and what it should have asked forThe commit gate trusted the commit function’s dict and should have asked for a commit SHA that exists; the review gate trusted a word in the review text and should have required APPROVE or REJECT as the first token; the worker gate trusted a status field and should have checked that files exist, are non-empty and came from this run; the CI gate trusted an exit code and should have required at least one completed check.1trustedthe commit function’s dictasked fora commit SHA that exists, in a receipt2trusteda word found in the review textasked forAPPROVE or REJECT as the first token;anything else rejects3trusteda status fieldasked forfiles that exist, are non-empty and camefrom this run4trustedthe exit code of a commandasked forat least one completed check with a nameand a conclusion
The fix in each case is evidence from outside the agent loop, and a missing measurement counts as a failure.Source: my 2026-08-05 code audit of the team, read from the code.

The rule I apply now: a gate’s evidence must come from outside the agent loop that it gates, and a missing measurement is a failure, not a pass. Empty output, zero checks, no commit object, no call site: all of them are absence, and absence rendered green is the most expensive bug class I have run into. The open question I have not answered, one machine and one audit’s worth of data: how many deliveries my team reported as complete were never complete at all.