A couple of years ago, "AI pair programming" was a novelty. Today it's the default. In most teams I talk to, a large share of new pull requests include code that was drafted, refactored, or tested by an AI assistant — and increasingly by autonomous agents that open PRs on their own.
That shift has quietly changed what "software quality" means. The bottleneck is no longer writing code. It's verifying it. When an agent can produce 800 lines in thirty seconds, the real question becomes: how do we know any of it is correct, secure, and maintainable?
I've spent the last couple of years helping teams adopt AI coding tools, and the pattern is remarkably consistent. The first few months feel magical — velocity goes up, backlogs shrink. Then the bill arrives: flaky tests nobody understands, duplicated helpers scattered across services, a dependency that turned out not to exist, and a security finding in code that "looked fine" during review.
This article is a practical guide to avoiding that bill. We'll cover how to build a continuous quality engineering practice designed specifically for AI-native workflows: the concepts, the pipeline, real code you can adapt, and the mistakes I've seen teams make along the way.

Why AI-Generated Code Needs Its Own Governance Model
Traditional quality processes were built around a simple assumption: a human wrote the code and understands it. Code review was a conversation between two people who could both explain the intent. Tests were written by someone who knew the edge cases they were worried about.
AI-generated code breaks that assumption in several subtle ways.
1. Plausibility is not correctness
Language models are optimized to produce code that looks right. That's exactly what makes their mistakes dangerous. An off-by-one error in a pagination function, a missing authorization check on one endpoint, a timezone bug in a date parser — these hide inside otherwise clean, idiomatic code. Reviewers pattern-match on style, see something familiar, and approve.
2. Volume overwhelms human review
When output volume grows several times over but reviewer headcount doesn't, something gives. Usually it's review depth. Research from Stanford (Perry et al.) found that developers using AI assistants were more likely to write insecure code and more likely to believe their code was secure. That confidence gap is the core governance problem.
3. New failure modes appear
AI introduces defect categories that barely existed before:
- Dependency hallucinations — the model suggests importing a package that doesn't exist. Attackers register those names ("slopsquatting") and wait.
- Context blindness — the model doesn't know your internal
retryWithBackoffhelper exists, so it writes a fourth one. - Test tautologies — AI-written tests that assert whatever the implementation currently returns, locking in bugs instead of catching them.
- Prompt-injected behavior — agents that read issues, docs, or web pages can be manipulated into writing code that wasn't requested.
- License and provenance ambiguity — snippets that closely mirror code with incompatible licenses.
4. Technical debt accumulates faster
Industry analyses of large codebases (GitClear's code quality research is a good example) have reported rising code churn and copy-paste duplication as AI assistance became widespread. Debt that used to accumulate over years can now pile up in a quarter.
The conclusion is straightforward: AI-generated code should be treated as untrusted input. Not bad — untrusted. The same way you'd treat a pull request from a talented contributor you've never met.
What Is Continuous Quality Engineering?
Continuous quality engineering (CQE) is the practice of embedding quality verification into every stage of the delivery lifecycle rather than bolting QA onto the end. In an AI-native workflow, that lifecycle starts earlier than most teams think — at the prompt — and ends later — in production telemetry.
Here's the model I recommend:
flowchart LR
A[Intent & Prompt] --> B[AI Generation]
B --> C[Local Guardrails<br/>IDE linting, pre-commit]
C --> D[Pull Request<br/>Provenance tagged]
D --> E{Risk Classifier}
E -->|Low risk| F[Automated Gates]
E -->|High risk| G[Automated Gates +<br/>Expert Human Review]
F --> H[Merge]
G --> H
H --> I[Progressive Delivery<br/>Feature flags, canaries]
I --> J[Production Observability]
J -->|Defect & drift signals| K[Feedback Loop]
K --> A
The key ideas:
- Shift left: catch problems at generation time and in pre-commit, where fixes cost seconds.
- Risk-based depth: not every change deserves the same scrutiny. Spend human attention where blast radius is largest.
- Shift right: some defects only appear under real traffic. Progressive delivery and observability are part of quality, not separate from it.
- Close the loop: production defects feed back into prompts, rules, and policies so the same mistake doesn't happen twice.
The Five Pillars of AI Code Governance
Let's break governance into five pillars you can implement incrementally.
Pillar 1: Provenance — Know What Was AI-Assisted
You can't govern what you can't see. The first step is recording which changes involved AI assistance and to what degree.
The lightest-weight approach is a commit trailer convention. Many AI coding tools already add trailers like Co-Authored-By; you can standardize on a team convention such as Assisted-by:
feat(billing): add proration for mid-cycle plan upgrades
Implements prorated credit calculation when a customer upgrades
between billing cycles.
Assisted-by: claude-code
AI-Scope: implementation, tests
Human-Reviewed: trueThen enforce it with a lightweight CI check. Here's a Python script that flags PRs touching sensitive paths with AI-assisted commits so they can be routed to stricter review:
#!/usr/bin/env python3
"""classify_pr_risk.py — tag PRs by AI provenance and path sensitivity."""
import json
import re
import subprocess
import sys
SENSITIVE_PATHS = [
r"^src/auth/",
r"^src/payments/",
r"^infra/",
r"^migrations/",
r"\.github/workflows/",
]
AI_TRAILERS = re.compile(r"^(Assisted-by|Co-Authored-By):.*(claude|copilot|agent|ai)", re.I | re.M)
def git(*args: str) -> str:
return subprocess.check_output(["git", *args], text=True)
def main(base: str = "origin/main") -> None:
commits = git("log", f"{base}..HEAD", "--format=%B%x00").split("\x00")
ai_assisted = any(AI_TRAILERS.search(c) for c in commits if c.strip())
changed = git("diff", "--name-only", f"{base}...HEAD").splitlines()
sensitive = [f for f in changed if any(re.search(p, f) for p in SENSITIVE_PATHS)]
lines_changed = sum(
int(a) + int(d)
for a, d, _ in (l.split("\t") for l in git("diff", "--numstat", f"{base}...HEAD").splitlines())
if a.isdigit() and d.isdigit()
)
if sensitive and ai_assisted:
risk = "high"
elif sensitive or lines_changed > 400:
risk = "medium"
else:
risk = "low"
result = {
"ai_assisted": ai_assisted,
"sensitive_files": sensitive,
"lines_changed": lines_changed,
"risk": risk,
}
print(json.dumps(result, indent=2))
with open("pr-risk.json", "w") as fh:
json.dump(result, fh)
if __name__ == "__main__":
main(*sys.argv[1:])This isn't about blame. It's about routing attention and measuring outcomes. Six months in, you'll want to answer questions like "Do AI-assisted changes in our payments service have a higher rollback rate?" — and you can't without provenance data.
Pillar 2: Guardrails at Generation Time
The cheapest defect is the one never written. Modern AI coding tools read project-level instruction files (for example CLAUDE.md, AGENTS.md, or similar rule files depending on your tooling). Use them as living engineering standards:
# Engineering Rules for AI Assistants
## Architecture
- Use `src/lib/http/retry.ts` for retries. Never write a new retry helper.
- All DB access goes through repositories in `src/data/`. No raw SQL in handlers.
## Security
- Every new HTTP handler MUST call `requirePermission()` before business logic.
- Never log request bodies, tokens, or PII.
- Do not add new dependencies without listing them in the PR description.
## Testing
- Tests must assert on behavior from the spec, not on current output.
- Include at least one negative test (invalid input, unauthorized user).
- Do not mock the module under test.A few things make these files effective:
- Be specific. "Write secure code" does nothing. "Call
requirePermission()first" does. - Point to existing utilities. This directly attacks the duplication problem.
- Version and review them like code. When a production incident traces back to an AI pattern, add a rule.
Pair instruction files with fast local checks — pre-commit hooks running formatters, linters, type checking, and secret scanning — so the assistant (and the human) get feedback before anything leaves the laptop.
Pillar 3: Automated Quality Gates in CI/CD
This is where governance becomes enforceable. A robust 2026 pipeline for AI-heavy teams typically includes:
| Gate | What it catches | Example tools |
|---|---|---|
| Static analysis (SAST) | Injection, unsafe APIs, insecure defaults | Semgrep, CodeQL, SonarQube |
| Secret scanning | Hardcoded keys and tokens | Gitleaks, TruffleHog |
| Dependency verification | Hallucinated, typosquatted, vulnerable packages | OSV-Scanner, Socket, Dependabot |
| Duplication and complexity | Copy-paste debt, god functions | SonarQube, jscpd |
| Mutation testing | Weak or tautological tests | Stryker, PIT, mutmut |
| SBOM and signing | Supply chain integrity | Syft, Sigstore/cosign |
| Policy-as-code | Org rules on risk and approvals | OPA/Conftest |
Here's a GitHub Actions workflow that wires these together with risk-based behavior:
name: ai-quality-gates
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
security-events: write
jobs:
classify:
runs-on: ubuntu-latest
outputs:
risk: ${{ steps.risk.outputs.risk }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: risk
run: |
python3 scripts/classify_pr_risk.py origin/${{ github.base_ref }}
echo "risk=$(jq -r .risk pr-risk.json)" >> "$GITHUB_OUTPUT"
static-and-secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Semgrep (custom + community rules)
run: |
pip install semgrep
semgrep scan --config p/default --config .semgrep/ --error
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify new dependencies exist and are trustworthy
run: python3 scripts/verify_new_deps.py origin/${{ github.base_ref }}
- name: OSV vulnerability scan
uses: google/osv-scanner-action/osv-scanner-action@v2
with:
scan-args: --recursive ./
tests:
needs: classify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --coverage
- name: Mutation testing on high-risk changes
if: needs.classify.outputs.risk == 'high'
run: npx stryker run --incremental
require-expert-review:
needs: classify
if: needs.classify.outputs.risk == 'high'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['ai-assisted', 'needs-security-review']
});Notice that mutation testing only runs on high-risk changes. That's deliberate — mutation testing is expensive, and risk-based gating keeps the pipeline fast for the 80% of changes that don't need it.
Catching hallucinated dependencies
This one deserves its own script, because it's one of the most practical AI-specific defenses you can add:
#!/usr/bin/env python3
"""verify_new_deps.py — block nonexistent or suspiciously new npm packages."""
import json
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone
MIN_AGE_DAYS = 30
MIN_WEEKLY_DOWNLOADS = 500
def added_packages(base: str) -> set[str]:
old = json.loads(subprocess.check_output(["git", "show", f"{base}:package.json"], text=True))
new = json.load(open("package.json"))
keys = ("dependencies", "devDependencies")
before = {p for k in keys for p in old.get(k, {})}
after = {p for k in keys for p in new.get(k, {})}
return after - before
def fetch(url: str):
try:
with urllib.request.urlopen(url, timeout=10) as r:
return json.load(r)
except Exception:
return None
def main(base: str) -> int:
failures = []
for pkg in sorted(added_packages(base)):
meta = fetch(f"https://registry.npmjs.org/{pkg}")
if meta is None:
failures.append(f"{pkg}: does not exist on the registry (possible hallucination)")
continue
created = datetime.fromisoformat(meta["time"]["created"].replace("Z", "+00:00"))
age = (datetime.now(timezone.utc) - created).days
if age < MIN_AGE_DAYS:
failures.append(f"{pkg}: published {age} days ago (below {MIN_AGE_DAYS}-day threshold)")
stats = fetch(f"https://api.npmjs.org/downloads/point/last-week/{pkg}") or {}
if stats.get("downloads", 0) < MIN_WEEKLY_DOWNLOADS:
failures.append(f"{pkg}: low adoption ({stats.get('downloads', 0)} weekly downloads)")
for f in failures:
print(f"::error::{f}")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))In larger organizations, the better long-term answer is an internal curated registry (Artifactory, Nexus, or similar) where new packages require approval. But this script is a solid starting point that takes an afternoon to deploy.
Encoding your own rules with Semgrep
Generic SAST rules won't know your conventions. Custom rules turn the instruction file from Pillar 2 into something enforceable:
rules:
- id: handler-missing-permission-check
languages: [typescript]
severity: ERROR
message: >
HTTP handlers must call requirePermission() before business logic.
AI assistants frequently omit authorization on new endpoints.
patterns:
- pattern: |
router.$METHOD($PATH, async ($REQ, $RES) => { ... })
- pattern-not: |
router.$METHOD($PATH, async ($REQ, $RES) => {
...
await requirePermission(...);
...
})
metadata:
category: security
ai-governance: trueThis is one of my favorite patterns: every guardrail in your instruction file should eventually have a matching automated check. Instructions guide the model; checks verify the result.
Pillar 4: Human Review That Actually Works
Automation handles the predictable. Humans handle intent, design, and the "wait, why are we doing this?" questions. But human review of AI code needs to change shape.
Review the intent first, then the code. Before reading the diff, ask: does the PR description clearly state what problem is being solved and why this approach? If the author (human or agent) can't explain it, the diff doesn't matter yet.
Use a focused checklist for AI-assisted PRs:
- Does this duplicate something that already exists in the codebase?
- Are all new dependencies justified and verified?
- Do tests assert on specified behavior, including failure paths?
- Is authorization enforced on every new entry point?
- Are error paths handled, or just the happy path?
- Would I be comfortable being paged at 3 a.m. for this code?
Keep PRs small. AI makes it trivially easy to generate enormous PRs. Set a soft limit (for example, around 400 changed lines) and ask agents to split work into reviewable increments. Large AI PRs are where review fatigue does the most damage.
Use AI reviewers — as a first pass, not a final word. AI code review tools are genuinely useful for catching obvious issues and summarizing changes. But an AI reviewing AI output shares many of the same blind spots. Treat it as triage that frees humans to focus on what matters.

