Who Signs Off When Agents Write the Code? Risk-Tiered Approval in an AI-First SDLC

13 min read
AI-First SDLCSpec-Driven DevelopmentAI AgentsCode ReviewSoftware Engineering

Picture a team that goes agent-first. Every developer pairs with Claude Code or Codex, every ticket gets a spec before a line is written, and every spec waits for the tech lead's signature. The first thing that breaks isn't the code. It's the lead. Specs an agent drafts in minutes sit in a queue for hours, and most of what gets signed is boring: a new field on a form, a copy change, a test for an edge case nobody had covered.

This post is about the AI-first SDLC approval workflow I moved to after that: approval tiers that are computed, not assumed. Each spec gets graded against explicit risk triggers. Low-risk work can be signed by a reviewer agent with a written rationale, and anything sensitive goes to a person, with the reasons attached. The tier gets re-checked against the actual diff at PR time, the merge closes the spec, and QA never blocks a developer. It's a workflow and a set of decision rules, not a tool. You can build it with whatever your team already uses.

The Sign-Off Queue in an Agentic Team

In a classic team, a tech lead reviewing every design doc is fine because design docs are rare. A developer might produce one a week. The review is a small tax on a slow process.

Agents change the ratio. When a developer can take a ticket from spec to green PR in an afternoon, they produce several specs a week, sometimes several a day. Multiply that by the team and a single human approver becomes a queue in front of everyone. You get one of two failure modes:

  • The rubber stamp. The lead skims and approves because the queue is long. The signature stops meaning anything, but everybody still waits for it.
  • The bottleneck. The lead reads carefully, and delivery speed collapses to the lead's reading speed. Developers start batching work or skipping the spec to avoid the wait, which defeats the point of spec-driven development.

Neither is a people problem. It's a routing problem. Not every spec carries the same risk, so not every spec needs the same reviewer. The fix is to decide which specs need a human, and to make that decision mechanical so nobody has to argue about it.

Tickets Stay High Level, Specs Live in the Repo

Before grading anything, you need to be clear about where things live. The split I settled on:

  • The ticket (in whatever tracker you use) stays short and high level: who wants what and why, what's in and out of scope, acceptance criteria a person can check, dependencies, open questions. No file paths, no contracts, no implementation detail. Product people own it and should be able to read it in a minute.
  • The spec lives in the repository, next to the code, one per ticket. It carries everything the agent needs: affected modules, contracts, data changes, test plan, the architecture decisions it relies on. It's versioned, reviewable in a PR, and readable by any agent session that opens the repo.

And one rule that makes everything downstream simpler: one ticket, one branch, one PR. When a spec maps to exactly one PR, "is this ticket done?" has a single, checkable answer. When it maps to three PRs across a week, nobody knows, and the status board turns into fiction.

Groupings of tickets don't need their own spec: a spec is for a unit of work an agent can execute. If a group of tickets needs a cross-cutting decision, that's an architecture decision record, which I wrote about in ADRs and session handoffs.

Here's the shape of a spec's front matter, written from scratch to illustrate the idea:

status: ready              # draft | ready | signed | merged | accepted
approval:
  tier: agent              # agent | human
  reasons: []              # filled by the grader, e.g. ["touches-schema"]
  signed_by: reviewer-agent
  rationale: "UI-only change, no contract or data impact, critic found no gaps."
prs: []                    # filled when the PR merges

The important fields are tier and reasons. The tier is never typed in by hand. It's the output of a grading step anyone can re-run.

Grading Risk Instead of Assuming It

The grader is a short list of triggers. If any one fires, the spec needs a human. If none fire, a reviewer agent can sign it. The list I use:

Risk triggerExampleWho signsWhat the reviewer agent writes
Needs a new architecture decisionIntroducing a queue where calls used to be synchronousHuman lead"Escalated: spec proposes a new pattern not covered by an accepted ADR."
Touches a contract or schemaNew column, changed API response, renamed eventHuman lead"Escalated: modifies a public contract; consumers listed below."
Touches infra, CI, auth or securityChanging a token lifetime, editing a deploy workflowHuman lead"Escalated: security-sensitive surface."
Too bigThe spec spans many modules or a long task listHuman lead"Escalated: exceeds the size threshold; consider splitting."
Critic agent has doubtsAn adversarial pass flags ambiguous acceptance criteriaHuman lead"Escalated: critic raised unresolved questions (quoted)."
Open decision owned by the lead"Do we soft-delete or hard-delete?" still unansweredHuman lead"Escalated: decision pending, owner is the lead."
None of the aboveAdd validation to an existing form fieldReviewer agent"Approved: UI-only, no contract or data change, criteria testable."

