Skip to content

Block the Bad Agent in CI: Gate on Trajectories, Not Just Outputs

26 min read

Block the Bad Agent in CI: Gate on Trajectories, Not Just Outputs

· 26 min read
An editorial illustration on warm cream paper in black ink line work: a boom gate or turnstile blocking a small robot arm that carries a parcel labelled PR, while a faint trail of footprints, the agent's trajectory, leads up to the barrier. A small all-caps serif title reading BLOCK THE BAD AGENT IN CI sits in the upper left, and one thin ink-blue line runs along the bottom margin.

In May 2026, GitHub published a post whose title doubles as a status report: “Agent pull requests are everywhere.” It found that more than one in five code reviews on GitHub now involve an agent, and that Copilot code review alone has processed over 60 million reviews, growing tenfold in under a year (GitHub, 2026). The same post cites a January 2026 study, “More Code, Less Reuse,” which found that agent-generated code introduces more redundancy and more technical debt per change than human-written code.

The reflex is to make humans review harder, or to bolt on a unit-test gate. But the volume defeats human-only review, and a unit-test gate inspects only the final diff. A diff can be correct for the wrong reasons: the agent duplicated a module that already exists, took a path that read a secret, or recalled an answer it could not actually derive.

Here is the thesis. Final-output pass/fail is the wrong merge gate for agent-authored change. A single Pass@1 verdict creates what one 2026 paper calls a “high-score illusion” that ignores the quality, efficiency, and soundness of the reasoning process (TRACE, arXiv:2602.21230, 2026). You block a bad agent change by gating on the trajectory, the recorded steps, tool calls, and reasoning, and you do it with a flake budget so the gate is not itself a flaky test.

One anti-promise first, because it is the honest part most write-ups skip. This is not “gate on the exact step sequence.” Anthropic warns that checking for a specific ordered list of tool calls is too brittle; agents find valid paths their designers never anticipated. Gate on trajectory properties and invariants, not a rigid script. And it is not “delete human review”; it is “spend the human’s attention where the gate cannot reach.”

Key Takeaways

  • More than one in five code reviews on GitHub now involve an agent (GitHub, 2026), and agent code adds more tech debt per change. Human-only review does not scale to that volume.
  • A single Pass@1 verdict on the final diff is the wrong gate. It creates a “high-score illusion” that ignores the reasoning path (TRACE, arXiv:2602.21230, 2026). Gate on the trajectory instead.
  • Gate on trajectory properties, not an exact step sequence (Anthropic warns that is too brittle): forbidden-path/security invariants, a tool-call budget, required evidence, plus rubric-scored output quality.
  • An LLM eval is non-deterministic, so build a flake budget into the gate: run N, score the median, gate on pass^k not Pass@1, and route high-disagreement items to a human. In my own three-judge run the panel disagreed beyond its band on 31% of answers.

This post is the operational end of an eval arc. LLM-as-judge is the new flaky test diagnoses why the grader is unreliable; evaluate models yourself builds the private eval; backtesting AI agents replays it nightly. This one wires that eval into CI as a synchronous, per-PR merge block. Where it reuses first-person numbers, they come from the 40-question, three-judge run in the project graph.

Why is final-output pass/fail the wrong gate for agent PRs?

Because a final-output gate scores the diff, not how the agent got there, and for agent-authored change the how is exactly what recurs. TRACE puts it precisely: reliance on a singular metric like Pass@1 creates a “high-score illusion” that ignores the quality, efficiency, and soundness of the reasoning process (arXiv:2602.21230, submitted 2026-02-05). The output number can be high while the process that produced it is bad.

The practitioner evidence points the same way. The GitHub post cites the January 2026 “More Code, Less Reuse” study finding that agent-generated code introduces more redundancy and more technical debt per change than human-written code. A passing test suite does not catch “this duplicates a module that already exists” or “this took a path that read a secret.” Those failures do not live in the diff. They live in the trajectory.

And the volume is the forcing function. More than one in five code reviews on GitHub now involve an agent (GitHub, 2026). At that scale the automated gate is the only thing that scales with the flood, so the gate has to measure the right thing. If it measures the wrong thing, it scales the wrong thing.

