---
title: "Spec-Driven Development with OpenSpec: How My Agents Write Production Code"
description: "I have spent months letting agents write production code. Not by magic: by specification. This is how SDD works with OpenSpec."
pubDate: "2026-03-06"
category: "programacion"
language: "en"
tags: ["claude-code", "sdd", "openspec", "ai", "productivity", "specifications", "agents", "ddd"]
---
I have spent months letting AI agents write production code. Not in side projects — in applications with real users, teams and production deploys. And it works. But not because AI is magic. It works because I give it something most people do not: a specification.

I have written about how I use Claude Code day to day and about setting up Agent Teams for complex development. But I was missing the piece that makes all of it work: the specification system that feeds those agents.

It is called SDD — Specification-Driven Development. And the framework that implements it is OpenSpec.

## The problem nobody wants to see

There is a term that has become fashionable: *vibe coding*. You tell the agent "build me a user CRUD" and it generates something that works. More or less. For a prototype, perfect. For production, it is a time bomb.

The problem is subtle. When you ask an agent for something vague, the agent does not ask — it invents. It invents field names, it invents relationships between entities, it invents business rules. And it does so with absolute confidence, which makes you think it has understood everything. But it has understood nothing, because there was nothing to understand.

The difference between "build me a CRUD" and a real specification is the difference between a sketch on a napkin and an architectural blueprint. Both give you an idea of the building. Only one lets you construct it.

Without a spec, the agent invents. With a spec, the agent executes. That is the line separating vibe coding from professional development with AI.

And I am not talking about documentation. Nobody likes writing documentation and nobody likes reading it. I am talking about an artefact that tells the agent exactly what to build, under which constraints, and why. A technical contract that both you and the agent can verify.

## What SDD is

SDD — Specification-Driven Development — is an approach where specifications do not document what has been done; they gate what is going to be done. The spec comes first. The implementation comes after. And the spec blocks the implementation until it is approved.

The flow is linear and each phase blocks the next:

**Proposal** → **Specs** → **Design** → **Tasks** → **Implementation** → **Verify**

You cannot implement without tasks. You cannot create tasks without a design. You cannot design without specs. You cannot write specs without an approved proposal. Each phase is a checkpoint that ensures what comes next makes sense.

This is not bureaucracy. It is that I have seen an agent implement something that made no sense far too many times, because nobody stopped to think about whether the proposal was correct. With SDD, that "wait, this is not what we wanted" moment happens in the proposal phase, when changing direction costs nothing. Not in the implementation phase, when 40 files have already been touched.

The spec is not documentation — it is a contract that agents consume. That difference changes everything. Documentation is written for humans and goes stale the next day. A spec is written for an agent to execute, and it is verified against the result.

## OpenSpec: the framework

OpenSpec is the framework that implements SDD. It is opinionated, designed for multi-repo projects with AI agents, and runs on markdown and YAML files inside the repository itself.

### config.yaml: the project contract

Everything starts with `openspec/config.yaml`. This file defines the complete project: which repositories exist, which artefact types are generated, which rules apply, and which conventions are followed.

```yaml
schema: spec-driven

repositories:
  backend:
    path: ~/work/project/backend
    type: symfony
    has_claude_md: true
    has_airules: true
    test_command: make test-unit
    branch_base: develop
    worktree_script: ./scripts/create-worktree.sh
  frontend:
    path: ~/work/project/frontend
    type: nuxt
    has_claude_md: true
    has_airules: false
    test_command: npm run test
    branch_base: develop

rules:
  proposal:
    - Every capability MUST be independent and separately deployable
    - Include an impact analysis on existing code
  specs:
    - Use MUST/SHALL for mandatory requirements
    - Every requirement MUST have at least one WHEN/THEN scenario
  design:
    - Follow strict DDD: Domain → Application → Infrastructure
    - Include the affected folder structure
  tasks:
    - Decompose by repository and phase
    - Order: Domain → Application → Infrastructure → Permissions → Translations → Tests

internal_packages:
  - name: shared-kernel
    path: packages/shared-kernel
  - name: audit-bundle
    path: packages/audit-bundle
```

