Index
Feed
Written by amara.dev · May 23, 2026
Green checks mean nothing if the agent weakened the tests to get them. This gate compares test counts and coverage against the base branch, flags deleted assertions, and refuses to merge on silence-by-subtraction.
Branch prefix is the low-friction option — your worktree/branch discipline from other recipes already provides it. The gate triggers on agent/*, codex/*, claude/* branches.
# .github/workflows/agent-gate.yml (trigger excerpt)
on:
pull_request:
branches: [main]
jobs:
agent-gate:
if: startsWith(github.head_ref, 'agent/') || startsWith(github.head_ref, 'codex/') || startsWith(github.head_ref, 'claude/')
runs-on: ubuntu-latestThe cheapest strong signal: total tests must not decrease. Run the suite on both refs with JSON output and diff the counts. Fewer tests than main is an automatic fail unless a human overrides.
git fetch origin main
git checkout origin/main -- .
npx vitest run --reporter=json --outputFile=/tmp/base.json || true
git checkout HEAD -- .
npx vitest run --reporter=json --outputFile=/tmp/head.json || true
base=$(jq '.numTotalTests' /tmp/base.json)
head=$(jq '.numTotalTests' /tmp/head.json)
echo "tests: base=$base head=$head"
if [ "$head" -lt "$base" ]; then
echo '::error::Test count decreased on an agent branch.'
exit 1
fi! Common failures
Did this recipe work for you?
Sign in to add your report — every count here is backed by a named account.
Absolute thresholds rot; deltas don't. Allow 0.3% slack for line-count noise, fail beyond it. jq does the arithmetic so you don't need a coverage SaaS.
base_cov=$(jq '.total.lines.pct' /tmp/base-coverage.json)
head_cov=$(jq '.total.lines.pct' /tmp/head-coverage.json)
drop=$(echo "$base_cov - $head_cov" | bc)
echo "coverage: base=$base_cov head=$head_cov drop=$drop"
if [ "$(echo "$drop > 0.3" | bc)" -eq 1 ]; then
echo "::error::Coverage dropped ${drop}% on an agent branch."
exit 1
fiCount assertion lines removed from test files. Removals are normal in refactors, so this one warns rather than fails — but the number goes in the PR comment where a reviewer will see '47 assertions deleted'.
deleted=$(git diff origin/main...HEAD -- '**/*.test.*' '**/*.spec.*' \
| grep -cE '^-.*(expect\(|assert|\.toBe|\.toEqual)' || true)
echo "assertions_deleted=$deleted"
if [ "$deleted" -gt 0 ]; then
echo "::warning::$deleted assertion lines deleted in test files — review closely."
fiA non-required check is a suggestion. Add agent-gate to branch protection, then verify from the CLI that it participates in merges.
gh api repos/:owner/:repo/branches/main/protection/required_status_checks/contexts -X POST -f 'contexts[]=agent-gate'
gh pr checks 128 --watchClose the loop the same way as review workflows: the gate's output is a machine-readable task. 'Restore the deleted tests in X or explain why each is obsolete' resolves most failures without human attention.
gh run view --log-failed | tail -50 > gate-failure.txt
claude -p "The CI agent-gate failed with the following output. Fix the branch so the gate passes WITHOUT weakening tests. Output follows." < gate-failure.txtRelated records
Strong evidence gets promoted into the record above.
Moderator note: several teams asked whether to run this on human PRs too. We do — it just fires so rarely that nobody notices. The agent-branch trigger is about CI cost, not about humans deserving less scrutiny.
Sign in to join the discussion, vote, and verify fixes.
I tested the gate against my archive of 40 known-bad agent PRs (collected over six months). It flags 31 of them: all 9 test deletions, all 14 coverage drops, 8 of 17 weakenings. The escapes are assertion-free tests, which matches the mutation-testing caveat exactly.