Here is the reframe the rest of the post hangs on. An output-only gate is character testimony: the diff looks fine, so it must be fine. A trajectory gate is the security-camera footage: it shows what the agent actually did to produce that diff. The agent’s output is the alibi. The trajectory is the evidence. “Right diff, wrong reasoning” is one of the recurring failure surfaces I catalogued in autonomous agent failure modes, and it is invisible to a gate that only reads the result.

What exactly is a trajectory, and what should you gate on?

The trajectory is the complete record of the run, not just the answer. Anthropic defines it directly: “A transcript (also called a trace or trajectory) is the complete record of a trial, including outputs, tool calls, reasoning, intermediate results, and any other interactions” (Anthropic Engineering, 2026). You gate on properties of that record, not on a rigid ordered script.

State the tension head-on, because cherry-picking the trajectory definition while hiding the warning would be dishonest. The same Anthropic guide cautions: “There is a common instinct to check that agents followed very specific steps like a sequence of tool calls in the right order. We’ve found this approach too rigid… agents regularly find valid approaches that eval designers didn’t anticipate,” and adds that “it’s often better to grade what the agent produced, not the path it took.” So how do you “gate on the trajectory” without gating on the path? You gate on invariants and budgets, not on an exact step sequence. The agent can take any path it likes. It cannot cross these lines.

Four things are worth gating on, and they sort into two kinds.

Forbidden-path invariants (security). Did the trajectory interpolate an untrusted PR body, issue body, or commit message into a prompt without sanitizing it? Did it read or exfiltrate a secret? The GitHub post is explicit: sanitize and quote untrusted content before it touches a prompt, and require least-privilege workflow permissions (permissions: read-all is a reasonable default). These are deterministic checks over the trace, not LLM judgments. This is the lethal trifecta, untrusted input plus tools plus exfiltration, expressed as a gate predicate, and it pairs with the blast-radius limits in sandboxing coding agents.

Tool-call budget (efficiency). Did the run stay inside a turn and tool-call budget? Runaway loops and wasteful retries are plainly visible in the trace and completely invisible in the diff. A budget invariant is also a cost control, the same per-call discipline as in agent cost observability.

Required-evidence properties (behavioral). For a code change: did the agent cite the file:line it claims to modify? Did it actually run the test it claims passes? These are checkable facts about the trace, not opinions about the diff.

Output quality (the rubric axis). The diff itself, scored by an LLM judge against an anchored rubric: correct, non-duplicative, readable. This is the axis the companion posts build.

The split matters because it tells you where the flakiness lives. The first three are deterministic: code, free to run, and impossible to bribe with a confident tone. They are exactly the control-plane assertions built in claude-code-agent-evals, skill triggers, subagent spawns, hook firing, MCP reachability, reused here as gate predicates rather than re-explained. The fourth, output quality, runs an LLM judge, and an LLM judge is non-deterministic. Prefer the deterministic checks wherever they work, and reserve the expensive, flaky judge for the quality axis that genuinely needs it. Which brings us to the budget that keeps that judge from poisoning the gate.

Per-PR blocking vs nightly replay: where does this gate sit?

A CI merge gate is synchronous and per-PR; it blocks this change now. Backtesting and replay are asynchronous and corpus-wide; they catch regressions across a recorded history, nightly. You want both, and they answer different questions. One 2026 vendor mid-year report estimates that enterprise agentic systems carry roughly a 37% gap between lab benchmark scores and real-world deployment performance (Ampcome, 2026); treat the figure as directional, not precise, but the direction is the point. A synchronous pre-merge gate closes that gap before code lands, where a nightly replay closes it after.

The contrast with backtesting AI agents is exact and worth stating in-text. That post records real agent sessions and replays them under a new configuration to catch behavioral regressions, the case where a one-line CLAUDE.md edit silently changes behavior across the corpus. Replay asks: “did our config change break the recorded history?” This gate asks: “should this single PR merge?” Same eval machinery, opposite direction in time. Replay looks backward over many runs; the gate looks forward at one.

