In this article
- The problem: capability without governance
- Architecture: five concentric layers
- The pipeline: nine stages, three hard stops
- Filesystem-driven phase detection
- Role-specialized subagents with tool allowlists
- Hooks: enforcement by invariant, not by prompt
- The knowledge lifecycle
- Attribution and traceability
- Lessons learned
Most teams use AI coding assistants the same way: open a chat, paste some context, hope for the best. That works for snippets. It falls apart for enterprise delivery, where a change isn't done until it has a spec, tests, updated documentation, a reviewable PR, and a traceable link back to the business requirement.
Over the past months I built a framework that closes that gap. It wraps a large enterprise frontend — a Next.js/React application for logistics pricing at a large shipping company — in a spec-driven, human-gated agentic pipeline built on Claude Code. An AI agent can now take a Jira ticket end-to-end to a pull request, but only through a pipeline that enforces specs, approval gates, quality checks, and full attribution.
This article walks through the architecture, the design decisions, and the lessons learned.
The problem: capability without governance
Modern coding agents are capable enough to implement a mid-sized feature autonomously. That's exactly the problem. Without structure, you get:
- Code without specs — nobody can review intent, only output.
- Documentation drift — the agent changes behavior, the docs describe the old behavior.
- Untraceable changes — six months later, nobody knows why a line exists or whether a human ever looked at it.
- Unbounded blast radius — an agent that can edit code can also edit the rules that constrain it.
The framework's thesis: agent capability should be earned through structure, not granted by default. Every power an agent has should be scoped, every critical transition should require a human, and every convention should be enforced by machinery rather than by prompt discipline.
Architecture: five concentric layers
The framework is organized as five rings, from most stable to most dynamic:
┌─────────────────────────────────────────────┐
│ 1. Cross-tool contract (AGENTS.md) │ ← read by every AI tool
│ ┌─────────────────────────────────────────┐ │
│ │ 2. Governance (.ai/ constitution, │ │ ← human-owned, agent read-only
│ │ SDD workflow, memory policy) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 3. Agent runtime (.claude/ agents, │ │ │ ← subagents, commands, hooks
│ │ │ commands, rules, hooks, memory) │ │ │
│ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ 4. Artifacts (spec/ per ticket) │ │ │ │ ← spec.md, plan.md, task.md
│ │ │ │ ┌─────────────────────────────┐ │ │ │ │
│ │ │ │ │ 5. Living docs (docs/<app>/)│ │ │ │ │ ← updated in same commit
│ │ │ │ └─────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Layer 1 — the cross-tool contract. A single AGENTS.md at the repo root is the entry point for every AI tool the team uses — Claude Code, Cursor, and GitHub Copilot all read it. It declares the stack, canonical commands, a path-to-documentation mapping, and a three-tier boundary: things agents may always do, things they must ask first about, and things they may never do. Tool-specific files (CLAUDE.md, .cursorrules, Copilot instructions) are thin bridges pointing back to it. One contract, many tools.
Layer 2 — the constitution. A .ai/ directory holds the human-owned governance: an immutable constitution (human-in-the-loop mandate, commit attribution format, escalation rules, verification gates), a stage-by-stage spec-driven-development workflow, and a memory policy. Every file is marked "Agents: read-only. Humans update via PR." Agents can read the law; they cannot amend it.
Layer 3 — the agent runtime. This is where Claude Code's native primitives get composed: subagents, slash commands, rules, hooks, and an executable orchestrator.
Layers 4 and 5 — artifacts and living docs — are the outputs: one spec folder per ticket, and per-domain documentation that must move in the same commit as the code.
The pipeline: nine stages, three hard stops
The core deliverable is an end-to-end pipeline:
spec.md with objective, boundaries, success criteriaplan.md + atomic task.mdtask.mdThe gates aren't suggestions in a prompt. The orchestrator returns a structured APPROVAL REQUIRED state and refuses to advance until the human replies with the approval phrase. Using exact phrases ("Spec approved", not "looks fine") was deliberate: it makes approval programmatically detectable and eliminates ambiguity about whether consent was given.
Filesystem-driven phase detection
The most satisfying design decision: the orchestrator is stateless. It infers the current pipeline phase entirely from what exists on disk:
- No spec folder for this ticket → we're at triage.
spec.mdexists but has no Decisions section → we're at clarify.plan.mdmissing → we're at planning.task.mdexists but git shows no source changes → we're at implement.- Source changed → we're at verify.
This makes the pipeline idempotent and resumable. Kill the session, come back tomorrow, re-invoke the workflow — it picks up exactly where it left off, because the truth lives in artifacts, not in conversation memory. Anyone who has watched an agent lose its place in a long session will appreciate why this matters.
Role-specialized subagents with tool allowlists
Rather than one omnipotent agent, the framework defines five narrow specialists, each with an explicit tool allowlist declared in its definition:
| Agent | Job | Tools |
|---|---|---|
| Jira analyst | Fetch ticket + linked docs, produce an implementation brief | 3 read-only MCP tools + file read. Cannot write anything. |
| Code reviewer | Convention & correctness review; may fix critical findings | Read/Edit/Write/shell, max 3 review cycles |
| Docs writer | Update per-domain documentation only | Read/Edit/Write — no shell access |
| Lint fixer | Run lint + type-check, apply fixes | Shell/Read/Edit, max 3 attempts |
| Security auditor | Final gate before PR: auth, input handling, secrets | Read/Edit/Write, max 2 fix cycles, verdict: APPROVED or ESCALATED |
The allowlist is the point. The Jira analyst cannot be prompt-injected into modifying code, because the harness never gives it a write tool. The docs writer cannot run arbitrary commands. Capability isolation is enforced at the platform level — the same principle as least-privilege IAM, applied to agents.
Each agent also carries a bounded retry budget (2–3 cycles) followed by mandatory escalation to a human. Agents don't thrash; they ask.
Hooks: enforcement by invariant, not by prompt
Every team has conventions that live in a wiki and die in practice. "Update the docs when you change the code." "Run the tests you touched." Prompting an agent to remember these works about as well as it does with humans.
The framework moves this enforcement into runtime hooks:
- A stop hook runs after every agent turn, inspects
git diff --name-only, and injects a system reminder if source files changed without corresponding documentation changes — or if spec and source changed without a changelog entry. - A post-edit hook fires whenever a test file is written and injects the exact test command to run for that file.
The distinction matters. A prompt instruction is advice the model may deprioritize under context pressure. A hook is a machine check that fires every time, regardless of what the model is paying attention to. Wherever a convention could be checked mechanically, we moved it out of the prompt and into a hook.
The knowledge lifecycle: three layers with a promotion path
Agentic systems have a memory problem in both directions: they forget useful lessons, and they "remember" things that were never validated. The framework addresses this with three knowledge layers and a controlled flow between them:
- Constitution (
.ai/) — immutable, human-authored. Changes require a human PR. - Session memory (
MEMORY.md) — agent-writable. Commands like/capture-learningand/harvest-pr-feedbackturn user corrections and PR review comments into structured, dated memory entries. A/consolidate-memorycommand periodically dedupes and prunes. - Living docs (
docs/<domain>/) — per-domainBUSINESS_CONTEXT,ARCHITECTURE, andCHANGELOGfiles, updated in the same commit as code.
Knowledge is promoted, never teleported: a lesson starts in memory, and only after it proves durable does /promote-to-docs move it into living documentation. The rules directory that constrains agent behavior can only be changed by humans. The result: the system learns from every PR, but agent-originated "knowledge" can never silently rewrite the rules that govern agents.
Attribution and traceability
Every commit an agent makes is prefixed [AGENT: <model>], and every PR body must include an "Agent Work Summary" — the agent's chain of thought: why this approach, what alternatives were rejected, what dependencies were chosen and why. Combined with the spec folder per ticket and changelog entries referencing the ticket, every line of agent-written code traces back through PR → spec → business requirement, with a named model on the commit.
When something breaks six months later, you know exactly what wrote it, why, and which human approved it.
Lessons learned
1. Structure beats prompting. Every convention we moved from prompt text into a hook, a tool allowlist, or a gated workflow stopped being violated. Prompts are suggestions; harness configuration is law.
2. Filesystem state is the best agent memory. Artifacts on disk survive context windows, crashed sessions, and model switches. Design pipelines so the next step is derivable from what exists, not from what was said.
3. Narrow agents are safer and better. A read-only Jira analyst produces better briefs than a do-everything agent, because its entire context is dedicated to one job — and it's structurally incapable of collateral damage.
4. Exact approval phrases sound bureaucratic and are worth it. Free-text approval invites the agent to interpret enthusiasm as consent. A fixed vocabulary makes the gate binary.
5. The framework is a product. It needs its own maintenance loop — harvesting feedback, consolidating memory, promoting learnings. The meta-commands that maintain the framework turned out to be as important as the delivery pipeline itself.
Closing
The interesting frontier in AI-assisted engineering isn't making agents more capable — the models handle that. It's building the scaffolding that makes their capability safe to use at enterprise scale: scoped permissions, human gates, machine-enforced conventions, and a knowledge lifecycle that lets the system improve without letting it rewrite its own rules.
Agents write the code. The framework decides what "done" means.