Files
takana/CLAUDE.md
T
SergioandClaude Opus 5 d45c0688e7 granja: el worker es el LXC PRESTADO (€0) — que no se vuelva a perder
El dato ya estaba en memoria y aun así se perdió dos veces, así que ahora vive en los sitios
que se leen sin buscarlos: regla 1 bis de CLAUDE.md (que carga todo agente en cada sesión) y
la skill 'granja' del repo. Se compila en dev.gioser.net PARA NO GASTAR HETZNER; no se
levantan cajas hcloud salvo petición explícita.

Y se arregla la fragilidad que salió al mirar: el reaper de cosecha-cron expulsa de .fleet
todo lo que no esté en hcloud, y el LXC se salvaba SÓLO porque su nombre contiene la subcadena
'gioser' y caía en una lista negra que existe para proteger al hub de Hetzner — no tiene nada
que ver con él. Medido con el bloque real del reaper contra un .fleet de juguete:

  nombre CON 'gioser'                    → 'en LISTA NEGRA ⇒ intocable'      sobrevive
  el MISMO host como 'pruebasia-lxc'     → 'ya no existe en hcloud'          .fleet VACÍO

O sea que la granja se mantenía conectada por una casualidad de nombre, y con otro nombre
volvía a 'flota vacía' en el primer ciclo sin que nada fallara. Ahora el reaper pregunta por
SSH si el host responde, que es preguntarle a la máquina en vez de al nombre. Verificado en
las dos direcciones, con control:

  host vivo, nombre sin 'gioser'  → 'no es de hcloud pero RESPONDE ⇒ se queda (€0)'
  host que no responde            → 'no responde ⇒ lo saco de .fleet'   (intención original)

Queda anotado también que .fleet está gitignored: un hub recién clonado nace con la flota
vacía y la granja queda desconectada en silencio — la cosecha dice 'flota vacía', los
artefactos no vuelven, y estado-granja.sh reporta 'no hay worker vivo' con el worker
compilando. Es como se descubrió esto hoy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 18:41:16 +00:00

7.3 KiB

Reglas del repo compartido

Este repo lo trabajan varios agentes a la vez (hoy: frente granja/store y frente kernel). Lo de aquí abajo no son preferencias de estilo: son las dos formas conocidas de que un agente destruya el trabajo de otro sin enterarse. El resto del diseño está en docs/.

1. Todo hammer build va envuelto en flock

flock work/.farm-build.lock ./target/release/hammer --store ./store build <receta>

Para una tanda, tomar el lock una sola vez y no por receta:

flock work/.farm-build.lock bash -c 'for r in ...; do ./target/release/hammer --store ./store build "$r"; done'

Por qué. hammer build comparte work/sources/<dep>-<sha> entre todas las recetas. Dos builds concurrentes que compartan una dependencia se pisan el árbol de fuentes: uno hace fetch y lo borra mientras el otro lo usa, y el árbol queda roto para siempre — reintentar no lo arregla. Medido a escala: al invalidar libdrm, ~93 de 205 recetas KDE murieron con /src/.zwrap/cc is not a full path to an existing compiler tool, que es el wrapper de zig que la receta deja EN EL ÁRBOL, barrido por el fetch concurrente de otra receta. Es el ADR 0012, sin decidir; hasta que se decida (lock por árbol / árbol privado / caché inmutable + copia), serializar es la única mitigación correcta. Por eso el worker corre con JOBS=1.

Y usar flock -o, no flock a secas — esto es medido, no teórico (2026-09-08). El lock lo sostiene la descripción de fichero abierta, y los hijos la HEREDAN: si un nieto se fuga, el lock queda tomado para siempre aunque el flock haya terminado hace rato. Pasó: dos firefox colgados de una caza de bugs sobrevivieron al kill del bwrap que los envolvía y dejaron a la granja sin poder compilar durante hora y media, en silencio — nada falla, simplemente el siguiente flock espera para siempre. Comprobado en los dos sentidos:

flock    lock sh -c 'sleep 25 & exit 0'   # ⇒ el nieto RETIENE el lock tras salir flock
flock -o lock sh -c 'sleep 25 & exit 0'   # ⇒ lock LIBRE; -o cierra el fd antes de ejecutar

Si el lock parece tomado y no hay ningún build, fuser -v work/.farm-build.lock dice quién lo tiene; casi siempre es un proceso fugado que nadie asocia con el lock. Los scripts de scripts/farm/ usan el estilo exec 9> + flock 9, que es vulnerable igual (haría falta 9>&- en cada hijo): deuda conocida, no barrida.

scripts/farm/farm-worker-loop.sh y campana-deuda.sh ya toman ese mismo fichero de lock, así que usarlo nos serializa con la granja además de entre nosotros. No está dentro de hammer build a propósito: esos scripts lo toman por fuera y hammer se bloquearía contra ellos.

