`work_root` sale de `dirname(store)/work`, y con el store en `/store` el padre es `/`: todo el scratch iba al overlay de 69 G de la jaula, que además es capa DESECHABLE — la caché de fuentes vivía en algo que se tira. Mientras tanto `/dev/sdc`, 255 G, estaba al 9 %. De los 6,8 G de `work/sources`, 5,4 G eran DOS COPIAS del mismo commit de tawasuyu: los árboles se nombran por receta, no por commit, así que cada receta del monorepo cuesta otros 2,7 G. Queda anotado que es deliberado (aislamiento del ADR 0012) y que no se deduplica de paso. Arreglado con enlaces a `/work/sergio/work` en vez de con `TAKANA_WORK`: el override existe y funciona, pero una docena de scripts llama a `takana build` y un export que hay que recordar se olvida. Comprobado con un build real sin ninguna variable — rc=0, el árbol cayó en sdc y el hash salió idéntico al de la corrida anterior. Overlay de 6,9 G a 14 G libres. Anotado también que el worker NO tiene este problema (un solo disco de 196 G), que los enlaces se van si la jaula se rehace, y que la premisa del `seal` (rename atómico ⇒ mismo filesystem) ya estaba rota en el hub antes de esto, porque `/work` y `/store` son discos distintos.
16 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 takana build va envuelto en flock
flock work/.farm-build.lock ./target/release/takana --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/takana --store ./store build "$r"; done'
Por qué. takana 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 takana build
a propósito: esos scripts lo toman por fuera y takana 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/id_ed25519 root@dev.gioser.net # la que entra HOY; NO hay entrada en ssh/config
⚠ Los scripts piden ~/.ssh/github5, ese fichero NO EXISTE en el hub, y aun así todo funciona —
por accidente (medido 2026-09-18). Una docena larga de scripts trae
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}": estado-granja.sh, cosecha-cron.sh, harvest-go.sh,
farm-up.sh, farm-down.sh, harkaq-vol.sh… El único fichero en ~/.ssh es id_ed25519, y sin
embargo estado-granja.sh con su default entra y reporta. ssh -i <fichero inexistente> no
falla: avisa por stderr y cae a las identidades por defecto, que es donde está la buena:
Warning: Identity file /home/sergio/.ssh/github5 not accessible: No such file or directory.
debug1: identity file /home/sergio/.ssh/id_ed25519 type 2
debug1: Offering public key: /home/sergio/.ssh/id_ed25519 ED25519 SHA256:kAqayFW4hSqq…
debug1: Server accepts key: … · Authenticated to dev.gioser.net using "publickey".
O sea que el -i de todos esos scripts es hoy un no-op y quien manda es el fallback. Se cae el
día que alguien ponga un ~/.ssh/github5 que no entre (el -i explícito GANA sobre el default), o
que aparezca un IdentityFile/IdentitiesOnly en ssh/config. Y el error dirá Permission denied, sin mencionar SSH_KEY ni github5. Mismo patrón que el reaper salvándose por la
subcadena gioser: una protección que funciona por accidente se ve igual que una que funciona por
diseño. Barrer el default de los scripts es su propia unidad de trabajo — no tocarlos de paso.
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).
⚠ Desde la jaula qorpa ese push doble SÓLO llega a gitea: el espejo de GitHub falla siempre
(medido 2026-09-18). El PATH del lab pone primero el git del CORPUS —/work/sergio/bin/git,
2.54.0, musl— y ese build no trae los helpers remote-http/remote-https. El push termina así,
con gitea ya actualizado y GitHub intacto:
To ssh://git.gioser.net:2345/sergio/takana.git
317ce48c..fd065720 main -> main ← gitea OK
git: 'remote-https' is not a git command. ← y el espejo se queda atrás
fatal: remote helper 'https' aborted session
Sale con estado ≠ 0, pero la primera línea dice «main -> main» y se lee como éxito. El git de la
imagen sí los tiene (/usr/bin/git, 2.55.0), así que el espejo se empuja con él:
/usr/bin/git push https://github.com/sergiovelasquezzeballos/takana.git main
Corolario: el espejo deriva solo mientras se trabaje desde adentro — el 2026-09-18 estaba una hora
atrás y nadie se había enterado. Comprobar con git log --oneline -1 de los dos lados, no con la
salida del push.
⚠ «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.
⚠ Pero el -- NO protege la ruta que vos nombrás, y la intuición es la contraria (medido
2026-09-15, repo de juguete): con un fichero en estado MM —stageado y modificado por otro—,
git commit -- f.txt commitea el ÁRBOL, no lo stageado, y además deja el índice en la versión
del árbol, o sea que lo que el otro agente tenía stageado en ESA ruta desaparece.
# f.txt: v2 en el índice (del otro), v3 en el árbol (mío)
git commit -m … -- f.txt # ⇒ commitea v3 · el índice queda en v3 · v2 se perdió
El pathspec aísla de lo que el otro dejó en otras rutas — que es lo del párrafo de arriba y
sigue siendo cierto. Corolario: un fichero que aparece MM y que vos no tocaste no se commitea
ni con pathspec — se avisa. Pasó con el Cargo.lock de tawasuyu (SDD 26 §7.quinquies), donde el
árbol del otro agente ya traía la reparación que hacía falta.
2 ter. git pull --rebase puede DESCARTAR tu commit sin decir nada
⚠ Medido acá el 2026-09-16, y cuesta una hora de trabajo si no sabés mirar el reflog.
Secuencia real: commit propio en main (10 ficheros nuevos) → git push → gitea lo rechaza
porque otro agente empujó → git pull --rebase → el push vuelve a fallar y el commit ya no está,
ni en el log ni en el ÁRBOL: los diez ficheros habían desaparecido del disco.
El reflog lo explica y es lo único que lo explica:
11a9ead6 HEAD@{0}: pull --rebase (finish): returning to refs/heads/main
11a9ead6 HEAD@{1}: pull --rebase (start): checkout 11a9ead6
f3d7be6c HEAD@{2}: commit: los 9 daemons propios entran al catálogo… ← el mío
Entre start y finish no hay ni un pick: el rebase no encontró NADA que reaplicar, y con
-q eso se ve igual que un pull limpio.
⚠ PASÓ DOS VECES EL MISMO DÍA (2026-09-16 y 2026-09-17), con el mismo reflog: commit propio,
(start): checkout <otro>, (finish): returning to refs/heads/main — y ni un pick en el medio.
Y el comando NO es el culpable: se reprodujo en un repo de juguete y ahí funciona bien. Dos
clones, el otro empuja, yo commiteo local, git pull --rebase -q origin main ⇒ mi commit se
reaplica y mi fichero sigue ahí. O sea que lo que falla no es pull --rebase: es que este árbol
lo comparten varios agentes y alguien mueve refs/heads/main mientras el rebase está en vuelo.
La receta práctica no cambia; el diagnóstico sí, y evita ir a buscar el bug al lugar equivocado.
⚠ La CAUSA exacta no está determinada, y conviene decirlo así en vez de inventarla. Lo medido es el
reflog de arriba y que los diez ficheros desaparecieron también del ÁRBOL, que es lo que un
reset --hard o un checkout concurrente de otro agente sí explicaría. Lo que NO es evidencia —y
se escribió como si lo fuera— es que los commits que trajo el pull cuelguen del commit anterior al
mío: eso es lo normal cuando el otro lado empujó desde una máquina que había hecho fetch antes, y
no dice nada de lo que pasó acá. Que el árbol perdiera los ficheros es el dato fuerte; el resto,
hipótesis.
Qué hacer:
git reflog -15 # tu commit está acá, con su sha
git cherry-pick <sha> # vuelve entero, ficheros incluidos
Y la comprobación que evita el susto: git log --oneline -1 después de cada pull --rebase. Si
tu commit no es el primero, no se rebasó — se perdió de la rama, y el reflog lo tiene.
⚠ El espejo de GitHub queda DIVERGENTE en ese escenario, y es normal: el push doble puede
triunfar en un remoto y fallar en el otro (pasó: gitea rechazó, GitHub aceptó). Tras recuperar el
commit, el espejo tiene un sha huérfano con el mismo contenido. Se repara empujando sólo la URL de
GitHub con --force-with-lease=main:<sha-huérfano> — con lease, que verifica que el remoto sigue
donde creés antes de pisarlo. Comprobar antes que el contenido no se pierde:
git diff --stat <huérfano> HEAD.
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.
3 bis. En el HUB, el scratch de build no cae donde parece — y el disco bueno estaba sin usar
⚠ Medido el 2026-09-21. work_root no es una ruta elegida: sale de dirname(store)/work
(crates/takana-build/src/config.rs). Con el store en /store, el padre es / ⇒ todo el scratch
de build cae en el overlay de la jaula. En esta caja eso son 69 G compartidos, y el disco de
verdad —/dev/sdc, 255 G— estaba al 9 %, sin usar.
Dos cosas lo empeoran, y ninguna se ve desde df:
- El overlay es capa DESECHABLE.
/workno tiene montaje propio: si la jaula se rehace, los árboles de fuentes se van con ella. La caché de build vivía en algo que se tira. - Los árboles se nombran por RECETA, no por commit.
bovedayshuma-preguntasalen del mismob80f7567…de tawasuyu y ocupaban 2,7 G cada una — 5,4 G de los 6,8 G totales eran el mismo contenido dos veces. Cada receta nueva de ese monorepo cuesta otros 2,7 G. Es deliberado (árbol por receta = el aislamiento que evita el destrozo del ADR 0012); no deduplicar de paso.
Arreglado con enlaces, no con una variable de entorno, porque una docena de scripts llama a
takana build y un export que hay que recordar se olvida:
ln -sfn /work/sergio/work/sources /work/sources
ln -sfn /work/sergio/work/out /work/out
Comprobado con un build real contra un store tirable y sin ninguna variable: rc=0, el árbol
cayó en sdc y el hash salió idéntico al de la corrida anterior. El overlay pasó de 6,9 G a 14 G
libres. Existe TAKANA_WORK como override (junto a TAKANA_LAB/ROOTFS/ZIG/CACHE) y
funciona, pero el enlace no se puede olvidar.
⚠ Si la jaula se rehace, los enlaces se van y el scratch vuelve al overlay en silencio — nada
falla, sólo se llena otra vez. Rehacerlos es lo de arriba; el contenido sigue en /work/sergio/work.
El WORKER no tiene este problema: un único disco de 196 G con todo encima (/, /opt/takana,
su store), así que ahí work_root y el store comparten filesystem y el seal —que es un rename,
atómico sólo dentro del mismo fs— cumple su premisa. En el hub esa premisa ya estaba rota antes
de este cambio: /work en sda4 y /store en sdb son discos distintos. Mover el store al disco
grande lo restauraría, pero es su propia unidad de trabajo — toca la granja, el hub y los scripts.
4. La superficie de la CLI va en INGLÉS; los mensajes, en castellano
Subcomandos, flags y nombres de opciones: inglés (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 (takana, qorpa, harkaq, yupana, arje).
La única excepción, y es cerrada: el ADR 0016 admite alias de marca en castellano sobre un
verbo canónico —hoy uno solo, forja → build— para que la invocación de la hoja de marca
funcione. El alias no reemplaza nada: el canónico sigue siendo el inglés y es el que usan los
scripts, el cron y los runbooks. Un alias es superficie de marca; el contrato es el canónico.
Agregar otro alias es una decisión de ADR, no algo que se hace de paso.
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.