The timing imposes a hard design constraint. A per-PR gate sits inside the developer’s merge loop, so it has a latency budget. It cannot run the full nightly corpus on every push. It runs a targeted slice plus the cheap deterministic invariants, and gets out of the way. The nightly replay runs the full golden set where latency does not matter.

That gives you a clean handoff. The gate runs deterministic invariants and a small targeted eval slice synchronously, at merge. A regression the gate’s slice happens to miss, the nightly replay catches the next morning, and you promote that case into the gate slice so it blocks next time. The gate is the fast, narrow guard; replay is the slow, wide net; cases flow from the net into the guard. This sits one layer above the first-line automated review in AI PR review: the first line, which triages before the gate decides.

The flake budget: don’t let your gate become a flaky test

An LLM eval is non-deterministic, so a single Pass@1 verdict makes the gate a flaky test, and teams learn to re-run a flaky gate until it goes green. The fix is to design the flakiness into the gate: run N times, score the median, gate on pass^k for anything that must be reliable, and route high-disagreement items to a human instead of auto-blocking. The grader itself is a flaky test, that is the whole thesis of LLM-as-judge is the new flaky test, so any gate that uses an LLM judge inherits that flakiness and has to budget for it.

Four levers turn a noisy judge into a stable gate.

Run N, score the median. One run is a single noisy sensor. Median-of-N is robust to one outlier verdict. This is the same median-of-N machinery from the judge post, applied to a gating threshold rather than a diagnosis.

Gate on pass^k, not Pass@1. Anthropic distinguishes pass@k, “the likelihood that an agent gets at least one correct solution in k attempts,” from pass^k, “the probability that all k trials succeed,” and recommends “pass^k for agents where consistency is essential” (Anthropic Engineering, 2026). Pass@1, “at least one run passed,” is the loosest and most gameable bar. A gate that protects trunk wants the consistency bar: every run clears the threshold.

Threshold against the interval, not the point. A two-point gap inside overlapping confidence intervals is not a failure; it is noise wearing a number. Gate on the noise-aware band, not the headline decimal.

Route disagreement to a human. When the runs or judges spread beyond a band, the gate’s verdict is “needs human review,” not “blocked.” This is what keeps the gate from crying wolf, and crying wolf is what kills trust in a gate. (The training-time analog is process reward modeling, scoring steps rather than only outcomes; the eval-time twin is gating on the trajectory rather than only the result.)

My own three-judge run is a flake budget, and here is what it bought. In the project graph I scored a 40-question benchmark across two repositories with a cross-family panel, Haiku 4.5, Sonnet 4.6, and Opus 4.7, taking the median of three with a 2-point disagreement band. When I pulled the raw judge log (R005, 2026-05-28) and counted, the panel disagreed by more than its 2-point band on 31% of answers, 37 of 120 datapoints, and only 16 of 120, about one in eight, were unanimous. As a gate, that means: on roughly a third of items, a lone randomly-chosen judge could have landed three or more points off the panel median on a 10-point scale. A Pass@1 gate would have been flipping that coin on every merge.

The band earns its keep on individual items, which is what a gate actually decides. Take django-symbol-03, a question about Django’s get_user_model. The agent cited django/contrib/auth/__init__.py:183 correctly but stopped there, with no explanation of what the function calls or raises. Opus scored it 9, reading the rubric leniently. Haiku and Sonnet both scored it 4, reading the same rubric strictly. The median came out 4. Set a gate threshold of 7 and a lone Opus verdict would have auto-passed an answer two other judges called barely passing; the panel median blocked it, and the spread beyond the band would have routed it to a human anyway. That is the difference between a gate and a coin flip.

There is a part nobody puts in a methods section: the grader itself flaked. On 6 of 200 judge calls in that run, a judge returned an empty justification scored as a 0, a silent crash recorded as a verdict. Average those raw zeros in and one toolset’s judge mean reads 6.08; drop the crashes and it reads 6.30, a 0.22-point swing on a benchmark whose headline gap was 0.82. The gate’s own grader was, quite literally, a flaky test. A cross-family panel costs about 3x a single judge. Next to merging a privilege-escalating agent change into trunk, that is rounding error.

