Esta serie empezó con el desarrollo de Monster High. La excusa fue mi hija: tenía un catálogo de Pokémon que había hecho en un ejercicio siguiendo un curso de Next y, cuando era muy pequeña, se me ocurrió que podía servirle para que se familiarizase con el manejo del ratón. Ya pasados los años, pensé que si le hacía algo con los personajes que le gustaban de verdad, podría motivarla a leer. De ahí salió todo lo que hemos visto en los artículos anteriores.
Ahora ha descubierto Pokémon. Quiere ver evoluciones, conocer tipos, descubrir más personajes. El catálogo simple que conoce ya no le vale.
El repositorio del catálogo de Pokémon lleva años en GitLab, la excusa perfecta para aplicar lo que habíamos visto en el artículo Infraestructura de agentes autónomos, pero esta vez en una plataforma distinta a GitHub.
Eso cambia más cosas de lo que parece.
El nexo
En el tercer artículo cerré con una nota al margen:
Los principios se aplican exactamente igual si trabajas con cualquier otra plataforma.
Era cierto en teoría. Este artículo es la prueba en la práctica. Y la práctica tiene matices.
El laboratorio
El stack del laboratorio:
- Repositorio: GitLab (tier Free)
- IDE: Antigravity v2.x
- Gestor de paquetes: Bun (migrado desde Yarn)
- Deploy: Vercel, desvinculado del repositorio y controlado desde el pipeline
El proyecto tiene las dependencias bastante desactualizadas (Next.js 12, React 18, TypeScript 4.7). Eso queda para otro ciclo. El objetivo de este laboratorio era definir la capa de seguridad para el agente, trasladando a GitLab la infraestructura de gobernanza que vimos en el artículo anterior.
El único cambio respecto al repositorio original fue migrar de Yarn a Bun como gestor de paquetes. Al retomar el
proyecto desde cero, con una instalación limpia de dependencias, era el momento natural para hacerlo. El pipeline
refleja esa decisión: usa oven/bun:1-alpine como imagen base y bun.lock como clave de
caché.
Seguridad en el deployment
El primer paso, en esto no hay diferencia, cortar la integración automática de Vercel. Si no se hace, Vercel despliega en paralelo al pipeline, completamente fuera de nuestro control.
El proceso:
- 1. Desvincular el repositorio desde el panel de Vercel (Settings → Git → Disconnect)
- 2. Generar un token en Vercel (Settings → Tokens) con nombre descriptivo, por ejemplo
GITLAB_CI_PIPELINE - 3. Obtener los IDs del proyecto. Se pueden obtener desde la interfaz de Vercel o ejecutando
vercel linken local, que genera.vercel/project.jsonconorgIdyprojectId - 4. Añadir las variables en GitLab (Settings → CI/CD → Variables):
VERCEL_TOKEN→ siempre Masked, Protected (porque solo se despliega desdemain)VERCEL_ORG_IDyVERCEL_PROJECT_ID→ sin enmascarar
Una vez desconectado Vercel, el único camino al deploy es el pipeline.
El pipeline
Para materializar la gobernanza en GitLab, diseñé un archivo .gitlab-ci.yml estructurado en dos
fases:
- La primera valida el código en cada Merge Request (MR). Si falla, el merge queda bloqueado.
- La segunda despliega a producción, pero solo cuando un humano pulsa el botón.
stages:
- validate
- build
.bun_cache: &bun_cache
cache:
key:
files:
- bun.lock
paths:
- node_modules/
lint-and-typecheck:
stage: validate
image: oven/bun:1-alpine
<<: *bun_cache
before_script:
- bun install --frozen-lockfile
script:
- bun run lint
- bun x tsc --noEmit
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
deploy-production:
stage: build
image: oven/bun:1-alpine
<<: *bun_cache
environment:
name: production
before_script:
- bun install --frozen-lockfile
script:
- bun x vercel pull --yes --environment=production
- bun x vercel build --prod
- bun x vercel deploy --prebuilt --prod --yes
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
Dos decisiones deliberadas aquí:
-
La caché usa el hash de
bun.lockcomo clave. Si se añade un paquete, el lockfile cambia, la clave cambia, el pipeline reinstala todo. Si no cambia nada, reutiliza losnode_modulescacheados. Automático, sin configuración adicional. -
El stage
validatesolo corre en Merge Requests. Amainno se puede hacer push directo, solo merges. Y el MR ya habrá validado el código. Ejecutar validate de nuevo al mergear sería gastar minutos de runner en algo ya comprobado.
El when: manual en deploy es la pieza central del Human in the loop
(HITL), el mismo principio que aplicamos en el artículo anterior. Sin click humano,
no hay deploy. Ningún agente puede saltarse eso, el control está definido en el servidor.
La protección de ramas
El pipeline solo funciona como guardrail si nadie puede saltárselo. Para eso hay que cerrar el acceso directo a
main desde la configuración de GitLab.
Settings → Repository → Branch Rules → main → View details
- Allowed to merge: Maintainers
- Allowed to push and merge: No one
- Allow force push: desactivado
En Settings → Merge Requests:
- Pipelines must succeed: activado
- All threads must be resolved: activado
Estas dos opciones son las que provocan que si el job de lint o type-check falla, el botón de merge queda desactivado y por tanto el pipeline queda bloqueado. Si el pipeline falla, el merge queda bloqueado para cualquiera.
Dónde choca GitLab con GitHub
Aquí es donde el laboratorio se vuelve interesante (y honesto).
En el artículo anterior, la infraestructura descansaba sobre tres pilares: CI/CD, CODEOWNERS con aprobación obligatoria, y protección de entornos. Los tres funcionaban juntos para hacer la gobernanza determinista.
En GitLab (free), el segundo pilar no existe.
CODEOWNERS requiere Premium. El archivo puede existir en el repositorio, pero el toggle que lo convierte en aprobación obligatoria solo aparece en Branch Rules si tienes Premium. En Free, el archivo es documentación.
Las Approval Rules tampoco están en Free. En GitHub, puedes exigir revisores concretos, invalidar aprobaciones cuando hay nuevos commits, o evitar que el autor apruebe su propio código. En GitLab Free, ninguna de estas opciones existe en la interfaz.
El pipeline es el único Hard Guardrail determinista remoto. Si el pipeline falla, el merge se bloquea.
La consecuencia práctica: la protección contra que el agente modifique sus propios archivos de gobernanza dependía
de
los Soft Guardrails (las instrucciones en AGENTS.md y GEMINI.md) más
que de mecanismos técnicos infranqueables.
Exactamente la limitación que el artículo anterior describía como insuficiente. Es una limitación real. Y es el tipo de limitación que vale la pena documentar.
La gobernanza del agente (Antigravity)
Una parte del laboratorio que aún no está validada en la práctica es la gobernanza específica de Antigravity. Lo que sí está definido es la estructura de configuración, lo que queda por comprobar es hasta qué punto el IDE la respeta por encima de su propio comportamiento por defecto.
La configuración del agente la dividí en tres capas, cada una con un propósito distinto.
AGENTS.md en la raíz — identidad universal, legible por cualquier agente en
cualquier IDE. Define el idioma, el comportamiento y los estándares generales, pero delega el detalle metodológico
en archivos de reglas bajo .agents/rules/: el ciclo TDD, la metodología XP, los estándares de código
y de testing. Esa separación es una buena práctica en sí misma, mantiene el archivo raíz legible y permite
actualizar cada regla de forma independiente.
GEMINI.md en la raíz — overrides específicos de Antigravity, pensados para extender
AGENTS.md cuando el comportamiento del IDE lo permite. Cubre la asignación de modelos según el tipo
de tarea, una política de ejecución que distingue entre Plan Mode y Fast Mode, una Terminal Policy con comandos
explícitamente permitidos y comandos que requieren confirmación humana, y reglas de gobernanza GitLab: no pushear
directamente a
main, no tocar .gitlab-ci.yml, y no aprobar ni triggerear el deploy de forma autónoma. La incógnita
es si
Antigravity respeta estas instrucciones cuando entran en conflicto con su configuración interna.
openspec/config.yaml — contexto para OpenSpec. Define el stack técnico, las
restricciones del proyecto, y las reglas por tipo de artefacto (proposal, specs, design, tasks). OpenSpec gestiona
el ciclo de cambios (/opsx:propose → /opsx:apply → /opsx:archive) y reemplaza el flujo manual de
reasoning → planning → action.
La lógica de la separación: lo que va en AGENTS.md es exportable a cualquier IDE. Lo
que va en GEMINI.md está pensado para ser Antigravity-only. Lo que va en
config.yaml es el
contrato que OpenSpec usa para generar artefactos útiles. Si esa separación funciona como está
diseñada es algo que la fase de construcción tendrá que confirmar.
Lo que me llevé
GitLab Free es un entorno de gobernanza incompleto comparado con GitHub. No es una opinión, es una constatación técnica: sin CODEOWNERS obligatorio y sin Approval Rules, la Frontera A que describí en el artículo anterior queda parcialmente abierta. En GitHub puedes delegar parte de la supervisión a la infraestructura. En GitLab Free, la infraestructura te da el pipeline. El margen de error humano es mayor.
El siguiente paso es empezar a construir. Mi hija lleva tiempo esperando las evoluciones, y a mí me permitirá,
mediante una práctica deliberada, comprobar cuánto pesa realmente trabajar con esta seguridad incompleta así como
observar qué tal funcionan los overrides de Antigravity definidos en GEMINI.md. Lo veremos en el
siguiente artículo.