A few things matter more than the exact list.

Triggers are about blast radius, not difficulty. A gnarly algorithm inside one function is hard, but if it breaks, a test catches it and a revert fixes it. A one-line schema change is trivial, but it can corrupt data that no revert brings back. The grader should care about the second kind.

The critic counts as a trigger. Before grading, I run an adversarial critic agent over the spec whose only job is to find gaps: untestable criteria, missing edge cases, contradictions with the ticket. If the critic can't get to "no open issues" in a few rounds, the spec goes to a human. That's the cheapest signal you have that the agent and the ticket don't agree on what's being built.

Every escalation carries its reasons. A human reviewer opening an escalated spec should see why it landed on their desk, so they review the risky part instead of rereading the whole thing. That's what keeps an escalation from turning into a full re-review.

What a Reviewer Agent May and May Never Do

Letting an agent sign anything makes people nervous, and that's the right instinct. The answer isn't to avoid it but to write down the boundary in plain words and enforce it in the workflow, not in a prompt.

The reviewer agent may sign a spec whose grade is low-risk, and only with a written rationale stored in the spec. The rationale is the audit trail. If a human later asks "why did this go through without me?", the answer is right there in the repo.

Lines an agent never crosses

  • It never accepts an architecture decision record. ADRs are always a human call.
  • It never approves a spec whose grade says human, no matter what the spec text claims.
  • It never merges a PR. Merging stays with a person.
  • It never edits the grading rules or thresholds it's graded by.
  • It never acts in the tracker on someone's behalf without review.
  • Its signature is always revocable: any human on the team can reopen an agent-approved spec, and that reopening sends it to the human tier.

The last point matters for trust. People accept automation more easily when they know they can overrule it in one move without asking permission.

If you use Claude Code, the natural shape for this is a dedicated subagent with a narrow tool set and a hook that refuses the operations on the list above. The prompt describes the role; the hook enforces it. I covered the general version of this idea, separating what an agent can say from what it can do, in Bounded Autonomy.

Grade the Diff, Not the Document

Here's the hole in everything above: the grader reads the spec, and the spec is a promise. The code is what actually ships.

A spec can say "UI-only change" in good faith, get signed by the reviewer agent, and then during implementation the agent discovers it needs a new column. It adds a migration, the tests pass, and now a schema change is about to merge on an approval that was never meant to cover it.

So the grade runs twice. Once on the spec, and again on the diff at PR time. The second pass doesn't care what the spec claimed. It looks at the files the PR touches:

  • migrations or schema files changed, so the contract trigger fires
  • CI workflows, infra definitions or auth modules changed, so the security trigger fires
  • the diff is far bigger than the spec suggested, so the size trigger fires

If the PR-time grade is higher than the spec's tier, the PR escalates on its own: the approval is downgraded to human, the reasons are attached, and the lead gets pulled in. Nobody has to notice. Nobody has to remember to ask.

Loading diagram...

This is the single most important safety property of the workflow. Grading only the document creates a quiet incentive to under-describe work. Grading the diff removes it. It also fits naturally into a layered code review setup, where the re-grade is one more PR-time check alongside the review bots.

The Merge Closes the Spec

The other bottleneck in most teams is status. Someone has to move tickets across the board, update the spec, write "done" somewhere. With agents producing work quickly, that admin work piles up and the board drifts from reality.

My rule: the merge is the close. When the PR is marked ready, the spec gets its closing pass inside that same PR:

  1. Record the PR in the spec.
  2. Tick only the acceptance criteria that the code implements and a test proves. A criterion that's implemented but untested stays unticked, visibly.
  3. Write short implementation notes and any deviations from the spec.
  4. Move the status to merged.

Because the closing happens inside the PR, merging the PR is closing the spec. Status comes from the repository, not from someone remembering to drag a card. A dashboard can read the spec files on the main branch and know exactly where every ticket stands.

That second step is where honesty lives. It's tempting to tick everything once the PR is green. Don't. An unticked criterion is useful information: it tells QA where to look, and it tells the next agent session what's still unproven.

Self-QA vs UAT: Why QA Never Blocks a Merged Ticket

