All essays
Essay AgentsAugust 24, 2026 · 16 min read

One ticket in, one reviewed pull request out

Four flows, twenty-six role agents and exactly one human stop per run. The delivery machine my work goes through end to end — how a ticket gets classified, isolated in a sandbox, specified, built, reviewed by a panel that re-runs in full after every repair, and released only once a person says yes.

The first delivery run I left unattended asked me eleven questions.

Not all at once — that would have been survivable. It asked one, waited, took my answer, worked for four minutes, and asked the next. Between the first question and the last I did nothing else, because a run that might stop at any moment is a run you have to sit in front of. The system was autonomous in the sense that it wrote the code. It was not autonomous in the only sense that matters, which is whether I could go and do something else.

The interesting part is that every one of those eleven questions was reasonable. "Should this write to the existing table or a new one." "Do you want the old endpoint kept for a deprecation window." A careful engineer would have asked them too. The problem was never the questions. The problem was that asking is an interrupt, and eleven interrupts cost more attention than doing the work by hand.

What I run now is a set of agentic delivery workflows: one ticket in, one reviewed pull request out, running unattended between two points a person owns. Four flows, twenty-six role agents, and exactly one stop per run. This is what is inside it.

#Four front doors, one ticket

Work arrives four ways and I want none of them to be special. A slash command in my terminal, a card dropped on a queue, a webhook from a chat message or an issue tracker, or something typed into a form. Each has its own adapter, and each adapter's only job is to produce the same file: a normalised ticket, on disk, with the same shape whatever it came from.

intake.funnelPASS / FAIL LOOPS
Four adapters, one redactor, one ticket file. Nothing after the ticket node can tell which door the work came through — which is the property that lets me add a fifth door without touching anything downstream of it.

The single Redact node is the part worth arguing about. There is one redaction implementation and every adapter runs it, including the one behind a form a person types into by hand. It is tempting to treat pasted text as already-trusted and skip it there; the reason not to is that the form is exactly where somebody pastes a stack trace with a bearer token in it, because they are in a hurry and it is the only field available.

After the ticket comes the router, and the router does two things. It picks a flow, a severity and the set of surfaces the work touches. And it returns a depthlite or standard — which decides how much authoring effort the run spends: whether the requirements checklist runs, whether there is a clarifying pass, how many tool calls reconnaissance gets, how large the repair budgets are.

Depth removes no gate. A one-line configuration change and a schema migration get the same review; what differs is how much work goes into describing them first. The moment depth is allowed to shorten review, it stops being an efficiency and becomes a way to route work around the checks — and the tickets most likely to be classified as small are the ones people are most confident about, which is not the same as the ones that are safe.

Below a confidence threshold the router does not guess. It returns one question — a single one, before any work starts — and that answer re-enters classification rather than going back to the reporter as a conversation.

#Four flows, and what each is for

A flow is a script with a fixed topology, bounded loops, and a declared set of stops. There are four.

The feature flow is the full path, and bugs run through it too. Seven phases, three bounded loops, one human stop. Everything below in this piece that is not explicitly about the others describes this one.

The hotfix flow is for production incidents. It races three fixes and then judges them, which is enough of a departure to be worth its own section.

The chore flow is the light path — refactors, tooling, dependency work, anything claiming no behaviour change. It has no specification gate. What it does have is reconnaissance running first, specifically to test the no-behaviour-change claim rather than accept it, and one hard rule: a chore must pass the existing suite without any test being modified. If a test had to change, the behaviour changed, and it was never a chore. It gets sent back to be routed again.

The factory flow is two nodes — classify, then dispatch — and it is what everything else is reached through.

That is a smaller number of flows than most people expect, and deliberately so. Every flow is a topology somebody has to be able to hold in their head when a run stops in the middle of it. Five flows that differ in small ways are worse than four that differ in kind.

#The path a feature takes

Reconnaissance, then authoring, then a specification gate, then a bounded build-and-test loop, then the review panel, then one human approval, then release.

feature.gatesPASS / FAIL LOOPS
Three backward edges land on Build and one lands on Spec. Every loop in the drawing is a repair, not a halt — the only place the run stops for a person is the single amber node, and it sits after everything has been built and before anything irreversible.

Reconnaissance runs before anything is written down. It maps the seam the change goes through, finds what already exists that could be reused, works out which test harness applies, and measures the blast radius. That last one matters more than it sounds: the blast radius it measures is what later decides which reviewers run, rather than the ticket's description of itself. Tickets systematically understate their own reach. Nobody files "small copy change" for something they know touches authentication.

The work is done in an isolated git worktree — a sandbox, in the vocabulary the rest of this site uses — so a run has its own checkout, its own branch, and no way to disturb anything else. The hotfix race uses three at once. Cleaning them up is part of the flow's own definition of finished, not a thing somebody remembers to do afterwards.

#A specification gate that repairs rather than halts

Gate one reads the specification package before any code exists: the specification itself, the plan, and the task list. It is checking for section completeness, for whether every requirement traces to a task, for whether the constraints the project declares have actually been applied, and for whether the contract a consumer will build against is described well enough to build against.