Pillar 5: Testing Strategies That Resist Tautology
The most insidious AI failure mode I've seen is the self-confirming test suite. You ask an assistant to implement a function and write tests. It writes the implementation, then writes tests that check whatever the implementation does. Coverage is 95%. The bug is locked in.
Defenses that work:
Write tests from the spec, not the implementation. Have the assistant generate tests before the implementation, or in a separate session given only the requirements.
Use property-based testing. Properties describe invariants that must always hold, which is much harder to fake. Here's an example with Python's Hypothesis for the billing proration code from earlier:
from decimal import Decimal
from hypothesis import given, strategies as st
from billing.proration import prorated_credit
money = st.decimals(min_value=Decimal("0"), max_value=Decimal("10000"), places=2)
days = st.integers(min_value=1, max_value=31)
@given(price=money, cycle_days=days, data=st.data())
def test_credit_never_exceeds_price(price, cycle_days, data):
days_used = data.draw(st.integers(min_value=0, max_value=cycle_days))
credit = prorated_credit(price, days_used, cycle_days)
assert Decimal("0") <= credit <= price
@given(price=money, cycle_days=days)
def test_unused_cycle_gives_full_credit(price, cycle_days):
assert prorated_credit(price, 0, cycle_days) == price
@given(price=money, cycle_days=days)
def test_fully_used_cycle_gives_no_credit(price, cycle_days):
assert prorated_credit(price, cycle_days, cycle_days) == Decimal("0")Measure test strength with mutation testing. Mutation testing introduces small bugs into your code and checks whether tests catch them. A suite with high coverage but a low mutation score is a red flag — often a sign of tautological AI-generated tests.
Contract tests at service boundaries. When agents modify APIs, consumer-driven contract tests (Pact or similar) catch breaking changes that unit tests never will.
Real-World Example: A Fintech Team's Turnaround
Let me walk through a composite scenario based on patterns I've seen across several teams.
A mid-sized fintech platform team adopted AI agents aggressively. Within two months, their PR throughput had roughly doubled. Then they hit three problems in a single quarter:
- An agent-written endpoint for exporting transaction history shipped without an ownership check. Any authenticated user could export any account's history by changing an ID. It was caught by a bug bounty researcher, not internally.
- A refactor PR introduced a dependency with a name nearly identical to a popular date library. It had been published two weeks earlier.
- Their incident rate went up, and postmortems kept finding "tests passed but didn't test the right thing."
Here's what they changed:
- Provenance trailers became mandatory, enforced by a commit-msg hook.
- They added a Semgrep rule requiring ownership checks on every handler taking an account ID.
- New dependency verification went into CI, plus an internal registry for production services.
- Mutation testing ran on high-risk paths; the initial mutation score on payments code was eye-opening and drove a test rewrite.
- PRs over 400 lines from agents were automatically flagged for splitting.
- Every incident postmortem now ends with the question: "What rule or check would have prevented this?" — and that rule gets added.
The result after two quarters: throughput stayed high, change failure rate dropped back below pre-AI levels, and reviewers reported less fatigue because they were reviewing smaller, pre-filtered PRs. Governance didn't slow them down; it made the speed sustainable.
Measuring What Matters
You can't improve governance without metrics. Track these, segmented by AI-assisted vs. not:
- Change failure rate and mean time to restore (the DORA metrics remain the gold standard).
- Rework rate — how often code is substantially modified within a few weeks of merging.
- Duplication percentage over time.
- Mutation score for critical modules.
- Security findings per thousand changed lines, by severity.
- Review latency and review depth (comments per PR, time spent).
flowchart TD
A[Provenance Data] --> D[Quality Dashboard]
B[CI Gate Results] --> D
C[Incident & Rollback Data] --> D
D --> E{Trend worsening?}
E -->|Yes| F[Update rules, prompts,<br/>and policy thresholds]
E -->|No| G[Relax gates for<br/>proven low-risk areas]
F --> H[Next iteration]
G --> H
Notice the feedback loop goes both ways. If data shows AI-assisted changes in a well-tested internal tool rarely cause issues, loosen the gates there. Governance should be evidence-driven, not fear-driven.

