WritingAI and agents
How to Tell Whether the Agent Workflow Improved Delivery
More merged pull requests is not evidence that an agent workflow improved delivery. A manager needs accountable roles, retained evidence, reversibility, escalation rules, and one honest rate inside a balanced view.
16 min readSeries 7 of 8
- 01AI Agents Need Architectural Boundaries, Not Just Prompts
- 02LLM Wiki for Software Teams: Two Problems AI Agents Can't Fix Themselves
- 03Keeping Spring Boot Delivery Aligned with Product Context and Engineering Standards
- 04AI Agents in the SDLC: What Changes Beyond Code Generation
- 05What a Passing Test or Eval Tells You About an Agent-Written Change
- 06Agents Can Open the Pull Request, but They Cannot Sign the Release
- 07How to Tell Whether the Agent Workflow Improved Delivery
- 08Agent-Written Prose Has No Failing Test

Before any of the controls in this series, the adoption dashboard looked the way adoption dashboards are supposed to look. More pull requests created and merged, a shorter median time to merge, a growing share of changes with the agent as author. The one-click publish change was one of them. It merged quickly, with passing tests and a clean review, and it counted toward every line on the board.
Underneath, the publish log said something else. Memo M-104 reached clients with no sign-off record, and two of its claims, c3 and c4, pointed at no note. The rule existed elsewhere in the repo, but it was missing from the ticket and from the new endpoint's checks. Every check passed. The dashboard counted the result as throughput.
This is the illustrative research desk from the series. This part is for the person who gets asked "is the agent thing working?" and has to answer with something better than a chart going up.
Output counts rise either way
Pull-request counts, merge time and agent-authored share measure adoption. A change that skips a constraint merges exactly as fast as one that honours it, often faster, so every one of those numbers is blind to the publish change.
Any count has the same trap. It rises with volume whether or not the work got better. A team that ships more changes and more mistakes looks identical to one that ships more changes and fewer. Agents tend to amplify whatever a delivery system already does well or badly, so a single number is the wrong instrument. You want several, with tension between them, read together.
A note on vocabulary. When I say "delivery metrics" I mean the standard software delivery set: change lead time, deployment frequency, change fail rate, failed deployment recovery time and deployment rework rate. I spell them out rather than use the acronym.
Who is accountable at each gate
Before any metric, a manager needs to know who decides what, and when something must escalate. This is the management view of the handoff model from the first article:
Decision | Accountable | Evidence that must survive | Can the result be undone? | Escalate when |
|---|---|---|---|---|
Intent accepted | Product manager | Intent record, every constraint owned | Yes | A constraint has no owner |
Design or policy accepted | Security reviewer, or the researcher for source rules | Criteria with each concern resolved or carried by name | Yes | A tier 3 path lacks its policy review |
Plan accepted | Engineer | Plan naming surfaces, tests, rollback | Yes | The plan touches a surface the intent did not name |
Implementation accepted | Code owner, with product manager on eval criteria | Test and eval results tied to the commit | Yes | High-risk finding open, or eval cases changed without the owner |
Release authorized | Named release authority | Release record bound to the artifact digest | Sometimes | Rollback path untested |
Memo approved | Researcher | Sign-off bound to the memo digest, evidence and policy version | Until clients are notified | A memo is published without a valid sign-off |
Rollback or retraction executed | Operations, with the researcher for retraction notices | Who decided, who executed | Partly | Clients were already notified |
"Sometimes" is the honest answer for releases. The one-click flag can stop further distribution, and a queued memo can still be cancelled, but a memo clients were already notified about can only be corrected or retracted.
The dashboard contract
The dashboard I would set up is written as a contract, in two groups. Delivery tiles answer whether the coding agent improved how the team ships software. Publication-control tiles answer whether the app's own controls held. Both matter, but an unsigned memo does not by itself tell you whether agent-assisted delivery got better or worse, so they are read separately and linked by a trace. For each tile: what each tile means, who owns it, how often it is read, and what happens when it moves. The action rules matter more than the tiles. Every window, margin and minimum sample below is an illustrative policy setting written as a variable. Agree the real values during the pilot, from your own baseline. The one fixed rule is completeness: tier 3 constraint capture is 100% or the change is blocked.
Delivery tiles
Tile | Formula | Owner | Read | Action when it moves |
|---|---|---|---|---|
Constraint capture (early warning) | tier 2+ change requests where every constraint has an owner, or with an owner-confirmed "none" ÷ tier 2+ change requests | Tech lead | Weekly | Tier 3 below 100% is a gate failure: block |
Intent-correction rate | mature agent-assisted changes needing an intent correction in their window ÷ mature agent-assisted changes, by tier | Product lead | Monthly | Above the non-agent baseline by more than margin M for two mature cohorts of at least S changes: review the intent template for that tier |
Change fail rate | deployments needing immediate intervention ÷ deployments, agent-assisted vs not | Ops lead | Monthly | Agent-assisted rate above the non-agent rate by more than M for two months with at least S deployments: review depth on agent PRs is checked |
Deployment rework rate | unplanned, incident-driven deployments ÷ deployments | Ops lead | Monthly | Same rule as change fail rate |
Change lead time | median time from first commit to production, agent-assisted vs not | Tech lead | Monthly | Trend only; read next to change fail rate |
Deployment frequency | production deployments per service per week | Ops lead | Monthly | Trend only |
Failed deployment recovery time | median time from a failed deployment to restored service | Ops lead | Monthly | Above the baseline by more than M: rehearse the rollback |
Reviewer minutes | median reviewer minutes per merged change, agent vs human | Tech lead | Monthly | Agent PRs above the human median by more than M for two months: narrow the agent's scope |
Review rounds | median review rounds per merged change, agent vs human | Tech lead | Monthly | Same rule as reviewer minutes |
Revert within N days | mature agent-assisted changes reverted within N days ÷ mature agent-assisted changes (N set in the pilot) | Tech lead | Weekly | Above the non-agent rate by more than M with at least S changes: the workflow is paused and reviewed |
Eval freshness | tier 3 cases confirmed within the agreed window ÷ tier 3 cases | Case owners | On every model change | Any stale tier 3 case: block tier 3 merges |
Guardrail denials | denied agent tool calls per 100 calls, by tier and model version | Platform lead | Weekly | More than M above the previous model or prompt version: rerun the evals |
Separation-of-duties violations | approvals at a software gate given by the coding agent's identity or by the engineer who requested the change | Engineering manager and security reviewer | Daily alert | Must be zero. Any row is an incident |
Cost per accepted change | model spend ÷ agent-assisted changes accepted at their gate | Engineering manager | Monthly | Trend only, never a target on its own |
Publication-control tiles
Tile | Formula | Owner | Read | Action when it moves |
|---|---|---|---|---|
Publications without a valid sign-off | publications whose sign-off was missing, unauthorized, for a different memo or evidence digest, under an old policy version, revoked or expired at publication time | Security reviewer | Daily alert | Must be zero. Any confirmed row is an incident; "unknown" rows are a telemetry defect, fixed within the week |
Structurally traced memos | published memos where every claim resolves to a sourced note ÷ published memos | Lead researcher | Weekly | Below 100%: the publishing service has a bypass; stop publishing |
Source-fidelity findings | claims a researcher marked as stronger than their note ÷ claims reviewed, by desk agent version | Lead researcher | Monthly | Above the previous agent version by more than M with at least S claims: rerun the debate evals |
Time to retraction notice | median time from a confirmed bad publication to the correction notice | Product manager | Per incident | Above the agreed target: rehearse the retraction path |
Every publication-control row carries a trace back to the change that produced it: software release, desk agent run, memo digest, sign-off, publication event. When a control fails, the trace tells you whether to look at a code change, a model change or a person.
I read it with two rules. Never show agent share or PR counts without review load and change fail rate next to them. And never show a missing value as zero. Incomplete data is "unknown".
The one rate I would start with
The leading tile, constraint capture, tells you whether intent is being written down at all. The intent-correction rate tells you, afterwards, whether intent reached the agent's inputs. A correction counts when the intent record, the criteria or a constraint had to change after merge, tagged as such in the tracker at the time. A code-only fix does not count.
The rate needs a fixed follow-up window per change, declared before anyone looks at the data, and it is reported only for mature cohorts, where every change has had its full window. The query counts a change once however many corrections it got, and leaves the rate empty when observation was incomplete:
# pseudocode: intent-correction rate per mature monthly cohort; all timestamps UTC
INCLUSION_RULE = "agent-assisted-v1" # versioned: an agent run record linked to the change
def intent_correction_rate(changes, corrections, cutoff, follow_up_days):
window = days(follow_up_days)
# a monthly cohort is mature only when its last possible change has had its full window
mature = [c for c in changes if end_of_month(c.merged_at) + window <= cutoff]
rows = []
for key, group in group_by(mature, "month", "risk_tier", "service",
"change_class", "agent_assisted"):
corrected = [c for c in group if any( # a change counts once
k.kind == "intent" and c.merged_at <= k.corrected_at <= c.merged_at + window
for k in corrections_of(c))]
observed = all(c.observed_through >= c.merged_at + window for c in group)
rate = len(corrected) / len(group) if observed else None # "unknown", never zero
rows.append({**key, "changes": len(group), "corrected": len(corrected),
"rate": rate, "inclusion_rule": INCLUSION_RULE})
return rowsThe real query also refuses duplicate change ids, unknown risk tiers and corrections for changes it does not know, because each of those quietly changes the denominator. Here is a small input, with illustrative data. All four changes are tier 3 features in the publishing service, observed through 2026-10-10, and the run uses a 30-day window and a cutoff of 2026-10-10:
Change | Merged | Agent-assisted | Corrections after merge |
|---|---|---|---|
ch-1 | 2026-08-03 | Yes | Intent on 2026-08-10, intent again on 2026-08-12 |
ch-2 | 2026-08-20 | Yes | Code only, 2026-08-21 |
ch-3 | 2026-08-25 | No | None |
ch-4 | 2026-09-28 | Yes | None |
cohort agent_assisted changes corrected rate
2026-08 False 1 0 0.0
2026-08 True 2 1 0.5The September change is left out because its cohort has not had its full window by the cutoff. ch-1 had two intent corrections and counts once. ch-2 had only a code fix, so it does not count.
Compare the agent-assisted rows with the non-agent rows for the same tier, service, change class and window. "Agent-assisted" means what the versioned inclusion rule says, and the rule is recorded on every row, so a changed definition shows up as a changed rule, not a changed rate. If there is no comparable non-agent group, use the first mature agent cohort as a baseline and call it a baseline, not a target.
The rate has limits. It is a local signal, not proof that the workflow caused anything. And it cannot tell a team that writes better intent records from a team that stopped tagging corrections. That recording bias is worst on teams that never tagged corrections before. So start tagging first, and treat the first period as calibration.
Valid at the moment of publication
"Published without sign-off" sounds simple and is not. A sign-off id on the publication record is not enough: the sign-off has to be for that exact memo digest, by an authorized researcher, not yet expired and not revoked, judged at the publication timestamp, not today. And an approval record that cannot be found is "unknown", which is a telemetry problem, not proof that nobody approved. The check reads the authoritative publication and sign-off records, and reuses signoff_status from the first part. Passing the publication time is not enough on its own: who counted as a researcher, and which policy version applied, also have to come from history, or a policy change today would mark every older publication as invalid:
# pseudocode: judge every publication against the sign-off as it stood at publication time
def publications_without_valid_signoff(publications, signoffs):
for pub in publications: # the publishing service's own records
at = pub.published_at
researchers = researcher_roster_as_of(at) # authority then, not today
policy = policy_version_as_of(at)
if pub.signoff_id is None:
reason = "missing"
elif pub.signoff_id not in signoffs or researchers is None or policy is None:
reason = "unknown" # a record or its history is absent
else:
reason = signoff_status(signoffs[pub.signoff_id], pub.memo, # memo as published
researchers, policy, at=at)
if reason != "valid":
yield pub.publication_id, pub.memo.id, at, reasonWith illustrative records, where the roster and policy history are complete and nothing changed in them:
Publication | Memo | Published (UTC) | Sign-off on the record |
|---|---|---|---|
p1 | M-104 | 2026-10-03 08:00 | None |
p2 | M-105 | 2026-10-04 08:00 | so-5: researcher-1, this exact snapshot, valid from 07:00 that day for 24 hours |
p3 | M-106 | 2026-10-04 09:00 | so-6: researcher-2, an earlier snapshot of M-106 |
p4 | M-107 | 2026-10-05 10:00 | so-7, which is not in the sign-off store |
publication_id memo_id published_at reason
p1 M-104 2026-10-03 08:00:00+00:00 missing
p3 M-106 2026-10-04 09:00:00+00:00 digest_mismatch
p4 M-107 2026-10-05 10:00:00+00:00 unknownp1 is the original incident. p3 was signed, but the memo was edited after sign-off. p4 points at a sign-off the record store cannot find, so it goes to the telemetry queue before anyone calls it an incident.
Every agent run leaves a record
None of the tiles above work without data, and the most useful data is a record of every agent run. It doubles as the audit trail, which is the question every reviewer, auditor or incident lead eventually asks: what exactly did the agent do, with which model and instructions, and who approved the result?
{
"run_id": "<uuid>", "started_at": "<utc>", "agent_id": "<agent identity>",
"requested_by": "<person>", "work_item": "<id>", "branch": "agent/<run_id>",
"risk_tier": 3,
"model": {"deployment": "<name>", "version": "<pinned version>"},
"instructions_sha256": "<hash of system prompt and repo instructions>",
"context": [{"doc": "intent-fast-publish.md", "sha256": "<hash>"}],
"tool_calls": [
{"tool": "write_file", "target": "src/api/memos.py", "decision": "allow"},
{"tool": "approve_pull_request", "decision": "deny", "rule": "human-decision"}
],
"usage": {"input_tokens": null, "output_tokens": null, "cost": null},
"outputs": {"pull_request": "<id>", "commit": "<sha>"},
"evals": [{"case": "B-ticket-silent", "runs": "<n>", "passes": "<x>",
"runs_with_critical_failure": 0, "lower_bound_95": "<p>", "gate": "pass"}],
"human_decisions": [{"gate": "software_release", "actor": "<person>", "decision": "approve",
"artifact_sha256": "<digest>", "at": "<utc>"}],
"data_classification": "no client data in prompts or context",
"retention_class": "<records policy id>"
}The whole record is illustrative, and null means not measured, never zero. Leave out raw credentials, client data and the model's hidden reasoning. Record actions, sources, decisions and outcomes.
If you send these to a log table, say AgentRun_CL, the snake_case JSON has to be mapped to the table's columns on the way in. With a data collection rule, that is one transform, with model, tool_calls and human_decisions declared as dynamic in the incoming stream:
// transformKql in the data collection rule
source
| project TimeGenerated = todatetime(started_at), RunId = tostring(run_id),
AgentId = tostring(agent_id), RequestedBy = tostring(requested_by),
RiskTier = toint(risk_tier), ModelDeployment = tostring(model.deployment),
ModelVersion = tostring(model.version), ToolCalls = tool_calls,
HumanDecisions = human_decisionsThen two queries cover the most important tiles. The first is the separation-of-duties alert:
// Separation of duties, software gates only: approvals by the coding agent or the
// engineer who requested the change. Memo approval is a different gate: a researcher
// approving the memo they commissioned is expected, so it is never in this query.
AgentRun_CL
| where TimeGenerated > ago(30d)
| mv-expand d = HumanDecisions
| where tostring(d.decision) == "approve"
| where tostring(d.gate) in ("implementation", "software_release", "waiver")
| where tostring(d.actor) == AgentId or tostring(d.actor) == RequestedBy
| project TimeGenerated, RunId, Gate = tostring(d.gate), Actor = tostring(d.actor)The second is the guardrail-denial rate:
// Guardrail denials per 100 tool calls, by tier and model version
AgentRun_CL
| where TimeGenerated > ago(30d)
| mv-expand t = ToolCalls
| summarize Calls = count(), Denied = countif(tostring(t.decision) == "deny")
by RiskTier, ModelDeployment, ModelVersion
| extend DeniedPer100 = round(100.0 * Denied / Calls, 1)Run the separation-of-duties check against the code host's own audit log too. The agent's own log cannot prove that something did not happen. The publication-control tile runs the sign-off check above against the publishing service's own records, not against telemetry, for the same reason.
The original failure leaves little trace until it fails. A constraint that never reached the intent record, and that no eval case or policy lookup asks about, produces no failing test, no failing eval, no rejected review, right up until the publish log shows a memo with no valid sign-off. Constraint capture is my best early signal, and it only measures whether people wrote something in a field, not whether they wrote the right thing. What a manager can do this week is smaller: open the last tier 3 release record and ask for the reversibility line, and ask when its eval cases were last confirmed. If nobody can answer, that is the measurement.
Traced is not the same as true
There is one row in the dashboard that none of this settles. After the controls, M-104 could only go out with every claim pointing at a sourced note. Claim c5, "Lead times fell in every region", pointed at note n4 from the start. It was structurally traced. It was also wrong: n4's data shows lead times fell in two of three regions. The source-fidelity tile counts findings like that, but only after a researcher reads the claim against the note.
The memo is agent-written prose. The last part asks what checks the sentences.
Newsletter
No comments yet.