Part 3 of Building My Agentic Workflow in Public. In Part 2 I pushed work onto parallel sub-agents to save context. This post is about the mess that created, and how I cleaned it up.
Parallel sub-agents let me build faster. They also introduced a problem I did not see coming. Each agent built its piece reasonably. The pieces did not match each other.
The divergence problem
Picture three sub-agents, each handed one feature of the same epic. Each one sees the epic. None of them sees the others. So they each make their own small decisions: how to log, how to handle errors, what to name things, where shared helpers live, how their feature talks to the next one. Every one of those decisions is defensible on its own. Together they are a mess.
flowchart TB EP["One epic"] EP --> A["Sub-agent A builds Feature 1"] EP --> B["Sub-agent B builds Feature 2"] EP --> C["Sub-agent C builds Feature 3"] A --> X["Three reasonable pieces that don't fit:<br/>different logging, naming, error handling"] B --> X C --> X
I felt this as a grind. I would evaluate the features one by one, and the evaluation almost always came back with blocking problems. Not because any single feature was wrong, but because they disagreed with each other. I spent hours reading long reports, fixing seams, and re-checking, and I was not getting much for it. The faster I fanned work out, the worse the pile-up got.
Write the rules down first
The fix sounds obvious once you say it: agree on the rules before anyone builds, not after. So I
added a step that runs before features get created. /generate-architecture writes a single file,
the code contract, that locks the cross-cutting decisions: naming, logging, error handling, where
shared utilities live, the seams between features. It is the one place that says “this is how we do
things here.”
The command that writes the contract (sanitized /generate-architecture)
It runs between /generate-epic and /generate-features, in its own sub-agent. A short description up top, then plain instructions. The last line is the load-bearing one: it makes the step a gate.
---
description: Before any feature is built, write the one file that locks the
cross-cutting decisions every feature must share.
argument-hint: <epic id>
---
Read the epic in $ARGUMENTS and a representative sample of the existing code.
Write one code-contract.md under 01-product-owner/00-architecture/ that locks:
module layout, naming, logging, error handling, where shared utilities live,
and the public seams between features. Do not write feature code. Show me the
contract and wait for my approval before saving it. This is a gate: no features
get generated until the contract exists.And it is a gate. No features get built until the contract exists. Every agent then reads the same contract before it writes a line, so they are all building to the same rules.
flowchart TB EP["One epic"] EP --> ARCH["/generate-architecture<br/>writes the contract first"] ARCH --> K["code-contract.md<br/>naming · logging · errors · shared seams"] K --> A["Feature 1"] K --> B["Feature 2"] K --> C["Feature 3"] A --> OK["Pieces that fit together"] B --> OK C --> OK
This one change did more for quality than anything before it. The agents stopped inventing their own conventions because the conventions already existed.
What the contract looks like (trimmed code-contract.md)
The section headers are stable on purpose, other commands target them by name. It lists module purposes and the seams between features, not every function, because an inventory rots the moment code is renamed. Copy the shape, fill in your own rules.
# Code Contract
#
# Module Layout
| Path | Purpose |
| ------------- | ---------------------------------------------- |
| src/api/ | HTTP routes only, no business logic |
| src/services/ | business logic, called from routes |
| src/models/ | data shapes, defined once, imported everywhere |
| core/ | cross-cutting utils: logging, errors, config |
#
# Logging Strategy
- Get the logger the same way everywhere; never roll your own.
- info = significant event, warning = recoverable, error = a user-visible failure.
- Never log secrets, tokens, or PII.
#
# Error Handling
| Layer | Behavior |
| -------- | ------------------------------------------------ |
| services | raise typed errors (ParseError, ValidationError) |
| routes | catch typed errors, map each to an HTTP status |
| never | a bare except that swallows the traceback |
#
# Public Seams Between Features
| Producer | Seam | Consumer |
| --------- | -------------------------- | --------- |
| Feature 1 | parse(file) -> ParsedDoc | Feature 3 |
| Feature 2 | analyze(input) -> Analysis | Feature 3 |When the contract is silent
No contract is complete on day one. So I gave the builders a release valve. When an implementer needs
a decision the contract does not cover, it does not just improvise and it does not edit the contract
either. It writes its suggestion to a separate file, code-contract-pending.md, that sits right next
to the contract. Later, /sync-contract reads those pending notes, checks each one against the actual
code, and folds the good ones into the real contract. The contract stays the single source of truth,
and it grows from what actually came up during building, instead of me trying to predict everything up
front.
A pending entry (code-contract-pending.md)
An implementer hit something the contract did not cover, so it wrote down a suggestion instead of editing the contract or improvising in silence. The file is just an append-only log of these, and /sync-contract drains it later.
# Code Contract - Pending Entries
#
## [date] - from /run-feature, Feature 4
Section: Shared Utilities
Proposed entry: add core/cache.py, an in-memory memoization helper.
Why needed: Feature 4 re-ran the same scoring 200 times per request. The
contract did not cover caching, so we put the helper next to the other
cross-cutting utilities rather than invent a new home for it.
Verification: the service imports memoize from core/cache.py; tests cover it.The command that folds it back in (sanitized /sync-contract)
One of only two commands allowed to write the contract. It verifies each pending note against the real code before adopting it, so a stale suggestion (from code that was later reverted or refactored) never sneaks in.
---
description: Fold the good pending suggestions back into the contract.
---
Read code-contract-pending.md and the current contract. Verify each pending
entry against the actual code: does it still exist as described? For each one,
decide adopt / modify / defer / reject. Show me the plan and wait. On approval,
add the adopted entries under the right section, note it in the changelog, and
clear those entries from the pending file. Defer anything you are unsure about;
it stays pending and comes back next time.There is a matching command for code that was written before a contract existed. /align-to-contract
reads the new contract and all the existing features at once, and produces a single set of changes to
bring everything in line. I review it once and approve it once, instead of grinding through the
feature-by-feature evaluation loop that used to eat my afternoons.
Three roles instead of one
The other half of the fix was to stop asking one agent to do everything. I split the work into three roles that check each other. A planner breaks the work down. An implementer writes the code. An evaluator reviews that code against the contract, and only reviews. The evaluator literally cannot write code, by design, so its judgment stays honest and it can never quietly “fix” something into existence.
When the evaluator reviews a feature, every finding lands in one of four buckets, which makes the result something I can act on instead of a wall of text:
flowchart TB E["The evaluator reviews work<br/>against the contract"] E --> B1["Blocking<br/>broken, must fix"] E --> B2["Contract violation<br/>fix the code"] E --> B3["Drift<br/>the contract is wrong, fix the contract"] E --> B4["Pattern candidate<br/>a new idea worth adopting"]
That third bucket matters more than it looks. Sometimes the code is right and the contract is what is out of date. Naming that explicitly, instead of forcing the code to obey a stale rule, is what keeps the whole thing from calcifying.
The same idea, for tests
Once this worked for code, I gave testing its own version of it. A separate contract for test code, and its own planner, builder, and evaluator. Tests have their own conventions, page objects, fixtures, the shape of a scenario, and they deserve their own rulebook rather than being crammed into the code one. Two contracts, kept apart, each free to evolve on its own.
What this phase bought me
This is the phase where the workflow grew up. Before, parallel work meant parallel chaos. After, the contract meant many agents could build at once and still produce something coherent, because the rules existed before the work did. The evaluator gave me a trustworthy second opinion that could not cheat. And the four buckets turned review from a slog into a short list of decisions.
Three posts in, the toolkit is no longer a pile of commands. It is one loop, and most steps now run in their own agent with a single job. Here it is end to end, the command on the left, the agent behind it on the right:
command agent what it does
-------------------------- -------------------- --------------------------------------------
/generate-epic you plain English becomes a structured epic
|
v
/generate-architecture arch-architect writes code-contract.md <-- THE GATE
|
v
/generate-features dev-planner the epic becomes small tickets, one contract
|
v
/eval-ticket dev-evaluator vets a ticket; 4 buckets, review only
|
v
/run-feature -task -defect dev-implementer builds it; a gap goes to the pending file
|
v
/create-defect -> /eval-ticket a bug becomes its own tracked ticket
|
v
/sync-contract contract-reconciler folds pending notes back into the contract
|
v
/align-to-contract contract-reconciler pulls pre-contract code into line
two files the loop turns on:
code-contract.md the rulebook (only /generate-architecture and /sync-contract write it)
code-contract-pending.md the inbox builders append to, never the contract itself
tests get the same shape in parallel: their own contract + qa-planner / qa-implementer / qa-evaluator
The shape that matters: the evaluator cannot write code and the implementer cannot rewrite the contract. Each role is boxed in, and that is what keeps the loop honest when several agents are building at once.
That coherence is exactly what made the next step possible. Once independent agents could build to a shared contract and be checked against it, I could stop driving every step by hand and start letting the whole loop run on its own. That is Part 4.