Policy-as-Code: Making Governance Auditable
As regulations like the EU AI Act phase in and frameworks like the NIST Secure Software Development Framework become procurement requirements, "we have a process" is no longer enough. You need to prove the process ran.
Policy-as-code with Open Policy Agent lets you express merge rules declaratively and keep an audit trail:
package pr.governance
import rego.v1
default allow := false
allow if {
input.risk == "low"
input.checks.sast == "pass"
input.checks.secrets == "pass"
count(input.approvals) >= 1
}
allow if {
input.risk in {"medium", "high"}
input.checks.sast == "pass"
input.checks.secrets == "pass"
input.checks.dependencies == "pass"
count(input.approvals) >= 2
some a in input.approvals
a.team == "security-champions"
}
deny contains msg if {
input.ai_assisted
not input.description_has_intent
msg := "AI-assisted PRs must describe intent and approach in the PR description."
}Store policy decisions alongside build artifacts and SBOMs. When an auditor (or your own incident review) asks, "How was this change verified?", you have an answer with evidence.
Common Mistakes to Avoid
I've seen each of these more than once:
- Banning AI tools outright. Developers will use them anyway, just without guardrails or provenance. Shadow AI is worse than governed AI.
- Treating all changes equally. Uniform heavy gates make the pipeline slow, and teams route around it. Uniform light gates miss the dangerous stuff. Use risk-based tiers.
- Trusting coverage numbers. AI makes high coverage cheap. Coverage measures what code ran, not what was verified.
- Letting AI review AI unsupervised. Useful as a first pass, dangerous as the only pass on critical code.
- Writing vague instruction files. "Follow best practices" is noise. Concrete, path-specific rules work.
- Ignoring agent permissions. Autonomous agents with broad repository write access, production credentials, or unrestricted network access are a supply chain risk. Apply least privilege to agents exactly as you would to service accounts.
- Accepting giant PRs. Review quality falls off a cliff past a few hundred lines.
- Never closing the loop. If incidents don't produce new rules and checks, you'll keep paying for the same mistake.
🚀 Pro Tips
- Pair every written rule with an automated check. Instruction files steer generation; Semgrep, linters, and policies verify outcomes.
- Run a "fresh context" test pass. Ask a separate AI session, given only the spec, to write tests for the feature. Disagreements with the original tests often reveal real bugs.
- Tag agent identities distinctly. Give each autonomous agent its own bot account and token scope. It makes provenance, auditing, and revocation trivial.
- Use feature flags for AI-heavy features. Progressive rollout turns a potential incident into a quiet flag flip.
- Maintain a "known AI pitfalls" doc for your codebase — the specific mistakes assistants keep making in your domain — and feed it into your instruction files.
- Budget for refactoring. Reserve a fixed slice of each sprint for consolidating duplicated AI-generated helpers before they spread.
- Review the prompt, not just the diff, for complex agent-generated changes. Linking the task description or agent session to the PR gives reviewers crucial context.
- Verify artifacts, not just source. Sign builds with Sigstore and aim for SLSA provenance so you know what actually shipped matches what was reviewed.
Best Practices Checklist
Use this as a starting roadmap. You don't need everything on day one.
Week 1–2: Foundations
- Adopt a commit trailer convention for AI assistance.
- Create or tighten project instruction files with concrete rules.
- Enable secret scanning and basic SAST on every PR.
Month 1: Gates
- Add new-dependency verification to CI.
- Write three to five custom Semgrep rules for your most important conventions.
- Introduce risk classification and route high-risk PRs to expert reviewers.
Quarter 1: Depth
- Add mutation testing for critical modules.
- Introduce property-based and contract tests at key boundaries.
- Build a quality dashboard segmented by provenance.
Ongoing: Maturity
- Express merge rules as policy-as-code with audit trails.
- Generate SBOMs and sign artifacts.
- Review metrics quarterly and adjust gates in both directions.
📌 Key Takeaways
- AI-generated code is untrusted input. Verify it with the same rigor you'd apply to an unknown contributor — no more paranoid, no less careful.
- Verification is the new bottleneck. Teams that invest in fast, automated, risk-based quality gates keep their AI speed gains; teams that don't give them back in incidents and rework.
- Provenance is foundational. Knowing which code was AI-assisted enables risk routing, audits, and honest measurement.
- Watch for AI-specific defects: hallucinated dependencies, duplicated utilities, missing authorization, and tautological tests.
- Governance must be evidence-driven. Measure outcomes, tighten where data shows risk, and relax where it shows safety.
- Close the loop. Every incident should produce a rule, a check, or a policy that prevents it from recurring.
Conclusion
AI co-developers aren't going away, and honestly, I wouldn't want them to. They've removed a huge amount of tedium from software engineering. But they've also shifted where engineering excellence lives. In 2026, great teams aren't distinguished by how fast they can generate code — everyone can do that now. They're distinguished by how confidently they can verify, govern, and evolve that code over time.
Continuous quality engineering is how you get there. Start small: tag your AI-assisted commits, write concrete rules, add a dependency check, and make every incident teach your pipeline something new. Within a quarter, you'll have something better than a policy document — you'll have a system that makes the safe path the easy path.
The goal isn't to slow AI down. It's to make sure that what ships fast also ships right.
References
- OWASP Top 10 for Large Language Model Applications
- NIST Secure Software Development Framework (SP 800-218)
- NIST AI Risk Management Framework
- Perry et al., "Do Users Write More Insecure Code with AI Assistants?" (Stanford)
- GitClear — Coding on Copilot: AI Code Quality Research
- DORA — State of DevOps Research
- SLSA — Supply-chain Levels for Software Artifacts
- Sigstore — Software Signing for Everybody
- Open Policy Agent Documentation
- Semgrep Rule Writing Documentation
- Stryker Mutator
- Hypothesis — Property-Based Testing for Python
- OpenSSF Scorecard
- EU Artificial Intelligence Act — Overview