I wrote earlier about six agents migrating services off a legacy workflow engine. That was one pipeline doing one job. This is the layer underneath it: what an agent is actually made of, where that definition lives, and what changes when you run an entire product development lifecycle this way — requirement through to deployment.
The short version: the model is the part most likely to change, and it is the part you own least. Everything else — the personas, the rules, the skills, the hand-off contracts, the record of who approved what — is yours. It should live where the rest of your engineering lives.
An agent is a composition, not a prompt
The word "agent" gets used for anything from a system prompt to a product. The definition I've settled on is a composition of five separable things:
| Part | What it is | Why it is separate |
|---|---|---|
| Persona | The remit — what this agent is for | Sets scope, so the agent declines work that isn't its job |
| Rules | Hard constraints it may never cross | Enforceable and testable; a persona is neither |
| Skills | Reusable capabilities with schemas | Shared across agents, versioned independently |
| Tools / MCP | How it reaches the real system | Swappable per environment; the permission surface |
| Memory | State that outlives one context | A system of record, not a scratchpad |
The separation earns its keep the first time you change one without touching the others. A new rule about data residency should apply to every agent that touches customer records without editing six personas. A skill that formats an API contract should be usable by the Product Owner agent and the Developer agent both, and should be fixable in one place.
The distinction I got wrong for a long time was persona versus rules. Persona is the fun part to write and it does almost nothing under pressure — "you are a meticulous senior engineer" does not survive a model that has decided it knows better. Rules are the part that holds, because they are specific, negative, and checkable: never modify the legacy folder, never generate code before the plan is approved, never invent a metric. If you find yourself expanding the persona to fix a behaviour, the fix probably belongs in rules.
Which is why it belongs in the repository
None of the above is a conversation. It's configuration, and configuration goes in version control:
repo/
├─ AGENTS.md persona + rules, vendor-neutral
├─ .claude/
│ ├─ agents/ one file per role agent
│ ├─ skills/ shared capabilities
│ ├─ commands/ entry points
│ └─ settings.json permissions, hooks
├─ .github/
│ └─ copilot-instructions.md thin pointer at AGENTS.md
└─ docs/pdlc/<feature>/
├─ 01-requirements.md
├─ 02-plan.md ← human gate
├─ 03-implementation.md
├─ 04-validation.md
└─ 05-release.md
Three things follow from this layout that do not follow from a chat window.
It reviews like code. A change to how the Developer agent behaves arrives as a pull request, with a diff and a reviewer. Someone can object to it before it affects anything.
It diffs. When output quality changes, you can find out what changed in the harness, because the harness has a history. "The model got worse" is usually "someone edited a rule".
It is portable. Keep the substance in vendor-neutral files and let each runtime's file be a thin pointer at them. Claude Code reads AGENTS.md and .claude/; GitHub Copilot reads .github/copilot-instructions.md. Point the second at the first and you maintain one set of rules.
I want to be precise about how far that portability goes, because it is oversold. The intent transfers cleanly — personas, rules, artefact structure, the definition of done for each phase. The execution does not: tool-calling models differ, permission models differ, and what one runtime does with a hook the other has no concept of. Treat portability as "I can move vendors without rewriting how my organisation works", not as "the same repo runs identically on both". The first is genuinely valuable and achievable. The second is a slide.
The lifecycle as agents
Every role in product development becomes an agent. Sixteen of them, grouped into five stages:
| # | Stage | Agents | Produces | Gate |
|---|---|---|---|---|
| 01 | Discover | Business Analyst · Product Manager · UX Researcher | 01-requirements.md |
Problem and measure of done agreed |
| 02 | Define | Product Owner · Solution Architect · Security Architect | 02-plan.md |
Approved before any code exists |
| 03 | Build | Backend Developer · Frontend Developer · Data Engineer | 03-implementation.md |
Diff reviewed |
| 04 | Verify | Test Engineer · QA Analyst · Performance Engineer · Accessibility | 04-validation.md |
Criteria, flags, rollback verified |
| 05 | Release | DevOps Engineer · SRE · Release Manager | 05-release.md |
Promotion signed off |
The grouping is the important part, and it took me a while to arrive at. My first version had one agent per stage — a single "Developer", a single "Tester". That is tidy on a diagram and wrong in practice, for the same reason the single-agent version of the whole pipeline was wrong: a remit broad enough to cover backend, frontend and schema work is too broad to constrain. "Never widen a public contract without an ADR" is an enforceable rule for a Solution Architect. Bolted onto a general-purpose Developer agent alongside twenty others, it is a suggestion.
Narrow agents also make the concurrency obvious. Within a stage the agents are independent — the Security Architect is not waiting on the UX Researcher — so they run at once and the stage closes when the slowest finishes. Between stages nothing overlaps at all. That asymmetry is the whole scheduling model: parallel inside a stage, strictly sequential across gates.
Gate 02 earns the whole structure. An agent that writes code before the approach is agreed produces something plausible and wrong, and plausible-and-wrong costs more to unpick than nothing at all — a reviewer reading confident code reviews the implementation rather than questioning the approach. The cheapest artefact to reject is a plan.
The hand-off contract is the design problem
Not the prompting. The prompting is the part that looks like the work and is the part that matters least, because it is the part you can change in a minute.
What actually determines whether this holds together is the contract between stages, and it comes down to four rules I'd now treat as non-negotiable.
The output is a file, not a message. If stage four's input is "whatever stage three said", you have no artefact to review, no thing to version, and no way to re-run stage four alone. A file forces you to decide what the stage actually produces.
Each agent receives what it needs and nothing more. This began as a token-cost optimisation and turned out to be a correctness measure. An agent that cannot see the previous stage's reasoning cannot inherit its mistakes — it only sees the conclusion, which is the thing that was reviewed.
Modifications are versioned, never overwritten. The diff between plan v1 and v3 is where most of the review value lives, because it shows what a human pushed back on. Editing 02-plan.md in place destroys exactly the evidence an audit asks for.
"Insufficient information to proceed" is a valid output. Early on, an unsure agent would produce a confident answer with the hedge buried in paragraph four. Making refusal an explicit, preferred outcome improved the pipeline more than any model change.
Sequential where it must be, parallel where it can be
The stage table above already implies this, but it is worth stating directly, because it is the scheduling decision the orchestrator exists to make. Inside verification, four concerns — security, performance, contract compatibility, regression against baseline — are genuinely independent, and running them as four agents with four clean contexts is both faster and better: no shared scratchpad means no cross-contamination, and four narrow reviews beat one review trying to hold four lenses at once.
Two things are easy to get wrong here.
Fan-out is a correctness decision before it is a speed one. The test is not "can these run at the same time" but "does any one of these change another's premise". Parallelising work that shares a premise gets you four confident, mutually inconsistent answers and a reconciliation problem larger than the original task. I have done this and spent longer merging than I would have spent in sequence.
Fan-in has to count arrivals, not assume them. Three of four reporting is the normal case, not the exception — agents time out, exceed budget, or return something unusable. That state needs modelling:
// Gather waits for every dispatched agent and returns what actually arrived.
// It deliberately does not fail on the first error: a partial result set is a
// legitimate outcome the caller has to make a decision about, not an accident.
func (o *Orchestrator) Gather(ctx context.Context, runs []Run) (Report, error) {
var (
mu sync.Mutex
report = Report{Expected: len(runs)}
wg sync.WaitGroup
)
for _, run := range runs {
wg.Add(1)
go func(r Run) {
defer wg.Done()
out, err := r.Execute(ctx)
mu.Lock()
defer mu.Unlock()
if err != nil {
report.Missing = append(report.Missing, Failure{Agent: r.Name, Err: err})
return
}
report.Findings = append(report.Findings, out)
}(run)
}
wg.Wait()
// The gate sees the shortfall; it does not get a quietly smaller answer.
return report, nil
}
The orchestrator's job is deciding which mode applies, and holding the state that no individual agent should hold: phase, approvals, artefact versions, and which agents are still outstanding.
What I would tell you before you start
- Write the rules before the personas. Rules are what hold under pressure. Personas are what you write when you are enjoying yourself.
- Make every stage produce a file. If it is not in version control, it did not happen — and you cannot re-run one stage in isolation.
- Keep the vendor-specific file thin. One set of rules, pointed at from
.github/and.claude/. The day you evaluate a different runtime, this is the decision you will be glad you made. - Only fan out across genuinely independent work. Shared premises plus parallelism equals reconciliation.
- Make the gates uncomfortable to skip. If approval is a keystroke somebody makes reflexively, you have ceremony rather than control.
None of this is really about model capability. It is boundaries, contracts, idempotency, retries, observability and failure modes — a distributed-systems problem, where one of the nodes happens to be a new kind of unreliable remote call. That is oddly reassuring, because we already know how to build those.