Wiring the merge block: the gate config

The gate is a required status check on pull requests that (1) runs deterministic trajectory invariants and fails fast and free if any is violated, (2) runs the targeted LLM-judge slice N times, (3) computes the median and the disagreement spread, and (4) blocks, passes, or routes to a human. The mechanism is vendor-agnostic: an eval CI action runs on every PR, posts a score summary as a check, and blocks the merge below a configured threshold. Here is the wiring.

# .github/workflows/agent-gate.yml
name: agent-merge-gate
on: pull_request

permissions: read-all          # least-privilege default (GitHub, 2026)

jobs:
  invariants:                  # deterministic, cheap, fail-fast, un-bribable
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Trajectory invariants
        run: bun run gate:invariants  # exits non-zero on any violation:
        #  - forbidden-path: untrusted PR/issue body reached a prompt unsanitized,
        #    or a secret was read/exfiltrated
        #  - tool-call budget exceeded
        #  - required-evidence missing (no file:line cited, claimed test not run)

  judge-slice:                 # expensive, flaky, runs only if invariants pass
    needs: invariants
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Sanitize untrusted input
        run: bun run gate:sanitize-pr   # quote/escape PR title+body before any prompt
      - name: Score the targeted slice
        run: bun run gate:judge          # writes verdict.json (see gate.ts below)
      - name: Publish check + route
        run: bun run gate:publish        # sets the check to failure | success | neutral,
        #  and applies a `needs-human-review` label when the verdict is "route"

The scoring step is where the flake budget lives. The deterministic invariants already passed; this is the output-quality axis, run N times and gated on pass^k:

// gate.ts: runs only after the deterministic invariants job passes
const N = 5;            // re-runs, to absorb non-determinism
const THRESHOLD = 7.0;  // anchored rubric score, 0-10 scale
const BAND = 2;         // disagreement tolerance (max - min across runs)

// one run = score the diff with a 3-judge cross-family panel, take the median
const runs = await Promise.all(
  Array.from({ length: N }, () => panelMedian(prDiff, trajectory)),
);

const median = medianOf(runs);
const spread = Math.max(...runs) - Math.min(...runs);
const passCaretK = runs.every((s) => s >= THRESHOLD);   // pass^k: ALL runs clear

const verdict =
  spread > BAND ? "route"          // judges/runs disagree -> human, don't auto-block
  : passCaretK  ? "pass"           // consistent and above threshold
  :               "block";         // a run dipped below -> merge denied

Two things make this honest. The invariants run first and fail fast, so the cheap deterministic checks reject the obvious cases for free before the expensive N-run judge slice ever starts. And the judge slice is a targeted slice, not the full corpus; the full golden set is the nightly replay’s job, not the merge loop’s.

The actual block is not in the YAML. It is the branch-protection setting that makes agent-merge-gate a required status check. Without that, the gate is advisory and the merge button ignores it. So roll it out advisory-then-enforced: ship it as neutral (non-blocking) for two weeks to calibrate the threshold and band against real PRs, then flip it to required. Flipping a miscalibrated gate to required on day one is how you train the team to hate it. My Pylon PR review runs multi-agent fan-out, several specialist reviewers in parallel; the gate is the aggregation layer on top. Each specialist contributes a trajectory assertion or a rubric score, and the gate combines them into one required check with a flake budget. The hook that records the trajectory the gate reads is the substrate from Claude Code hooks.

Match the gate to the stakes, and don’t over-block

Not every change needs the full gate. Match the rigor to the blast radius of the merge. A multi-run, multi-judge gate costs roughly N runs times about 3x for the panel of a single Pass@1 check, so reserve the full trajectory-plus-flake-budget gate for the merges that can actually hurt you.

Run the full gate, trajectory invariants plus N-run median plus pass^k plus a required check, when the PR is agent-authored and merges to trunk or a release branch, touches security-sensitive paths, or feeds code the next agent will read and build on. The gate decides what enters the trunk the next agent inherits, so a weak gate compounds: agent debt accumulates, and the next agent learns from it.

