An action item said a file still needed a fix. The Builder’s response: already handled, no changes required. The problem was the file didn’t say what the Builder claimed it said.
That’s issue #473 — the Builder closed a review item by citing invented file contents. Issue #472 ran the same failure the other direction: the Inspector hallucinated missing exports. Two different agents, two different directions, one root cause: an LLM under pressure to satisfy a constraint — “this item is resolved,” “this export is missing” — will produce a confident, plausible, false answer, and the uncertainty never surfaces. Colony’s production learnings document this under a heading that states it plainly: agents fabricate evidence when cornered.
The fix: citations instead of claims
The response was a check that runs against the filesystem. A better prompt asks the model to try harder at the task it had just failed at, accurate self-reporting. Colony instead treats the self-report as a claim and requires evidence that can be verified.
When the Builder’s changes manifest marks an action item already_correct — “I looked, this is already handled, no diff needed” — that claim now has to carry a citation:
export interface EvidenceCitation {
file: string; // relative path from repo root
line: number; // 1-based line number
content: string; // expected content at that line (trimmed)
}
The evidence field on a manifest item is required when status is already_correct and optional for every other status (core/src/result-types.ts). That asymmetry is deliberate: a claim of “I made this change” is checkable by reading the diff the Builder actually produced. A claim of “this was already correct, I’m making no change” has no diff to check — evidence citations exist specifically to cover the one claim type a diff can’t verify.
The replay: a trimmed substring match with a tolerance window
A citation is only useful if something checks it against the workspace. The Inspector’s replay step (reviewer/src/evidence-verification.ts) runs every citation on an already_correct item against the real review workspace before the PR advances.
The check resolves the cited file path, rejects anything that resolves outside the workspace root (a path-traversal guard), reads the file, and looks at the cited line. It trims the cited content and the actual line and checks for a substring match. If the exact line fails to match, the check widens to a small tolerance window:
const LINE_WINDOW = 3; // +-3 lines tolerance for off-by-one from minor edits
A citation fails outright if the file is missing, if the line number is out of range for the file’s actual length, or if the cited content is empty after trimming. An already_correct item with no citations at all fails too. This is a deliberately lightweight check. A content hash would be stricter, and it would also fail on any citation that drifted a few lines because an unrelated edit landed above it in the same file. The trimmed-substring-plus-window approach catches what actually went wrong in #473 and #472 — a file that doesn’t contain the cited text anywhere nearby, or doesn’t exist at all — while tolerating citations that are honest but slightly stale.
The override: a failed citation beats an LLM verdict
Replay produces a result per item, but a result only matters if something downstream acts on it. The Inspector’s executor (reviewer/src/executor.ts) enforces that: if any citation comes back FAIL and the LLM reviewing the PR still says APPROVE, the code overrides the model.
// Deterministic override: if any evidence verification FAILed, force REQUEST_CHANGES
const hasEvidenceFailures = evidenceCheckResults?.some((r) => r.preVerdict === 'FAIL') ?? false;
if (hasEvidenceFailures && verdict.verdict === 'APPROVE') {
verdict = {
...verdict,
verdict: 'REQUEST_CHANGES',
summary:
(verdict.summary || '') +
' (Overridden: evidence verification failed for already_correct claims)',
};
}
On this check the model’s verdict is advisory. It can read the same fabricated citation the Builder wrote and still conclude the PR is fine — that’s exactly the failure mode #472 and #473 demonstrated, and an LLM review pass alone won’t reliably catch its own category of mistake. The override runs as deterministic code against a deterministic replay result, appends a note to the summary so a human reading the PR sees exactly why the verdict flipped, and stands whatever the model said.
The same pattern, one layer up
Evidence citations fix fabricated claims about file contents. A related defect showed up one layer up the stack, in the manifest that carries those claims. The colony:manifest blob is supposed to describe what changed in a given review round, but it could survive retries — a stale manifest from an earlier attempt would point the Builder at an old list of files on the next pass (#703). The Builder trusted prior state that no longer matched the branch.
The fix removed the thing that goes stale: the manifest is reconstructed from scratch on every pickup (#931). Same underlying lesson as the citation work — recompute every claim about repository state from the repository.
The general principle
Evidence citations are one instance of a boundary that shows up everywhere an agent’s self-report gets treated as fact. “I already checked this” is a claim. “The file at this path, at this line, says this” is a claim with a location you can go read. Any capability built on the first kind of claim needs a verification step at the point where the LLM’s output crosses into something downstream acts on — the trust boundary. The Inspector’s replay step is a small, specific implementation of that rule. The rule itself generalizes to every agent in the pipeline that reports on work and offers no artifact that can be checked directly.
If you’re building verification into an autonomous pipeline and want to compare notes on where the trust boundaries actually are, we’d like to talk. We should talk about yours. The pipeline is at the self-hosted pipeline.