1 bis. El worker es el LXC PRESTADO, y es GRATIS: no se levanta nada en Hetzner

dev.gioser.net (154.197.1.13, hostname interno PruebasIA) es dónde se compila. Es un LXC prestado en el Proxmox de gioser: 6 cores, 16 G RAM + 8 G swap, 196 G de disco, coste €0. Se usa exactamente por eso — para no gastar Hetzner.

No levantar cajas hcloud (farm-up.sh, farm-run.sh) salvo que el usuario lo pida por su nombre. El modelo efímero de pago sigue documentado y funcionando, pero está SUPERADO como sitio de trabajo desde el 2026-09-05. Un farm-up por reflejo cuesta dinero real y no hace falta.

ssh -i ~/.ssh/github5 root@dev.gioser.net     # github5 está en su authorized_keys
ssh -i ~/.ssh/sergio  root@dev.gioser.net     # la otra que entra; NO hay entrada en ssh/config

Se enchufa por scripts/farm/.fleet (<nombre> <ip> por línea), que está en .gitignore: o sea que un hub recién clonado nace con la flota VACÍA y la granja queda desconectada sin que nada falle — la cosecha dice «flota vacía» cada 30 min y los artefactos del worker no vuelven. Pasó, y se descubrió por casualidad. Si estado-granja.sh dice «no hay worker vivo», mirá .fleet antes de creerle.

echo "dev.gioser.net 154.197.1.13" > scripts/farm/.fleet

Y hasta el 2026-09-08 eso sobrevivía por una CASUALIDAD DE NOMBRE. El reaper de cosecha-cron.sh saca de .fleet a todo lo que no esté en hcloud; el LXC se salvaba sólo porque su nombre contiene la subcadena gioser y caía en una lista negra que existe para proteger al HUB de Hetzner y no tiene nada que ver con él. Medido: el mismo host llamado pruebasia-lxc era expulsado en el primer ciclo. Arreglado — ahora el reaper pregunta por SSH si el host responde en vez de mirarle el nombre — pero la lección queda: una protección que funciona por accidente se ve igual que una que funciona por diseño.

2. Nunca git add -A — y acotar el COMMIT, no sólo el add

Sólo rutas explícitas. Un add -A arrastra al commit los ficheros a medias de otro agente. Commits granulares, en español, directo sobre main, y git push tras cada unidad de trabajo (el origin empuja a gitea y al espejo privado de GitHub; ver scripts/espejo-setup.sh).

«Rutas explícitas» en el add NO ALCANZA, y esto es medido, no teórico (2026-09-06): el commit ff0b556 se llevó dentro un rename y un borrado de otro agente habiendo usado git add recipes/firefox.toml y git commit sin -a. La causa es que el índice es estado COMPARTIDO: git commit commitea el índice ENTERO, no lo que vos acabás de añadir, así que cualquier cosa que el otro agente dejó en git add viaja en tu commit. Comprobado en un repo de juguete, en los dos sentidos:

git add mio.txt && git commit -m …          # ⇒ arrastra ajeno.txt (estaba staged por el otro)
git commit -m … -- mio.txt                  # ⇒ SÓLO mio.txt; lo del otro queda staged e intacto

Entonces: git commit -m "…" -- <rutas>. El -- acota el commit por pathspec y es lo único que aísla de verdad. Vale también para git commit -F -. Corolario: no dar por bueno el alcance sin mirarlo — git show --stat sobre el commit recién hecho cuesta un segundo y es la única forma de enterarse el mismo día en vez de por el otro agente.

3. Antes de dar un artefacto por presente, mirá que tenga contenido

Un directorio vacío en el store no es un artefacto: es un nombre. Store::has ya lo rechaza y respaldo-storagebox.sh --listar los separa a work/respaldo-vacios.txt, pero la regla general sigue valiendo para cualquier código nuevo — un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien.

4. La superficie de la CLI va en INGLÉS; los mensajes, en castellano

Subcomandos, flags y nombres de opciones: inglés, sin excepción (build, hash, hydrate, mirror push, kernel closure, qorpa pull). Lo que el usuario LEE —ayuda, errores, logs— va en castellano, y los nombres propios del proyecto son quechua (qorpa, harkaq, yupana, arje).

Por qué. Un verbo de CLI es contrato: entra en scripts, cron y runbooks, y renombrarlo después rompe llamadores que nadie recuerda. Media superficie en cada idioma obliga a adivinar en cada comando nuevo, y ya pasó: el ADR 0015 nació con traer/crear/correr y hubo que corregirlo.

Deuda conocida, NO barrida todavía: varios scripts de scripts/ exponen flags en castellano (--crear, --traer, --listar, --sha). El barrido es su propia unidad de trabajo — tocarlos de paso rompe cron y la granja. Código NUEVO nace en inglés desde hoy.