Un blog de un idioma, tres días después de tres

Hace poco teníamos una página web en Astro con un único idioma. La semana pasada, el mismo blog servía en español, catalán e inglés, con un selector de idioma, feeds de RSS y sitemap por idioma, y un despliegue que se hacía solo desde Git.

No fue magia. Fue una pyme agéntica: un conjunto de agentes de Hermes trabajando con GitOps (Flux CD), un modelo local (Qwen 3.8 27B) y una herramienta como OpenCode, sobre una base de infraestructura que ya teníamos montada. Lo que quiero contarte no es la hazaña: es cómo lo hicimos, paso a paso, con el detalle suficiente para que lo puedas replicar. Y lo que aprendimos de coste en el camino.

Este post narra un caso real del repositorio ia_develop/astro. El trabajo se hizo en la rama develop, pero hoy el blog vive en la rama main (tag 0.0.2) con i18n completo, en producción. No es una ficción: cada cifra que aparece aquí sale del historial de git, del estado de Kanban o de los logs. Si algo no es real, no está.

La pyme agéntica: quién hizo qué

Lo primero que hay que entender es que no hubo “una persona trabajando 60 horas”. Hubo varios agentes trabajando en horas sueltas, coordinados por un tablero Kanban que era la fuente de la verdad. Así se repartió el trabajo de la migración i18n (18 commits, del 19 al 25 de agosto, en la rama develop, mergeada luego a main con el tag 0.0.2):

Quién Qué tipo de trabajo Ejemplo real (commit)
Hermes (agente principal) Framework i18n, localización de contenido, routing, feeds feat(i18n): content layer per-locale (es/ca/en), feat(i18n): per-locale RSS + sitemap feeds
OpenCode UI y mejoras con soporte directo de operador con livecoding i18n: extract UI strings to locale files (nav, about, index)
Humanos (operadores) Revisión, decisiones, contenido feat(i18n): localize blog index per-locale, i18n(content): add ca + en versions…

La clave no es que los agentes escribieran solo. Es que el trabajo se dividió en tareas pequeñas y verificables, cada una con un resultado comprobable. Kanban era el tablero; Git era la pista; la revisión humana, el control de calidad.

Esto es importante: un agente no “resuelve” el problema solo. Un agente hace una tarea, la deja en un estado verificable, y el siguiente la recoge. La coordinación no está en la cabeza de un agente, está en el tablero.

Tablero de Hermes (Kanban): evidencia del reparto de tareas durante la migración i18n

Evidencia: el tablero de Hermes (Kanban) que hizo de fuente de la verdad durante la migración i18n.

Cómo lo hicimos, paso a paso

El flujo tuvo cinco fases. Cada una es una tarea de Kanban con su propio resultado y su propia revisión.

1. Investigar antes de escribir

No se planifica sin datos. Antes de tocar nada, el agente se preguntó: ¿qué hay ya, qué hay en el cluster, qué ha hecho cada uno? Se inventariaron:

  • Los boards de Kanban (qué trabajo existía: astro-blog-i18n, astro-ai-blog-delivery, etc.).
  • El flujo GitOps (cómo se construye y despliega la imagen).
  • La documentación (docs/, ADRs, runbooks).
  • Los commits por autor (qué hizo Hermes vs. OpenCode vs. el humano).

Sin esa evidencia, el post sería una invención. Con ella, es un relato veraz.

Evidencia del inventario previo: boards de Kanban, flujo GitOps, docs y commits por autor

Evidencia: tantas adiciones para tan poco tiempo… es una locura.

2. Escribir el contenido (con revisión)

El post se escribe en la voz que leemos a diario, pero firmado por quien entiende la técnica. Aquí aplicamos una regla no negociable: toda tarea que genera contenido, código o manifests termina con una revisión. El borrador no se publicaba sin pasar por un ojo humano.

3. Verificar el build

npm run build && npm run preview
# y comprobar que el post aparece en los tres idiomas
grep -c '<slug>' dist/rss.xml dist/sitemap.xml   # >= 1 por idioma

Un build verde y los feeds correctos en los tres idiomas son el cierre de esta fase.

4. Abrir el PR (nunca a main directo)

Nunca commit directo a main. Se crea una feature branch y se abre un pull request. Aquí el trabajo del agente termina: el humano decide si mergea.

5. Merge + despliegue (humano)