What makes this file powerful is that the agent reads it before doing anything. It knows where each repository is, what type of project it is, how to run the tests, and what the rules are for each artefact. It does not have to invent anything.

### The structure of a change

Every feature or change lives in its own folder inside `openspec/changes/`:

```
openspec/
├── config.yaml
├── changes/
│   └── attendance-tracking/
│       ├── 01-proposal.md
│       ├── 02-specs/
│       │   ├── capability-1-auditory-attendance.md
│       │   ├── capability-2-block-scheduling.md
│       │   └── capability-3-remedial-tracking.md
│       ├── 03-design/
│       │   ├── capability-1-auditory-attendance.md
│       │   ├── capability-2-block-scheduling.md
│       │   └── capability-3-remedial-tracking.md
│       ├── 04-tasks/
│       │   ├── capability-1-auditory-attendance.md
│       │   ├── capability-2-block-scheduling.md
│       │   └── capability-3-remedial-tracking.md
│       └── 05-verify.md
└── specs/
    └── main/
        └── attendance-tracking.md
```

The artefacts are progressive: the proposal generates the specs, the specs generate the design, the design generates the tasks. Each one references the previous. And at the end, the consolidated spec moves to `specs/main/` as the project's permanent source of truth.

A real example: a Jira ticket with one paragraph of requirements turned into 7 capabilities, each with its spec, its design and its tasks. More than 200 tasks organised by repository and phase. All generated by the agent from that ticket, but structured and verifiable.

## Anatomy of each artefact

### Proposal: the "why" and the "what changes"

The proposal is the first artefact. It defines the scope of the change, the independent capabilities, the impact on existing code, and the high-level decisions.

```markdown
# Proposal: Attendance Tracking System

## Context
The current system records attendance at block level, but does not allow
breaking it down by submodule or managing REMEDIAL blocks with their
specific recovery rules.

## Capabilities

### Capability 1: Auditory Attendance Records
Create detailed attendance records per audit, broken down by
SubjectMatter, linked to the block and to the parent submodule.

### Capability 2: Block Scheduling Rules
Scheduling rules that determine which blocks can coexist in the
same time slot, with conflict validation.

### Capability 3: Remedial Block Tracking
Specific management of REMEDIAL blocks: linkage to source blocks,
per-student recovery tracking, and automatic closure when the
conditions are met.

## Impact
- **Backend**: New entities, services and endpoints in the Scheduling bounded context
- **Frontend**: New UI module for detailed attendance management
- **Migrations**: 3 new tables, 2 columns added to existing tables

## Decisions
- Each capability is independent and separately deployable
- The existing AuditBundle is reused for change tracking
- REMEDIAL blocks keep a reference to the source block but do not depend on it to function
```

The proposal forces you to think before acting. It is not bureaucracy — it is the difference between having a plan and having hope.

### Specs: verifiable requirements

Specs are the heart of SDD. Each capability has its spec file with requirements expressed in formal language (MUST/SHALL) and concrete scenarios in WHEN/THEN format.

```markdown
# Spec: Auditory Attendance Records

## Requirement: SetAuditoryAttendanceService MUST create detailed records for REMEDIAL blocks

### Scenario: REMEDIAL block finish with submodule recovery
- **WHEN** a REMEDIAL block is finished
- **AND** the block has linked attendances
- **THEN** one record MUST be created for each unique SubjectMatter
- **AND** each record MUST reference the parent submodule
- **AND** the record status MUST be PENDING until tutor validation

### Scenario: REMEDIAL block without attendances
- **WHEN** a REMEDIAL block is finished
- **AND** the block has NO linked attendances
- **THEN** a single record MUST be created with status NO_DATA
- **AND** the coordinator MUST be notified via a domain event

## Requirement: Attendance records MUST be immutable after tutor validation

### Scenario: Attempt to modify a validated record
- **WHEN** a record has status VALIDATED
- **AND** a user attempts to modify any field
- **THEN** the system MUST throw AttendanceRecordAlreadyValidatedException
- **AND** the change MUST NOT be persisted

## Requirement: Each record SHALL reference exactly one SubjectMatter

### Scenario: Multiple SubjectMatters in the same block
- **WHEN** a block contains 3 different SubjectMatters
- **THEN** exactly 3 attendance records MUST be created
- **AND** each record MUST reference a different SubjectMatter
```