The verdict is GO or NO-GO, and NO-GO is the interesting one, because it is not a halt.

A rejected specification is not a question for a person. It is a list of blockers, and the blockers feed straight back into authoring, which rewrites and resubmits. The gate runs again on the new package. That loop has a budget like every other loop here, and when it is exhausted the decision goes to an arbiter rather than to me — a different arbiter from the one that judges hotfix candidates further down, doing a different job: it decides whether the run gets one more targeted round, a narrowed scope, or a terminal stop. I have written separately about what that budget actually costs and why a timeout is not one.

The thing I got wrong for a long time was treating a rejected specification as escalation-worthy. It is the cheapest possible failure — no code exists yet, nothing has been built on top of the misunderstanding, and the fix is a rewrite of a document. Stopping a run there to tell somebody about it spends a human interrupt on the one failure that costs nothing to absorb.

#Why the whole panel re-runs

Gate three is a panel of six reviewers, and each carries a predicate deciding whether it runs at all. Code review always runs. Tests always run. Acceptance coverage runs for features and bugs. Security review runs when the diff reaches authentication, secrets, sensitive data, or an outbound call. Documentation runs when a public surface changed. Interface verification runs when something user-facing changed and drives the real application to check it.

Each predicate runs against the blast radius reconnaissance measured. A one-line chore should not pay for a full panel, and it does not.

Review panel · one featureEvery reviewer re-runs every round
ReviewerRound 1Round 2Round 3Round 4
Code reviewalwaysPassedPassedPassedPassed
TestsalwaysFailedPassedPassedPassed
Acceptancefeature · bugPassedFailedPassedPassed
Securityauth · secrets · outboundPassedPassedFailedPassed
Documentationpublic surface changedSkipped — not applicable to this changeSkipped — not applicable to this changeSkipped — not applicable to this changeSkipped — not applicable to this change
Driven interfaceuser-facing changedPassedPassedPassedPassed
Security passed in rounds 1 and 2 and failed in round 3 — the repair that satisfied acceptance coverage widened an outbound call. Re-running only the reviewer who complained is how that ships.

The expensive decision is the columns rather than the rows: after a repair, the whole panel re-runs, not just the reviewer that complained.

That costs real money, and I have twice been tempted to make it incremental. The matrix above is why I have not. Security passes in rounds one and two, and fails in round three — because the repair that satisfied acceptance coverage widened an outbound call. There is nothing exotic about that. A fix for one finding routinely creates another, and the reviewer best placed to catch it is precisely the one that had no complaint last round and would therefore be skipped. A panel that only re-runs the complainer is a panel that ships its own repairs unreviewed.

Reviewers report and fix nothing. The one that runs the suite runs it and reports honestly; it does not make it green. Keeping the roles apart is what stops the cheapest route to green — loosening the failing assertion — from being available to the thing being graded.

#Three sandboxes, three strategies

Production incidents get a different shape, because the constraint is different: you do not know which approach will work and you cannot afford to find out serially.

hotfix.racePASS / FAIL LOOPS
The three candidates run the same failure under assigned strategies — minimal patch, root cause, defensive guard. Two edges reach Ship, and the fork is the argument: the fix ships on approval, and the run is not finished until the retrospective specification is back-filled.

The strategies are assigned, and that is the whole reason the race works. I tried it first with three identical agents on three sandboxes, expecting diversity to fall out of sampling. It does not. Identical agents given an identical failure produce near-identical diffs, and you have paid three times for one answer. Diversity is a product of the assignment: one candidate is told to write the smallest patch that stops the bleeding, one to find and fix the root cause, one to add the guard that makes this class of failure impossible.

Then an arbiter picks one, and it is not first-green-wins. Selecting on arrival time sounds like a neat use of the parallelism and is actually a scoring function that rewards whoever weakened the failing test fastest. The candidate that deleted an assertion finishes first every time. Selection is on merit — root-cause depth, blast radius, the quality of the regression test written before the fix — and a candidate that took the cheap route is disqualified outright rather than ranked lower.

"None of these should ship" is a verdict the arbiter is allowed to return. That is the edge looping back to triage in the drawing, and a race that cannot return it is not judging, it is just picking.

The last node is the one people skip. A hotfix takes on debt, not an exemption. The fix ships on approval, and the run is not done until a retrospective specification has been written for what shipped. Without that, "we'll write it up after" is where a codebase quietly acquires a layer nobody specified and nobody can safely change.

#One stop, and what it is holding

Every flow halts exactly once, in the same place: after the work is built and reconciled, before anything irreversible. The run waits there. Approving is the resume.

Getting to one stop meant removing the ability to ask. Flow agents have no question tool at all — not a discouragement in a prompt, an absence in the harness. An unclear decision is recorded as a question, a default is applied, its impact is rated, and the run keeps going. Every accumulated question surfaces together at the one gate. Eleven interruptions become one review. That design and the budget that bounds it are the subject of a separate piece; what matters here is that the stop is structural rather than a convention.