El operador mergea, Flux reconcilia, el Image Update Automation consume la rama y despliega. El agente no mergea ni despliega a producción. Esa decisión es humana, siempre.

La regla que sostiene todo: todo trabajo pasa por Kanban, y toda tarea que genera código, manifests o scripts acaba con una revisión requerida.

Detalle técnico replicable

Aquí viene lo que puedes copiar. Todo el código que sigue es real y está en la rama main (tag 0.0.2) de ia_develop/astro.

a) Configurar los idiomas (src/config.ts)

Los idiomas se definen una vez, en un solo lugar:

export const locales = ['es', 'ca', 'en'] as const;
export const defaultLocale = 'es';
export type Locale = (typeof locales)[number];

// Nombres propios (autónimos) de cada idioma, en un único sitio
export const localeNames: Record<Locale, string> = {
  es: 'Español',
  ca: 'Català',
  en: 'English',
};

La idea es que los labels de navegación no estén “duras” en el código, sino que se resuelvan por idioma. El array navItems guarda key, y la etiqueta se busca en el catálogo del idioma activo.

b) Rutas por idioma con Astro (src/pages/[locale]/index.astro)

Astro usa routing por archivos. Si pones un archivo en la carpeta [locale]/, genera una variante por cada idioma configurado:

export const getStaticPaths = () =>
  locales.map((locale) => ({ params: { locale }, props: { locale } }));

const { locale } = Astro.props as { locale: (typeof locales)[number] };
const t = getMessages(locale);

Un único index.astro dentro de [locale]/ produce /es, /ca y /en sin duplicar el código.

c) Resolver el idioma y las fechas (src/lib/i18n.ts)

import { getLocaleByPath } from 'astro:i18n';
import { locales, defaultLocale, type Locale } from '../config';
import es from '../locales/es.json';
import ca from '../locales/ca.json';
import en from '../locales/en.json';

const messages: Record<Locale, typeof es> = { es, ca, en };

export const getMessages = (locale: string | undefined) => {
  const key =
    (locale && (locales as readonly string[]).includes(locale)
      ? (locale as Locale)
      : defaultLocale) as Locale;
  return messages[key];
};

Los strings de la interfaz (navegación, sobre, índice) se extraen a src/locales/*.json. El contenido de los posts vive en src/content/blog/{es,ca,en}/<slug>.md.

d) Contenido por idioma sin colisiones (src/content.config.ts)

El truco que hace que tres idiomas del mismo post no se pisen es el generateId:

const blog = defineCollection({
  loader: glob({
    pattern: '**/*.md',
    base: './src/content/blog',
    generateId: ({ entry, data }) => {
      // entry = "ca/getting-started-with-astro.md"
      const rel = entry.replace(/\\/g, '/').replace(/^\.?\/+/, '');
      const [folder, file = ''] = rel.split('/');
      const slug = data?.slug || file.replace(/\.md$/i, '') || rel;
      // id único: <locale>-<slug>
      return LOCALES.includes(folder) ? `${folder}-${slug}` : slug;
    },
  }),
  schema: z.object({
    title: z.string(),
    slug: z.string().optional(),
    locale: z.enum(LOCALES as [string, ...string[]]).default(defaultLocale),
    description: z.string(),
    pubDate: z.date(),
    tags: z.array(z.string()),
    author: z.string().optional(),
    image: z.string().optional(),
    featured: z.boolean().optional(),
  }),
});

Sin este generateId, dos locales del mismo slug colisionarían en el store y uno borraría al otro. El id como <locale>-<slug> resuelve el conflicto.

e) El despliegue con GitOps (el CI de la rama main)

El flujo que convierte un commit en un despliegue es GitOps puro. El CI de Forgejo emite una tag cronológica inmutable:

# .forgejo/workflows/build-image.yml (resumen)
env:
  REGISTRY: registry.cris-mora-cv.es
  UNIXTS: $(date +%s)        # tag inmutable: 0.0.2
  # develop -> :develop (preview), main -> :launch (producción, tag 0.0.2)

La ejecución de estos jobs no se hace en una máquina fija: Forgejo dispara la workflow (git actions) en pods efímeros controlados por KEDA. KEDA escala los workers de Forgejo desde cero según los eventos de la cola, arranca el pod solo para el job y lo destruye al terminar; así no hay infraestructura de CI ociosa, solo se consume lo justo por despliegue.