Notice what the specs achieve: they leave no room for interpretation. The agent cannot invent what to do when a REMEDIAL block has no attendances — the spec tells it. It cannot decide whether a validated record is editable — the spec tells it. Every scenario is a test that can be verified.

The MUST/SHALL language is not a whim — it comes from the RFCs. When an agent reads "MUST", it knows there is no negotiation.

### Design: the architecture decisions

The design translates the spec requirements into concrete implementation decisions. DDD structure, affected folders, migrations, permissions.

```markdown
# Design: Auditory Attendance Records

## Bounded Context
Scheduling (existing)

## Domain Layer

### Entities
- `AuditoryAttendanceRecord` (Aggregate Root)
  - id: AuditoryAttendanceRecordId (VO)
  - blockId: BlockId (VO)
  - subjectMatterId: SubjectMatterId (VO)
  - parentSubmoduleId: SubmoduleId (VO)
  - status: AttendanceRecordStatus (Enum: PENDING, VALIDATED, NO_DATA)
  - createdAt: DateTimeImmutable
  - validatedAt: ?DateTimeImmutable
  - validatedBy: ?UserId

### Value Objects
- AuditoryAttendanceRecordId (UuidValueObject)
- AttendanceRecordStatus (StringEnum)

### Domain Events
- AuditoryAttendanceRecordCreated
- AuditoryAttendanceRecordValidated
- NoAttendanceDataDetected

### Repository Interface
- AuditoryAttendanceRecordRepository
  - save(AuditoryAttendanceRecord): void
  - findByBlock(BlockId): array
  - findBySubjectMatter(SubjectMatterId): array

## Application Layer

### Commands
- CreateAuditoryAttendanceRecordCommand
- ValidateAuditoryAttendanceRecordCommand

### Queries
- FindAuditoryAttendanceRecordsByBlockQuery

## Infrastructure
- Migration: Version20260301_create_auditory_attendance_record_table
- Full DDD structure: Domain/ → Application/ → Infrastructure/
```

The design is the blueprint the agent follows to the letter. It does not decide where to put the files or how to name the entities — the design tells it. And because it derives from the specs, there is full traceability from requirement to folder.

### Tasks: decomposition by repo and phase

Tasks are the last layer before implementation. They break the design into atomic units of work, organised by repository and phase.

```markdown
# Tasks: Auditory Attendance Records — Backend

### Phase 1: Domain
- [ ] Create AuditoryAttendanceRecordId (UuidValueObject)
- [ ] Create AuditoryAttendanceRecord (Aggregate Root with invariants)
- [ ] Create domain events: Created, Validated, NoDataDetected

### Phase 2: Application
- [ ] Create CreateAuditoryAttendanceRecordCommand + Handler
- [ ] Create SetAuditoryAttendanceService (orchestrates the creation logic)

### Phase 3: Infrastructure
- [ ] Migration + DoctrineRepository + Controller + DI

### Phase 4: Permissions and Translations
### Phase 5: Unit + integration tests
### Phase 6: API testing & docs
```

The order is not arbitrary. Domain comes first because everything depends on it. Each phase builds on the previous one until the tests verify everything and the documentation closes the cycle.

When the agent receives these tasks, it does not have to think about order or dependencies. It just executes, phase by phase, task by task.

## The command that orchestrates it all