Two other mechanisms decide when the run stops early rather than at the declared place. If the number of high-impact assumptions passes a small ceiling, that alone trips a gate — the system escalating because it has noticed it is guessing too much, rather than presenting a large pile of guesses at the end. And any write to an enumerated guarded path halts for explicit approval regardless of severity, incidents included. Configuration is guarded alongside code, because the configuration is what decides how the machinery behaves; a system that protects its source and not its own settings has protected the less dangerous half.

Finished is machine-checked, not asserted. Every gate passed, a pull request exists, commits recorded, the knowledge base written back, assumptions reviewed, no sandbox left behind. Anything less is blocked or failed. There is no quiet partial success, because a quiet partial success is how you find out three weeks later that the last four runs never wrote anything back.

#What the machine remembers

This is a knowledge base and not a knowledge graph — different thing, despite the neighbouring vocabulary. Knowledge graphs and hybrid retrieval are how an agent searches a corpus it did not write. This is how the delivery system records what it learned about the project it works on, and the unit is not a document.

The unit is an atomic node with a stable identifier. A file path names a document, and a document holds many assertions; cite the path and you have cited all of them, including the three that have since become false. Agents cite node identifiers and resolve them through an index, which means a node can be retracted without every citation of its neighbours going stale.

Six types, and which are hand-authored is the interesting axis. Requirements, constraints and decision records are authored. Standards are authored and carry both a rationale and the name of whatever enforces them — a standard nothing enforces is a preference, and writing it down as a standard is how a document becomes something people learn to skim. Patterns are observations, promoted automatically after enough sightings, and never promoted into standards, because "this is how it is done" and "this is how it should be done" are different claims and conflating them launders habit into policy. Facts are derived from the code and never hand-written, which is what kills schema drift at the root.

Every node names the test or file proving it still holds. A linter fails the build on a dangling citation or a stale anchor — a check that citing paths could never support, because a path that still exists tells you nothing about whether the assertion about it survived.

Retractions write a new node linking back rather than stacking a "superseded" block inside the answer. It is a small thing that compounds: current truth should never be sitting behind three layers of correction, because the version an agent acts on is whichever one it read first.

#Only human signal teaches it

The last part is the system changing its own behaviour, and it is the part I am most careful with.

learning.tiersPASS / FAIL LOOPS
Two rejections carry the whole design. Self-graded evidence never promotes, and two occurrences never promote. The golden case at the end is what makes a rule removable later — without one, nothing can ever be safely deleted.

Four tiers: raw events, episodic run records, the knowledge base, and — the one that is actually self-improvement — the agents' own prompts, guards and governing rules.

Only human signal promotes. A gate rejection and the reason given for it. The difference between what an agent produced and what actually merged. An assumption somebody overrode. Agent self-assessment never promotes, and this is not a philosophical position: a system learning from its own unreviewed output reinforces its mistakes with rising confidence, and the confidence is the dangerous half.

The evidence bar is three occurrences, cited by run identifier. Two is a coincidence. This threshold is set high on purpose, because a rule is far more expensive to remove than to add — once it is in a prompt, nobody can tell whether the failure it was written for has stopped happening because the rule works or because the code changed underneath it.

Which is what the golden cases are for. Every accepted change ships with the artefact that slipped through, replayable. Without that, "learning" is unfalsifiable prompt drift, and no rule can ever be safely removed because nobody can demonstrate it is no longer earning its place. Prompts carry a hard line budget, and rules whose golden case has not fired in a long time get flagged for removal. Six months of appended rules is an archive of accumulated trauma that no agent can actually act on.

Changes are ratified by tier. Patterns, checklist items and reconnaissance hints apply themselves and are revertable. Agent prompts, guards and governing rules need a person to say yes. And proposals are required to prefer the cheapest mechanism that works — a guard over a prompt, a prompt over a rule — because a guard fails loudly and a prompt fails silently.

#What it does not do

Three limits worth stating plainly, because a system described only by its guarantees is being sold rather than explained.

Redaction is pattern-based and can miss a credential format it has never seen. It reduces exposure; it does not eliminate it, and the ticket file is treated as sensitive regardless of having been through it.

The intake idempotency store lives in memory, so a webhook retry arriving during a cold start can produce a duplicate run. It is a known edge with a known fix and it is not fixed yet.

The learning loop needs roughly ten records before its findings stop being coincidences, and it is built to say "not yet" rather than produce a confident pattern from four. That is the correct behaviour and it is also the least satisfying possible answer in the first fortnight of using it.

None of this is autonomy. It is transport — pick the work up, isolate it, move it through gates, loop on failure, report back — with judgement kept in separate agents that can be tested, replaced and argued with. That separation is the thing that makes the rest possible, and it is worth its own piece: testing an agent's control flow without a model. The vocabulary underneath all of it — harness, loop and graph — is set out on agentic engineering, and the builds this machine is used to deliver are on services.

The measure of the whole thing is not how much it writes. It is how long I can be somewhere else.

Building something in this shape and want a second pair of eyes on the architecture?

Book an audit