Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.
08/06/2026 | Desarrollo de aplicaciones,Desarrollo de software,Desarrollo Full-stack,General
Desarrollo dirigido por especificaciones con Claude Code y OpenSpec: del spec al CI
Este artículo se ha redactado con la ayuda de IA. El proyecto descrito está basado en hechos reales, si bien algunos detalles se han modificado u omitido para preservar su confidencialidad. Los fragmentos de código son ilustrativos y se han simplificado para el formato de blog, no son necesariamente funcionales.
TL;DR
La conclusión, por si no llegas al final: en el desarrollo dirigido por especificaciones (spec-driven development), la diferencia entre que un agente de codificación te entregue un juguete o un producto no está en el modelo, está en la especificación que le das. Cuando alimentamos a Claude Code con una especificación bien escrita y revisada usando el framework OpenSpec, el resultado fue una aplicación web completa, con alta cobertura de código, ejecutándose sobre contenedores y verificada en el CI. Y lo más revelador: no hizo falta artillería pesada. Lo contamos abajo.
El caso: modernizar un sistema legacy de inscripciones con un agente de IA
El proyecto reemplazaba un sistema legacy de inscripción en actividades extraescolares —una web montada sobre tecnologías obsoletas que se convertía en un cuello de botella y un problema de mantenimiento— por una aplicación moderna: una SPA responsive para que las familias se registren, exploren el catálogo de actividades e inscriban a sus hijos, respaldada por una API y una base de datos que persista todo de forma fiable. La Asociación de Familias de Alumn@s (AFA) del colegio era el cliente y stakeholder principal del proyecto.
El stack era deliberadamente convencional: TypeScript de extremo a extremo, NestJS para la API, Angular con Ionic Framework para la SPA y PostgreSQL para la persistencia. La elección de tecnología no es casual; volveremos a ello.
OpenSpec: la especificación como contrato, no como documentación muerta
Antes de escribir una sola línea de código de aplicación, el trabajo fue especificar. Con OpenSpec, un cambio se organiza como una cadena de artefactos con dependencias explícitas: proposal → specs → design → tasks → implement. La propuesta explica el porqué; las specs describen el comportamiento esperado; el diseño baja al cómo; y las tasks son la lista que el agente irá completando (más información del workflow aquí).
Las specs no son prosa ambigua. OpenSpec favorece escenarios verificables en formato Given/When/Then (similar a Gherkin de Cucumber), que se leen igual de bien por un humano que por un agente:
## Inscripción en una actividad #### Scenario: La actividad tiene plazas - GIVEN una actividad con cupo de 20 y 19 inscripciones - WHEN una familia inscribe a su hijo - THEN la inscripción se confirma y el cupo pasa a 20/20 #### Scenario: La actividad está completa - GIVEN una actividad con cupo de 20 y 20 inscripciones - WHEN una familia intenta inscribir a su hijo - THEN el sistema rechaza la inscripción con un error de "sin plazas"
Esos documentos se redactaron con ayuda de la IA a partir de notas del proyecto y de las transcripciones de las reuniones con el equipo de la AFA —quienes conocían el flujo real y todas sus excepciones—, pero, además de varias revisiones de contraste, pasaron por una revisión manual. La IA acelera; la persona decide. Ese filtro humano es lo que convierte un borrador plausible en un contrato fiable.
Una pieza importante es el fichero de configuración openspec/config.yaml, que inyecta contexto y reglas en las instrucciones de cada artefacto. Ahí vive la «definición de hecho» del proyecto, escrita una sola vez:
# openspec/config.yaml
schema: spec-driven
context: |
Stack: TypeScript, NestJS (API), Angular + Ionic (SPA), PostgreSQL.
Testing: Jest (unit) + Playwright (E2E).
Definición de "hecho": cobertura de unit tests ≥ 80%;
E2E de los happy paths; todo ejecuta sobre Docker en el CI.
rules:
design:
- Respetar el diseño aportado (PDF en directorio /design).
tasks:
- Ninguna tarea se da por terminada sin sus tests.
Gracias a esto, el listón de calidad no era algo que hubiera que recordar en cada turno: era parte del contrato. El agente sabía, en cada tarea, que «hecho» incluía tests y cobertura.
Claude Code con Opus 4.8 en VS Code: potencia bien dirigida
Con la especificación en su sitio, la implementación arrancó con un comando de OpenSpec: /opsx:apply, que trabaja las tareas y las va materializando en código. El motor era Claude Code con Opus 4.8 sobre VS Code, configurado con el esfuerzo al máximo (max effort).
El detalle que merece subrayarse: no hizo falta activar ultrathink ni la ventana de contexto de 1M de tokens. La especificación estaba tan acotada y bien estructurada que el modelo no tenía que «adivinar» arquitectura ni mantener en memoria un contexto inmenso; tenía que ejecutar un plan claro. Esa moraleja puede ser contraintuitiva: invertir en la spec reduce la necesidad de quemar presupuesto de cómputo en el modelo.
El código generado no era de juguete. Un ejemplo: la regla de negocio «no inscribir si no hay plazas» no acabó como un if ingenuo con condición de carrera, sino dentro de una transacción con bloqueo, tal como exigía la spec:
@Injectable()
export class EnrollmentsService {
constructor(private readonly dataSource: DataSource) {}
async enroll(dto: CreateEnrollmentDto): Promise<Enrollment> {
return this.dataSource.transaction(async (tx) => {
const activity = await tx.findOneOrFail(Activity, {
where: { id: dto.activityId },
lock: { mode: 'pessimistic_write' },
});
const taken = await tx.count(Enrollment, {
where: { activityId: activity.id },
});
if (taken >= activity.capacity) {
throw new ConflictException('La actividad no tiene plazas disponibles');
}
return tx.save(Enrollment, tx.create(Enrollment, dto));
});
}
}
La entrada se validaba con DTOs declarativos, otro patrón que NestJS y TypeScript hacen trivial de generar de forma consistente:
export class CreateEnrollmentDto {
@IsUUID()
childId: string;
@IsUUID()
activityId: string;
}
Por qué el stack tecnológico respondió tan bien
Aquí la tecnología convencional se cobra su recompensa. NestJS impone una estructura predecible —módulos, controladores, servicios, inyección de dependencias— que es justo el tipo de patrón regular que un agente reproduce sin desviarse. TypeScript actúa como red de seguridad: muchos errores que un modelo podría introducir se convierten en fallos de compilación inmediatos, no en bugs silenciosos. Y en el frontend, Angular aporta la estructura tipada y los formularios reactivos mientras Ionic resuelve los componentes táctiles y el comportamiento responsive a partir del PDF del diseño de GUI de referencia.
En otras palabras: cuanto más convencional y autoexplicativo es el ecosistema, más fiable es la generación. Las convenciones fuertes de Node y Angular son una ventaja, no una limitación, cuando quien escribe es un agente.
Tests, Docker y CI: el bucle de calidad que lo cierra todo
Generar código no es terminar. El criterio de cobertura definido en el config (≥ 80% en unitarios) se materializó en tests con Jest, y los happy paths se cubrieron de extremo a extremo con Playwright. Lo notable de Playwright es lo poco que cuesta obtener una verificación realista del flujo completo:
import { test, expect } from '@playwright/test';
test('una familia inscribe a su hijo en una actividad con plazas', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Registrarse' }).click();
await page.getByLabel('Email').fill('familia@example.com');
await page.getByLabel('Contraseña').fill('Sup3rSecret!');
await page.getByRole('button', { name: 'Crear cuenta' }).click();
await page.getByRole('link', { name: 'Robótica' }).click();
await page.getByRole('button', { name: 'Inscribir' }).click();
await expect(page.getByText('Inscripción confirmada')).toBeVisible();
});
Además, Playwright genera artefactos de diagnóstico (vídeo y trazas) que sirven como feedback visual del comportamiento real de la app sin tener que reproducir el flujo a mano:
// playwright.config.ts
export default defineConfig({
use: {
baseURL: 'http://localhost:4200',
video: 'on',
trace: 'retain-on-failure',
},
webServer: [
{ command: 'npm run start:api', url: 'http://localhost:3000/health', reuseExistingServer: !process.env.CI },
{ command: 'npm run start', url: 'http://localhost:4200', reuseExistingServer: !process.env.CI },
],
});
Docker hace que «funciona en mi máquina» deje de ser una frase. Levantar la base de datos y la API para los tests es una línea, y el mismo docker-compose sirve en local y en el pipeline:
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_DB: extraescolares
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
api:
build: ./api
depends_on: [db]
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/extraescolares
ports: ['3000:3000']
Y todo se ata en el CI, donde la cobertura se convierte en una puerta que el pipeline no deja pasar si no se cumple:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: extraescolares
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
# Actions espera a que la BD acepte conexiones antes de seguir
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/extraescolares
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm run test:cov # falla si la cobertura baja del 80%
- run: npx playwright install --with-deps
- run: npm run test:e2e # Playwright levanta API + frontend y prueba los happy paths
El conjunto —Jest, Playwright, Docker y un workflow de CI— se integra con una fricción sorprendentemente baja. Cada una de estas herramientas está pensada para ejecutarse sin intervención, y esa cualidad es justo lo que permite que un agente no solo escriba código, sino que compruebe que ese código hace lo que la spec prometía.
La pregunta abierta
Si la especificación OpenSpec es el contrato, los tests son verdes y todo corre en contenedores, surge una pregunta incómoda y fascinante: ¿llegará el día en que la generación de código sea, sin más, un paso más del pipeline de CI? Imagina un repositorio en el que el artefacto versionado y revisado por humanos es la especificación OpenSpec, y donde un job del pipeline regenera (o reconcilia) la implementación, ejecuta la suite completa y sólo promociona el build si la spec y los tests están satisfechos. No estamos ahí —la revisión humana del diseño, la deuda técnica acumulada y los casos límite siguen exigiendo criterio—, pero ninguna de las piezas que hoy lo impedirían parece, ya, técnicamente lejana.
Si te interesa explorar el desarrollo dirigido por especificaciones con agentes en proyectos reales —desde escribir y revisar la spec hasta dejar el CI verificándolo todo—, en nuestro equipo llevamos tiempo trabajando exactamente en ese terreno. Cuéntanos tu caso y vemos juntos hasta dónde puede llegar tu pipeline. Hablemos.