OpenSpec is not just a folder structure — it is a set of slash commands in Claude Code that automate the whole flow.

### /lm:new — From ticket to proposal

```
/lm:new PROJ-1234
```

This command kicks off the full cycle. It reads the Jira ticket (title, description, comments, attachments), analyses the existing code across the configured repositories in parallel, and generates the initial proposal.

It does not generate a generic proposal. It analyses existing entities, project conventions and patterns already in use, and proposes something coherent with what is there. If the project already has an `AuditBundle`, the proposal reuses it instead of inventing another one.

### /lm:spec — From proposal to specs

```
/lm:spec
```

It takes the approved proposal and generates the specs for each capability. This is where things get serious: every requirement with MUST/SHALL, every scenario with WHEN/THEN, every edge case documented.

I review the specs before approving them. It is the most important moment of the whole process, because everything that follows starts here. If a spec is wrong, the agent will implement the wrong spec — perfectly.

### /lm:implement — The agent pipeline

```
/lm:implement
```

This is the command that triggers implementation. It reads the generated tasks and launches the agent pipeline. In a setup with [Agent Teams](/en/claude-code-agent-teams/), each phase can run in parallel wherever possible.

The agent follows the tasks to the letter. It does not improvise. It does not skip steps. If the task says "create AuditoryAttendanceRecordId as a UuidValueObject", that is exactly what it does. The agent's creativity applies to *how* to implement, not to *what* to implement. The *what* is already decided.

### /lm:jira — Documentation back to Jira

```
/lm:jira
```

It generates the QA documentation and publishes it as a comment on the Jira ticket: endpoints, payloads, permissions, test scenarios. The ticket is both origin and destination — no Google Docs lost somewhere in a Drive.

## Rules: the project's memory

Rules are not documentation. They are scars. Every rule was born from a mistake an agent made, that I corrected, and that I turned into a rule so it would not happen again.

On one project's backend I have 38 accumulated rules. DDD patterns, naming conventions, testing rules, Doctrine quirks, security constraints. Each with a reason to exist.

Some examples:

```markdown
# Rule: Value Objects always with a named constructor
Use `::create()` as the named constructor in every Value Object.
Do not use `new` directly outside the class itself.

# Rule: Translations - key format
Translation keys follow the pattern: `{bounded_context}.{entity}.{field}`
Example: `scheduling.auditory_attendance.status.pending`

# Rule: Tests - one test per spec scenario
Every WHEN/THEN scenario in the spec MUST have at least one unit test.
The test name MUST reflect the scenario: `testItShouldCreateRecordForEachSubjectMatter`
```

Rules load automatically. When the agent starts a session, it reads the project's `CLAUDE.md` and the associated rules. It does not have to remember that Value Objects use `::create()` — the rule tells it every session.

The process is organic: the agent makes a mistake → I correct it → I create the rule → never again. Over time, the agent becomes increasingly precise on that specific project. Not because it learns (it does not, between sessions), but because the rules accumulate the knowledge it loses.

## Precommit: the last line of defence

However good the specs and rules are, something can always slip through. The precommit hook is the last line of defence before code reaches the repository.

### The hook

The precommit runs two validations over the staged files:

**ECS (Easy Coding Standard)**: validates and automatically fixes code style. If the agent generates a file with an unused `use` or an extra space, ECS fixes it before the commit.

**Translation validation**: this is the script that has saved me the most trouble. It scans every staged PHP and Twig file, extracts the `->translate()` calls, obtains the keys, and validates them against the YAML translation files for each language.

If a translation key is missing in any configured language, the commit is blocked. No negotiation. An untranslated key is a production bug — text that shows up as `scheduling.auditory_attendance.status.pending` instead of "Pending".

### The make commands

Around this hook there are several make commands: `make precommit` runs ECS plus validation on staged files (what the hook runs automatically), `make check-translations-diff` validates only the files changed against the base branch (fast for day-to-day work), `make analyse` runs full PHPStan plus ECS, and `make test-mutation` for mutation testing — if a test passes even when you mutate the code, it is a test that proves nothing real.

