SDD con OpenSpec: Cómo Mis Agentes Escriben Código de Producción
Llevo meses dejando que agentes de IA escriban código de producción. No en side projects — en aplicaciones con usuarios reales, equipos, y despliegues a producción. Y funciona. Pero no porque la IA sea mágica, sino porque le doy algo que la mayoría no le da: una especificación.
He escrito sobre cómo uso Claude Code en mi día a día y sobre cómo montar Agent Teams para desarrollos complejos. Pero me faltaba contar la pieza que hace que todo funcione: el sistema de especificaciones que alimenta a esos agentes.
Se llama SDD — Specification-Driven Development. Y el framework que lo implementa es OpenSpec.
El problema que nadie quiere ver
Hay un término que se ha puesto de moda: vibe coding. Le dices al agente “hazme un CRUD de usuarios” y te genera algo que funciona. Más o menos. Para un prototipo, perfecto. Para producción, es una bomba de relojería.
El problema es sutil. Cuando le pides algo vago a un agente, el agente no pregunta — inventa. Inventa nombres de campos, inventa relaciones entre entidades, inventa reglas de negocio. Y lo hace con una confianza absoluta que te hace pensar que lo ha entendido todo. Pero no ha entendido nada porque no había nada que entender.
La diferencia entre “hazme un CRUD” y una especificación real es la diferencia entre un boceto en una servilleta y un plano de arquitectura. Los dos te dan una idea del edificio. Solo uno te permite construirlo.
Sin spec, el agente inventa. Con spec, el agente ejecuta. Esa es la línea que separa el vibe coding del desarrollo profesional con IA.
Y no hablo de documentación. A nadie le gusta escribir documentación y a nadie le gusta leerla. Hablo de un artefacto que le dice al agente exactamente qué construir, con qué restricciones, y por qué. Un contrato técnico que tanto tú como el agente podéis verificar.
Qué es SDD
SDD — Specification-Driven Development — es un enfoque donde las especificaciones no documentan lo que se ha hecho, sino que gatean lo que se va a hacer. La spec va primero. La implementación va después. Y la spec bloquea la implementación hasta que está aprobada.
El flujo es lineal y cada fase bloquea la siguiente:
Proposal → Specs → Design → Tasks → Implementation → Verify
No puedes implementar sin tasks. No puedes crear tasks sin design. No puedes diseñar sin specs. No puedes escribir specs sin un proposal aprobado. Cada fase es un checkpoint que asegura que lo que viene después tiene sentido.
Esto no es burocracia. Es que he visto demasiadas veces a un agente implementar algo que no tenía sentido porque nadie se paró a pensar si la propuesta era correcta. Con SDD, ese momento de “espera, esto no es lo que queríamos” ocurre en la fase de proposal, cuando cambiar de dirección cuesta cero. No en la fase de implementación, cuando ya hay 40 archivos tocados.
La spec no es documentación — es un contrato que los agentes consumen. Esa diferencia lo cambia todo. La documentación se escribe para humanos y se queda obsoleta al día siguiente. La spec se escribe para que un agente la ejecute y se verifica contra el resultado.
OpenSpec: el framework
OpenSpec es el framework que implementa SDD. Es opinado, está pensado para proyectos multi-repo con agentes de IA, y funciona sobre ficheros markdown y YAML dentro del propio repositorio.
config.yaml: el contrato del proyecto
Todo empieza con openspec/config.yaml. Este fichero define el proyecto completo: qué repositorios hay, qué tipo de artefactos se generan, qué reglas aplican, y qué convenciones se siguen.
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:
- Cada capability DEBE ser independiente y desplegable por separado
- Incluir análisis de impacto en código existente
specs:
- Usar MUST/SHALL para requisitos obligatorios
- Cada requisito DEBE tener al menos un escenario WHEN/THEN
design:
- Seguir DDD estricto: Domain → Application → Infrastructure
- Incluir estructura de carpetas afectadas
tasks:
- Descomponer por repositorio y fase
- Orden: Domain → Application → Infrastructure → Permisos → Traducciones → Tests
internal_packages:
- name: shared-kernel
path: packages/shared-kernel
- name: audit-bundle
path: packages/audit-bundle
Lo que hace poderoso a este fichero es que el agente lo lee antes de hacer cualquier cosa. Sabe dónde está cada repositorio, qué tipo de proyecto es, cómo correr los tests, y cuáles son las reglas para cada artefacto. No tiene que inventar nada.
Estructura de un cambio
Cada feature o cambio vive en su propia carpeta dentro de 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
Los artefactos son progresivos: el proposal genera las specs, las specs generan el design, el design genera las tasks. Cada uno referencia al anterior. Y al final, la spec consolidada se mueve a specs/main/ como fuente de verdad permanente del proyecto.
Un ejemplo real: un ticket de Jira con un párrafo de requisitos se convirtió en 7 capabilities, cada una con su spec, su design, y sus tasks. Más de 200 tasks organizadas por repositorio y fase. Todo generado por el agente a partir de ese ticket, pero estructurado y verificable.
Anatomía de cada artefacto
Proposal: el “por qué” y el “qué cambia”
El proposal es el primer artefacto. Define el alcance del cambio, las capabilities independientes, el impacto en código existente, y las decisiones de alto nivel.
# Proposal: Attendance Tracking System
## Contexto
El sistema actual registra asistencia a nivel de bloque, pero no permite
desglosar por submódulo ni gestionar bloques de tipo REMEDIAL con sus
reglas específicas de recuperación.
## Capabilities
### Capability 1: Auditory Attendance Records
Crear registros detallados de asistencia por auditoría, desglosados por
SubjectMatter, vinculados al bloque y al submódulo padre.
### Capability 2: Block Scheduling Rules
Reglas de programación que determinan qué bloques pueden coexistir en
un mismo slot temporal, con validaciones de conflicto.
### Capability 3: Remedial Block Tracking
Gestión específica de bloques REMEDIAL: vinculación con bloques origen,
tracking de recuperación por alumno, y cierre automático cuando se
cumplen las condiciones.
## Impacto
- **Backend**: Nuevas entidades, servicios, y endpoints en el bounded context de Scheduling
- **Frontend**: Nuevo módulo de UI para gestión de asistencia detallada
- **Migraciones**: 3 tablas nuevas, 2 columnas añadidas a tablas existentes
## Decisiones
- Cada capability es independiente y desplegable por separado
- Se reutiliza el AuditBundle existente para el registro de cambios
- Los bloques REMEDIAL mantienen referencia al bloque origen pero no dependen de él para funcionar
El proposal fuerza a pensar antes de hacer. No es burocracia — es la diferencia entre tener un plan y tener esperanza.
Specs: requisitos verificables
Las specs son el corazón de SDD. Cada capability tiene su fichero de specs con requisitos expresados en lenguaje formal (MUST/SHALL) y escenarios concretos en formato WHEN/THEN.
# Spec: Auditory Attendance Records
## Requirement: SetAuditoryAttendanceService MUST create detailed records for REMEDIAL blocks
### Scenario: REMEDIAL block finish with submodule recovery
- **WHEN** se finaliza un bloque REMEDIAL
- **AND** el bloque tiene attendances vinculadas
- **THEN** se DEBE crear un registro por cada SubjectMatter único
- **AND** cada registro DEBE tener el submodule padre como referencia
- **AND** el status del registro DEBE ser PENDING hasta validación del tutor
### Scenario: REMEDIAL block without attendances
- **WHEN** se finaliza un bloque REMEDIAL
- **AND** el bloque NO tiene attendances vinculadas
- **THEN** se DEBE crear un único registro con status NO_DATA
- **AND** se DEBE notificar al coordinador vía evento de dominio
## Requirement: Attendance records MUST be immutable after tutor validation
### Scenario: Attempt to modify validated record
- **WHEN** un registro tiene status VALIDATED
- **AND** un usuario intenta modificar cualquier campo
- **THEN** el sistema DEBE lanzar AttendanceRecordAlreadyValidatedException
- **AND** el cambio NO DEBE persistirse
## Requirement: Each record SHALL reference exactly one SubjectMatter
### Scenario: Multiple SubjectMatters in same block
- **WHEN** un bloque contiene 3 SubjectMatters distintos
- **THEN** se DEBEN crear exactamente 3 registros de attendance
- **AND** cada registro DEBE referenciar un SubjectMatter diferente
Fíjate en lo que consiguen las specs: no dejan margen de interpretación. El agente no puede inventar qué hacer cuando un bloque REMEDIAL no tiene attendances — la spec se lo dice. No puede decidir si un registro validado es editable — la spec se lo dice. Cada escenario es un test que se puede verificar.
El lenguaje MUST/SHALL no es capricho — viene de los RFC. Cuando un agente lee “MUST”, sabe que no hay negociación posible.
Design: las decisiones de arquitectura
El design traduce los requisitos de la spec en decisiones concretas de implementación. Estructura DDD, carpetas afectadas, migraciones, permisos.
# Design: Auditory Attendance Records
## Bounded Context
Scheduling (existente)
## 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
- Migración: Version20260301_create_auditory_attendance_record_table
- Estructura DDD completa: Domain/ → Application/ → Infrastructure/
El design es el plano que el agente sigue al pie de la letra. No decide dónde poner los archivos ni cómo nombrar las entidades — el design se lo dice. Y como parte de las specs, hay trazabilidad completa desde el requisito hasta la carpeta.
Tasks: la descomposición por repo y fase
Las tasks son la última capa antes de la implementación. Descomponen el design en unidades de trabajo atómicas, organizadas por repositorio y fase.
# Tasks: Auditory Attendance Records — Backend
### Fase 1: Domain
- [ ] Crear AuditoryAttendanceRecordId (UuidValueObject)
- [ ] Crear AuditoryAttendanceRecord (Aggregate Root con invariantes)
- [ ] Crear domain events: Created, Validated, NoDataDetected
### Fase 2: Application
- [ ] Crear CreateAuditoryAttendanceRecordCommand + Handler
- [ ] Crear SetAuditoryAttendanceService (orquesta la lógica de creación)
### Fase 3: Infrastructure
- [ ] Migración + DoctrineRepository + Controller + DI
### Fase 4: Permisos y Traducciones
### Fase 5: Tests unitarios + integración
### Fase 6: API Testing & Docs
El orden no es arbitrario. Domain va primero porque todo depende de él. Cada fase construye sobre la anterior hasta que los tests verifican todo y la documentación cierra el ciclo.
Cuando el agente recibe estas tasks, no tiene que pensar en el orden ni en las dependencias. Solo ejecutar, fase por fase, task por task.
El comando que lo orquesta todo
OpenSpec no es solo una estructura de carpetas — es un conjunto de slash commands en Claude Code que automatizan todo el flujo.
/lm:new — De ticket a proposal
/lm:new PROJ-1234
Este comando arranca el ciclo completo. Lee el ticket de Jira (título, descripción, comentarios, attachments), analiza el código existente en los repositorios configurados en paralelo, y genera el proposal inicial.
No genera un proposal genérico. Analiza las entidades existentes, las convenciones del proyecto, los patrones que ya se usan, y propone algo coherente con lo que hay. Si el proyecto ya tiene un AuditBundle, el proposal lo reutiliza en lugar de inventar otro.
/lm:spec — De proposal a specs
/lm:spec
Toma el proposal aprobado y genera las specs de cada capability. Aquí es donde la cosa se pone seria: cada requisito con MUST/SHALL, cada escenario con WHEN/THEN, cada edge case documentado.
Yo reviso las specs antes de aprobarlas. Es el momento más importante de todo el proceso, porque todo lo que viene después parte de aquí. Si una spec está mal, el agente implementará la spec mal — perfectamente.
/lm:implement — El pipeline de agentes
/lm:implement
Este es el comando que dispara la implementación. Lee las tasks generadas y lanza el pipeline de agentes. En un setup con Agent Teams, cada fase puede ejecutarse en paralelo donde sea posible.
El agente sigue las tasks al pie de la letra. No improvisa. No se salta pasos. Si la task dice “crear AuditoryAttendanceRecordId como UuidValueObject”, eso es exactamente lo que hace. La creatividad del agente se aplica al cómo implementar, no al qué implementar. El qué ya está decidido.
/lm:jira — Documentación de vuelta a Jira
/lm:jira
Genera la documentación de QA y la publica como comentario en el ticket de Jira: endpoints, payloads, permisos, escenarios de prueba. El ticket es el origen y el destino — nada de Google Docs perdidos en algún Drive.
Rules: la memoria del proyecto
Las rules no son documentación. Son cicatrices. Cada rule nació de un error que un agente cometió, que yo corregí, y que convertí en una regla para que no volviera a pasar.
En el backend de un proyecto, tengo 38 reglas acumuladas. Patrones DDD, convenciones de naming, reglas de testing, peculiaridades de Doctrine, restricciones de seguridad. Cada una con una razón de ser.
Algunos ejemplos:
# Rule: Value Objects siempre con named constructor
Usar `::create()` como named constructor en todos los Value Objects.
No usar `new` directamente fuera de la propia clase.
# Rule: Traducciones - formato de keys
Las keys de traducción siguen el patrón: `{bounded_context}.{entity}.{field}`
Ejemplo: `scheduling.auditory_attendance.status.pending`
# Rule: Tests - un test por escenario de spec
Cada escenario WHEN/THEN de la spec DEBE tener al menos un test unitario.
El nombre del test DEBE reflejar el escenario: `testItShouldCreateRecordForEachSubjectMatter`
Las rules se cargan automáticamente. Cuando el agente empieza una sesión, lee el CLAUDE.md del proyecto y las rules asociadas. No tiene que recordar que los Value Objects usan ::create() — la rule se lo dice en cada sesión.
El proceso es orgánico: el agente comete un error → yo lo corrijo → creo la rule → nunca más. Con el tiempo, el agente se vuelve cada vez más preciso en ese proyecto específico. No porque aprenda (no lo hace entre sesiones), sino porque las rules acumulan el conocimiento que él pierde.
Precommit: la última línea de defensa
Por muy buenas que sean las specs y las rules, siempre puede colarse algo. El precommit hook es la última línea de defensa antes de que el código llegue al repositorio.
El hook
El precommit ejecuta dos validaciones sobre los ficheros staged:
ECS (Easy Coding Standard): Valida y corrige automáticamente el estilo de código. Si el agente genera un fichero con un use sin usar o un espacio de más, ECS lo arregla antes del commit.
Validación de traducciones: Este es el script que más problemas me ha ahorrado. Escanea todos los ficheros PHP y Twig staged, extrae las llamadas a ->translate(), obtiene las keys, y las valida contra los ficheros YAML de traducción de cada idioma.
Si falta una key de traducción en cualquiera de los idiomas configurados, el commit se bloquea. No hay negociación. Una key sin traducir es un bug en producción — un texto que aparece como scheduling.auditory_attendance.status.pending en lugar de “Pendiente”.
Los comandos make
Alrededor de este hook hay varios comandos make: make precommit ejecuta ECS + validación sobre ficheros staged (lo que corre el hook automáticamente), make check-translations-diff valida solo los ficheros que han cambiado respecto a la rama base (rápido para el día a día), make analyse lanza PHPStan + ECS completo, y make test-mutation para mutation testing — si un test pasa incluso cuando mutas el código, es un test que no prueba nada real.
Cuando el agente implementa, le pido que ejecute make precommit antes de hacer el commit. Si falla, corrige. Si corrige y sigue fallando, reviso yo. Pero la inmensa mayoría de las veces, el agente lo resuelve solo porque las rules le dicen cómo nombrar las keys de traducción y el precommit valida que lo haya hecho bien.
Verificación y sync de specs
Cuando la implementación está completa, el ciclo no termina. Hay que verificar y sincronizar.
/lm:finish — Cerrar el ciclo
/lm:finish
Este comando ejecuta tres pasos:
Verify: Compara la implementación contra las specs. Cada requisito MUST/SHALL se valida contra el código generado. Cada escenario WHEN/THEN se cruza con los tests. Si falta algo, el verify lo detecta.
Sync: Las specs de la capability se consolidan y se mueven a openspec/specs/main/. Este directorio es la fuente de verdad del proyecto — contiene la spec actualizada de cada feature, viva y mantenible.
Archive: Los artefactos del cambio (proposal, specs por capability, design, tasks) se archivan. Sirven como histórico pero la verdad está en specs/main/.
Las specs principales como verdad del proyecto
Las specs en specs/main/ no son estáticas. Cada cambio futuro que afecta una feature actualiza su spec. Si mañana cambio las reglas de los bloques REMEDIAL, la spec de attendance tracking se actualiza. No es opcional — es parte del proceso. Eso resuelve el problema clásico de la documentación obsoleta.
Cuándo tiene sentido SDD (y cuándo no)
Sí: cuando el contexto lo justifica
- Equipos que usan agentes: Si tus desarrolladores trabajan con Claude Code u otros agentes, SDD multiplica la calidad del output. Las specs son el mejor prompt que puedes escribir.
- Proyectos multi-repo: Cuando un cambio toca backend, frontend, y quizá un paquete compartido, tener tasks organizadas por repositorio es la diferencia entre coordinación y caos.
- Features complejas: Cualquier feature que tenga más de 2-3 escenarios de negocio se beneficia de specs formales. El coste de escribir las specs se recupera en la implementación.
- Equipos con rotación: Las specs son onboarding instantáneo.
No: cuando el overhead no compensa
- Hotfixes de una línea: Si el fix es cambiar un valor en una constante, no necesitas un proposal de 3 páginas. Sentido común.
- Prototipos desechables: Si estás explorando una idea y sabes que el código se va a tirar, vibe coding es el camino correcto.
- Cambios triviales: Actualizar una dependencia, corregir un typo, ajustar un estilo CSS. No todo necesita especificación.
La regla práctica que uso: si el cambio toca más de 5 archivos o tiene más de 2 escenarios de negocio, escribo spec. Si no, implemento directo. El umbral lo ajustas según tu proyecto y tu tolerancia al riesgo.
El coste real de las specs
Escribir las specs de una feature compleja me lleva entre 30 y 60 minutos. La implementación con agente, con specs, me lleva entre 1 y 2 horas para algo que manualmente serían 2 o 3 días.
Sin specs, la implementación con agente me llevaría quizá 3-4 horas, pero con más errores, más ida y vuelta, y más revisión posterior. El coste de las specs se paga solo en la primera implementación.
Y hay un beneficio invisible: las specs quedan. La próxima vez que alguien toque esa feature — un compañero, un agente, yo mismo dentro de 6 meses — las specs están ahí, actualizadas, explicando qué hace el código y por qué.
Conclusión
SDD no es burocracia — es darle al agente el contexto que necesita para hacer bien su trabajo. La spec es el prompt más importante que vas a escribir.
He pasado de guiar agentes paso a paso, corrigiendo errores, repitiendo contexto entre sesiones, a darle una spec y recibir una implementación que puedo mergear con confianza. No siempre perfecta, pero siempre coherente con lo que pedí.
El flujo completo — de ticket de Jira a código en producción pasando por specs verificables — parece mucho trabajo la primera vez. Pero una vez que tienes el config.yaml, las rules acumuladas, y los slash commands configurados, el proceso es fluido. El agente hace el trabajo pesado. Tú tomas las decisiones importantes.
Y eso, al final, es lo que debería ser el desarrollo con IA: tú decides qué construir y por qué. El agente decide cómo y lo ejecuta. La spec es el puente entre los dos.
P.D.: Si estás probando SDD o tienes tu propio sistema de especificaciones para agentes, me encuentras en Twitter como @lmmartinb. Y si todavía no usas Claude Code, empieza por mi guía de consejos de uso diario.
¿Te ha gustado este artículo?
Explora más artículos sobre desarrollo, buenas prácticas y herramientas.