Evidencia: flujo de build en Forgejo Actions (workflow git actions en pods efímeros con KEDA)

Evidencia: el flujo de build en Forgejo Actions (git actions) ejecutándose en pods efímeros escalados por KEDA.

El Image Update Automation de Flux consume esa rama y una política numérica:

ImagePolicy: pattern '^0.0.2-' + policy.numerical desc
-> el build más reciente (el unixts mayor) gana, y se despliega solo.

Un commit en main → Forgejo lanza el job en un pod efímero (KEDA) → CI emite la imagen 0.0.2-<unixts> → IUA detecta la nueva imagen → Flux reconcilia → el blog se actualiza. Sin tocar el cluster a mano.

Las skills: cómo Hermes recuerda lo que aprende

Una parte que a veces se pasa por alto: Hermes no reinventa la rueda en cada tarea. Usa skills, procedimientos que se guardan y se reutilizan. Algunas de las que aplicó este trabajo:

  • astro-blog — crear y publicar posts (content layer, feeds, build, deploy).
  • flux-gitops-integration — auditar GitRepos y Kustomizations de Flux.
  • flux-image-automation — depurar cuando la IUA no reconcilia.
  • kanban-board-management / kanban-worker — el tablero como fuente de verdad.
  • forgejo-git / forgejo-http-troubleshooting — operaciones Git con Forgejo interno.

Lo interesante es que algunas skills se auto-aprenden: cuando un flujo es difícil y se resuelve, Hermes lo guarda como skill para no volver a pelearlo. Es memoria procedural: no solo recuerda qué pasó, sino cómo se hizo.

Cifras y reflexión: el coste de la IA ya no es lo que era

Vamos a lo concreto, porque aquí está lo que de verdad me importaba transmitir.

La migración i18n dejó 18 commits en la rama develop (mergeados a main, tag 0.0.2), del 19 al 25 de agosto — una ventana de unos 6 días. Tuvimos 7 posts en los tres idiomas y una infraestructura (rutas, feeds, switcher, routing) completa. El trabajo se hizo en horas sueltas, repartido entre varios agentes y un par de humanos.

Esto no significa que no costó nada. Significa que el coste dejó de ser lineal con las horas-hombre. Antes, pasar un blog a tres idiomas era una semana de una persona, y luego otra semana para mantenerlo. Ahora es una actividad de horas sueltas que un par de agentes coordinan, con un humano al final revisando.

Y aquí está la reflexión que quiero dejarte: imagina lo que se podría hacer con hardware empresarial — un servidor con GPUs de verdad — frente a la máquina en la que esto se probó, una DGX Spark con GB10. Si con una máquina de sobremesa corriendo un modelo local (Qwen 3.8 27B) pudimos hacer esto en una pyme agéntica, el salto de calidad con infraestructura seria no es incremental: es de otro orden.

La conclusión que me llevo es esta: el cuello de botella ya no es el hardware ni el precio del modelo. Es saber preguntar bien y coordinar bien. Con recursos locales, el coste de “una pyme agéntica” se acerca a la nada en comparación con lo que costaba una persona completa dedicándose a tareas repetidas. El futuro no es “más humanos trabajando más horas”; es “mejor coordinación entre agentes, con humanos en los puntos que importan”.

Cierre

Pasamos de un idioma a tres. No fue un milagro: fue una pyme agéntica trabajando con Kanban como fuente de verdad, GitOps como autopista, un modelo local como cerebro y una revisión humana en el punto de decisión.

Si quieres replicarlo, el código que viste arriba está en ia_develop/astro (rama main, tag 0.0.2). Si quieres copiar el método, la regla es simple: divide el trabajo en tareas pequeñas, verifícalo, y deja la decisión importante para el humano.

Este post cierra el arco del blog con lo conseguido: la migración i18n en una semana. Lo que viene a continuación es el roadmap de cómo montamos nuestro homelab como la pyme de desarrollo — la infraestructura (cluster, GitOps, modelos locales, Forgejo y KEDA) que hizo posible todo lo que has visto aquí.

Y si te queda la pregunta de “¿y con hardware de verdad?”, la respuesta es la que debería dejar más claro este post: con una máquina de sobremesa ya se puede; con hardware empresarial, lo que se pueda hacer hoy es difícil de imaginar.