When the agent implements, I ask it to run `make precommit` before committing. If it fails, it fixes it. If it fixes it and it still fails, I review it myself. But the vast majority of the time the agent resolves it alone, because the rules tell it how to name translation keys and the precommit validates that it did so correctly.

## Verification and spec sync

When implementation is complete, the cycle does not end. It has to be verified and synchronised.

### /lm:finish — Closing the cycle

```
/lm:finish
```

This command runs three steps:

**Verify**: compares the implementation against the specs. Every MUST/SHALL requirement is validated against the generated code. Every WHEN/THEN scenario is cross-checked with the tests. If something is missing, verify catches it.

**Sync**: the capability's specs are consolidated and moved to `openspec/specs/main/`. That directory is the project's source of truth — it holds the up-to-date spec for each feature, alive and maintainable.

**Archive**: the change artefacts (proposal, per-capability specs, design, tasks) are archived. They serve as history, but the truth lives in `specs/main/`.

### The main specs as project truth

The specs in `specs/main/` are not static. Every future change affecting a feature updates its spec. If tomorrow I change the rules for REMEDIAL blocks, the attendance tracking spec is updated. It is not optional — it is part of the process. That solves the classic problem of stale documentation.

## When SDD makes sense (and when it does not)

### Yes: when the context justifies it

- **Teams using agents**: if your developers work with Claude Code or other agents, SDD multiplies the quality of the output. Specs are the best prompt you can write.
- **Multi-repo projects**: when a change touches backend, frontend and perhaps a shared package, having tasks organised by repository is the difference between coordination and chaos.
- **Complex features**: any feature with more than 2-3 business scenarios benefits from formal specs. The cost of writing them is recovered during implementation.
- **Teams with turnover**: specs are instant onboarding.

### No: when the overhead does not pay off

- **One-line hotfixes**: if the fix is changing a value in a constant, you do not need a 3-page proposal. Common sense.
- **Throwaway prototypes**: if you are exploring an idea and you know the code will be thrown away, vibe coding is the right path.
- **Trivial changes**: updating a dependency, fixing a typo, adjusting a CSS style. Not everything needs a specification.

The practical rule I use: if the change touches more than 5 files or has more than 2 business scenarios, I write a spec. If not, I implement directly. You adjust the threshold to your project and your risk tolerance.

### The real cost of specs

Writing the specs for a complex feature takes me between 30 and 60 minutes. Implementation with an agent, with specs, takes between 1 and 2 hours for something that would take 2 or 3 days manually.

Without specs, implementation with an agent would take maybe 3-4 hours, but with more errors, more back and forth, and more review afterwards. The cost of the specs pays for itself in the first implementation.

And there is an invisible benefit: the specs remain. The next time someone touches that feature — a colleague, an agent, or me six months from now — the specs are there, up to date, explaining what the code does and why.

## Conclusion

SDD is not bureaucracy — it is giving the agent the context it needs to do its job well. The spec is the most important prompt you will write.

I have gone from guiding agents step by step, correcting mistakes and repeating context between sessions, to handing over a spec and receiving an implementation I can merge with confidence. Not always perfect, but always coherent with what I asked for.

The full flow — from Jira ticket to production code by way of verifiable specs — looks like a lot of work the first time. But once you have the config.yaml, the accumulated rules and the slash commands configured, the process flows. The agent does the heavy lifting. You make the important decisions.

And that, in the end, is what development with AI should be: you decide what to build and why. The agent decides how, and executes it. The spec is the bridge between the two.

---

**P.S.**: If you are trying SDD or have your own specification system for agents, you can find me on Twitter as [@lm_martinbar](https://x.com/lm_martinbar). And if you want to cut what those agents cost you, start with [how I reduced Claude Code token usage by 80%](/en/claude-code-token-optimization/).