Run a light gate, deterministic invariants only with an advisory score, when it is a draft, a docs-only change, or a low-risk human-authored change. The cheap invariants still run, because they are free; the expensive N-run judge slice does not block.

The rule is one line: the cost of the gate should be a fraction of the cost of the bad merge it prevents. An N-run, three-judge gate to keep a privilege-escalating change out of trunk is trivially worth it. The same gate on every docs typo is waste that trains the team to bypass it. A gate that blocks too often gets disabled, and a disabled gate protects nothing. As I argued in engineering outlasts the paradigm, the durable skill is gate discipline, the right unit, the right threshold, the right flake budget, not the specific CI vendor you wire it through. A gate is a change-quality control, not a throughput tax (DORA in the AI era).

Frequently asked questions

How do you block a bad AI agent change in CI?

Make an eval a required status check on pull requests. Run deterministic trajectory invariants first, forbidden-path/security, tool-call budget, required-evidence, and fail fast and free if any is violated. Then run a targeted LLM-judge slice N times, take the median, and gate on pass^k against a threshold. Block below it, pass above it, and route high-disagreement items to a human instead of auto-blocking. Wire it to branch protection so the check is required, not advisory.

Should you gate on the agent’s output or its trajectory?

Both, but the trajectory is what output-only gating misses. A diff can be correct for the wrong reasons: duplicating a module, reading a secret, or recalling an answer. TRACE shows a single output metric like Pass@1 creates a “high-score illusion” that ignores reasoning quality (arXiv:2602.21230, 2026). Gate on trajectory properties and invariants, no forbidden path, within tool-call budget, not on a rigid step sequence; Anthropic warns exact-step checks are too brittle.

What is a flake budget for an eval gate?

It is the design that keeps a non-deterministic eval from becoming a flaky CI check. Instead of one Pass@1 verdict, you run the eval N times, score the median, and gate on pass^k (“all k runs pass”) for anything that must be reliable. You set the threshold against confidence intervals, not the headline number, and route high-disagreement items to human review. Without it, teams re-run a flaky gate until green and stop trusting it.

Is Pass@1 a reliable merge gate?

No. Pass@1 means “at least one run passed,” the loosest and most gameable bar, and a single run of a non-deterministic eval is a flaky test. For a merge gate, gate on pass^k (“all k trials succeed”) where consistency matters, and on the median of N runs to absorb one outlier verdict (Anthropic Engineering, 2026). Pass@k is fine where one success is enough; a gate that protects trunk is not that case.

Conclusion

The agent’s output is the alibi; the trajectory is the evidence. As agent PRs flood repos, more than one in five GitHub reviews now involve an agent, final-output pass/fail is the wrong gate, and a single Pass@1 verdict is a flaky check wearing a green hat. The policy in one breath: gate trajectory invariants (security, budget, evidence) deterministically; gate output quality with an LLM judge run N times, median-scored, pass^k against a noise-aware threshold; route disagreement to a human; make it a required status check; roll it out advisory-then-enforced.

Keep two honest limits in view. Do not gate on an exact step sequence, Anthropic is right that it is too brittle, so gate on properties the agent must respect on any valid path. And do not over-block, because a gate that cries wolf gets disabled. I run a three-judge median with a disagreement band as my flake budget and still route spread to a human, because I have watched a lone Pass@1 verdict confidently wave through a 9 the panel scored a 4. Wire the gate, then distrust it enough to keep the human in the loop where it disagrees.

The grader inside the gate is itself a flaky test (LLM-as-judge is the new flaky test); build the eval the gate runs (evaluate models yourself); build the suite it draws on (claude-code-agent-evals); and replay it nightly to catch what the gate’s slice missed (backtesting AI agents).

Share this post

If it was useful, pass it along.

What the link looks like when shared.
X LinkedIn Bluesky

Search posts, projects, resume, and site pages.

Jump to

  1. Home Engineering notes from the agent era
  2. Resume Work history, skills, and contact
  3. Projects Selected work and experiments
  4. About Who I am and how I work
  5. Contact Email, LinkedIn, and GitHub