Two kinds of testing tend to get mixed up, and mixing them creates another queue.

Self-QA is the developer's own check before the PR. It's scoped to what the branch changed: run the repo's verification commands, drive the app against the acceptance criteria the change touches, save evidence. With an agent helping, it's cheap, and it catches the embarrassing stuff before a reviewer ever sees it.

UAT is QA's acceptance testing against a deployed environment, after the merge. It's what moves a spec from merged to accepted.

The rule that keeps things moving: QA never blocks a merged ticket. The developer's job ends at merge. If UAT finds a problem, that's a new ticket, linked to the original, not a reopened PR holding someone's branch hostage. And if your team doesn't have a QA role, there's no accepted step at all. Merge means done. Pretending you have a QA gate you don't staff just creates a column where tickets go to die.

Here's how responsibilities split across the lifecycle:

StagePMTech leadDeveloperQAReviewer agent
Ticket (high level)OwnsConsultedConsultedInformedNo role
Spec draft and refinementConsultedConsultedOwns (with agent)InformedRuns critic
Approval, low-riskInformedCan revokeInformedNo roleSigns with rationale
Approval, sensitiveConsulted on product callsSignsInformedNo roleWrites escalation reasons
ADRsConsultedDecidesProposesInformedNever accepts
Implementation and self-QANo roleInformedOwnsNo roleNo role
PR re-grade and mergeNo roleMerges if escalatedMerges if notNo roleNever merges
UAT and verificationInformedInformedFixes via new ticketOwnsNo role

Your table will differ. The point is to write one down so that "who signs this?" never needs a long chat thread.

Rolling It Out

I wouldn't switch a team over in one go. What worked:

Opt-out defaults. Start with every trigger switched on and the thresholds conservative, so most specs still go to a human in the first days. Loosen deliberately. It's much easier to relax a rule after seeing it's too strict than to tighten one after something slipped through.

Per-repo thresholds. "Too big" means different things in a small library and in a monolith. Sensitive paths differ too: one repo's auth module is another's billing code. Keep thresholds and the list of sensitive paths in each repo, versioned, and change them through PRs like anything else.

Make the grader re-runnable by anyone. If a developer disagrees with a tier, they should be able to run the grader locally and see which trigger fired. Opaque grading feels arbitrary; transparent grading invites good arguments about the rules instead of about individual specs.

What to measure in the first two weeks:

  • How long specs wait between ready and signed, per tier.
  • How often the PR-time re-grade escalates something the spec grade let through. Frequent escalations mean specs are under-describing work, which is worth a conversation.
  • How often a human revokes an agent signature, and why. Each revocation is a candidate new trigger.
  • How many criteria are left unticked at merge. That's your real test coverage of intent.

Expect the first friction exactly where you'd guess: a size threshold that's too tight for your codebase, and confusion about who owns the open decisions flagged in specs. Fix both by changing the rules, not by adding more review. That's the pattern to aim for.

Takeaways Checklist

If you want to try this, here's how to start:

  • Keep tickets high level and put one spec per ticket in the repo. One ticket, one branch, one PR.
  • Write down your risk triggers: new ADR, contract or schema, infra/CI/auth/security, size, critic doubt, open decision owned by the lead.
  • Run an adversarial critic over every spec before grading.
  • Let a reviewer agent sign only specs with zero triggers, always with a written rationale.
  • Write the "lines an agent never crosses" list and enforce it with hooks or permissions, not just prompts.
  • Re-grade the diff at PR time and auto-escalate when the tier goes up.
  • Close the spec inside the PR. Tick only criteria a test proves.
  • Separate self-QA (before PR) from UAT (after merge). QA never blocks a merged ticket.
  • Start strict, keep thresholds per repo, and measure wait time, escalations and revocations.

If you want to see a small public take on spec-first loops with agents, my spec-dev-ralph repo is a good starting point, and Spec-Driven Development tools compared covers the tooling side.

Conclusion

When agents write most of the code, the scarce resource is human judgment, and spending it on boring specs is a waste. Risk-tiered approval doesn't remove humans from the loop. It points them at the work that actually needs them: the schema change, the auth tweak, the decision nobody has made yet. The reviewer agent handles the rest in the open, with its reasoning written down and its signature one click from being revoked.

The two ideas I'd keep even if you drop everything else: grade the diff, not the document, and let the merge close the spec. The first keeps you safe. The second keeps your status honest.