Abrir una segunda sesión fallaba con «Error: ninguna imagen empieza por "\u{1}" — traela con hammer
qorpa pull». El `\u{1}` no era un misterio: la línea que extrae el sha de la imagen tenía un `sed`
con `\1` de reemplazo, y el fichero terminó guardando el BYTE 0x01 literal en su lugar. O sea que
`qorpa create --base` recibía el carácter de control.
Se reemplaza por un `awk` que parte por comillas: no tiene retroreferencias ni nada que otra
herramienta pueda reinterpretar al escribir el fichero. Y si la extracción sale vacía, ahora corta
con un mensaje que nombra el fichero, en vez de pasarle basura a qorpa.
Comprobado: la segunda sesión se crea y arranca (claude-sergio-3, Claude 2.1.240) con la primera
todavía abierta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El síntoma era «no hay agente atendiendo» y después «arranqué shuma-daemon y en 10 s no atendió
ningún socket», con un daemon de sergio VIVO al lado. La causa: esta caja no tiene logind, así que
nadie crea /run/user/<uid> ni exporta XDG_RUNTIME_DIR, y entonces cada programa elige su propio
repliegue y dejan de encontrarse — el tui buscaba en `$XDG_RUNTIME_DIR/shuma.sock`, o sea en la nada,
mientras el daemon abría `/tmp/shuma-<uid>.sock`. Dos repliegues distintos para el mismo acuerdo.
Ahora /etc/profile.d exporta XDG_RUNTIME_DIR y la card de arranque crea /run/user/<uid> para cada
cuenta con home. Comprobado en una sesión de login de verdad: XDG=/run/user/1001, el tui levanta su
agente ahí y lista los conjuntos guardados (shuma 4 puestos, telefono-864, los diag…), que estaban en
disco y no se perdieron al reiniciarlo.
Entra también la card del volumen de trabajo —esta caja no tiene fstab y el init no monta discos
nuevos— y quedan versionados /etc/profile y los dos profile.d, que hasta ayer no los leía nadie
porque /etc/profile no existía.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1) `publicar-webs.sh` corría como root y hacía el `git pull` igual: git dejó 24 entradas de root
dentro del .git de /work/sergio/tawasuyu —refs, logs, config, directorios de objetos— y el dueño se
quedó sin poder ni hacer `fetch` («unable to append to .git/logs/refs/remotes/origin/main»). El clon
quedó congelado 21 commits atrás sin que nada fallara del lado de root. Ahora el pull va COMO EL
DUEÑO, y los cuatro clones quedaron con sus permisos.
2) `cc-por-zig.sh` elegía el rust con `ls -d …/*-rust | tail -1`, que es el último ALFABÉTICO:
certificaba `fe277b32…` mientras la jaula usaba el vigente `6441302d…`. Dos artefactos distintos, uno
certificado y otro en uso. Ahora pregunta por el hash vigente de la receta y sólo cae al más reciente
—con el nombre exacto— si no puede. Lo detectó el agente comparando los dos guiones.
3) `rustdoc` entra en `tools`: el artefacto salía con cargo y rustc y SIN rustdoc, o sea que en una
caja takana `cargo doc` no existe. `docs = false` se queda —la documentación de la std es otra
decisión, cientos de MB—. Radio medido: 0 dependientes de build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Misma causa y mismo síntoma que el `/.dev-fs` de esta tarde: `/store/` sólo ignora un directorio, y
un clon que quiera construir necesita `store -> /store` (así lo tiene /opt/takana). El enlace
aparecía como `?? store` en cada `git status`, que es ruido permanente en un repo donde varios
agentes leen ese status para separar lo suyo de lo ajeno.
Los encontró la corrida en el worker, y los tres estaban disfrazados de resultado.
1. El `exit 77` se insertaba antes de la ÚLTIMA `'''` del fichero, que no siempre cierra una fase:
una receta puede traer bloques `'''` después (un `[[service]]`, una nota larga). En `dwarves`,
`elfutils` y `elfutils-libdw` cayó fuera de toda fase, el build corrió entero y SELLÓ — con el
hash movido por el `set -e`, o sea tres artefactos basura. Ahora se rastrea la anidación.
2. El veredicto se leía con `grep "exit 77"` sobre el log, y takana IMPRIME el script de la fase:
esa cadena aparece siempre porque la inyectamos nosotros. Daba SOBREVIVE hasta a un build que
selló. Ahora se ata a `build phase falló (exit 77)`, que es el estado de salida de verdad.
(Comprobado a posteriori que los 14 SOBREVIVE de la corrida buena sí habían salido 77.)
3. Sin `--store`, takana usa su default `/store`, que en el worker es un store de SCRATCH casi
vacío — reconstruía el mundo en vez de reusar el de la granja (`./store`, 908 artefactos). Ahora
el store va por `STORE`.
Y un guardián para lo que no debería volver a pasar: si un build SELLA, se clasifica
`SELLO-INDEBIDO` y se grita el hash, en vez de contarse como un resultado más. Los tres artefactos
basura ya se borraron del `/store` de scratch del worker; el store de la granja nunca se tocó.
Medido desde la jaula, con el hash forzado para que no hubiera caché: `anew`
(Go, estática) sella en 6,8 s y `lz4` (C, con `make install` escribiendo /out)
en 4,4 s. Con `root = false` el bwrap anidado se levanta y /proc monta.
Lo del uid 0 sigue siendo cierto para una receta cuyo `install` fije DUEÑOS, y
así queda escrito — pero no es «los builds no corren», que es lo que se leía y
lo que mandaba a pedirle cada receta al anfitrión por ssh.
Los diez envoltorios daban verde porque el control se corrió EN LA CAJA, que es
musl. Adentro de la imagen de Arch, `rustc`, `cargo` y `python3` —las tres musl
DINÁMICAS— mueren con `cannot execute: required file not found`: su PT_INTERP
pide `/lib/ld-musl-x86_64.so.1` y ahí no está. `LD_LIBRARY_PATH` no puede
taparlo, actúa después de que el cargador cargue. Ahora se invoca el cargador
del lab explícitamente, y sólo si la caja no tiene el suyo: el mismo guion sirve
en los dos sitios sin ramificar.
Segundo defecto, del mismo viaje: `zig cc` a secas no sirve de `cc` para cargo.
cc-rs le pasa su propio triple y zig no lo entiende —`unable to parse target
query 'x86_64-unknown-linux-musl'`—, lo que tumba `blake3` y con él medio árbol
de tawasuyu. Los dos drivers (el de la caja y el de la jaula) lo descartan.
Las banderas de enlace van dentro del envoltorio de `rustc` y no en
/etc/profile.d: la jaula no corre shells de login, así que allá el fichero
quedaría presente, con contenido y sin efecto.
Y los controles ahora COMPILAN en vez de preguntar `--version`, que no tocaba ni
el driver de C ni el enlazador y por eso no vio ninguno de los dos defectos.
Medido con esto puesto y sin una sola variable a mano:
`cargo check -p tejido` sobre tawasuyu, verde en 1m13s.
Buscando si los cambios de atuq habían llegado a la caja apareció esto: el hub y el worker
calculaban `b3:e556024b` y un clon limpio `b3:5156cb8f`, con los 52 ficheros versionados de
`recipes/atuq/` byte a byte IGUALES en las tres máquinas y la receta con el mismo md5.
La diferencia era un fichero de más: `tools/__pycache__/rebrand.cpython-314.pyc`, bytecode que dejó
alguien al correr `rebrand.py` dentro del repo. Comprobado en los dos sentidos — metiendo y sacando
ese .pyc en un clon limpio, el hash salta de `5156cb8f` a `e556024b` y vuelve.
Lo traicionero es que `__pycache__/` está en `.gitignore`: el árbol se ve limpio en `git status`
mientras el hash ya es otro. Y el artefacto sellado en el store era el de `e556024b` — o sea uno que
un clon limpio no puede reproducir jamás. Es la regla 3 al revés: no un vacío que pasa por lleno,
sino basura invisible que pasa por fuente.
`ArtifactHash::of_tree` no sabe de `.gitignore`, y son dos cosas legítimamente distintas; pero hoy
divergen por descuido y no por decisión. Queda escrito en la receta, con la comprobación barata al
lado, para las cuatro que usan `source.dir`. El `.pyc` se barrió del hub y del worker: las tres
máquinas ya convergen en `5156cb8f`.
La siembra de código hub→worker de `cosecha-cron` corre con `--delete` cada 30 minutos. Las copias
del experimento viven en `recipes/` por obligación (takana resuelve las deps relativas al directorio
de la receta), así que una tanda larga en el worker las vería desaparecer a mitad. Con `REC` se
apunta a una copia entera de `recipes/` fuera del árbol sembrado y el problema no existe.
Tres cosas mías que sólo se vieron al correrlo allá. El lock estaba cableado a `/work/.farm-build.lock`,
que es la ruta de la CAJA (donde `work` es symlink a /work); en el worker `work` es un directorio de
verdad y flock moría con `cannot open lock file`. `takana` no está en el PATH del worker, que lo
invoca por `./target/release/takana`. Y el techo de 420 s alcanza para las ligeras pero no para llvm,
firefox ni los kernels, así que ahora es `TLIMITE`.
Todas por entorno y con el valor de antes por defecto, así que la corrida de la caja no cambia.
`/.dev-fs/` sólo ignora un directorio. Desde que el lab vive fuera del árbol y se engancha con un
enlace (`.dev-fs -> /work/dev-fs`, que es como está en /opt/takana y como lo necesita cualquier clon
que quiera construir), el enlace aparecía como `?? .dev-fs` en cada `git status`. Ruido permanente
en un repo donde varios agentes leen ese status para ver qué es suyo y qué es de otro.
El diseño es la parte que vale: NO SELLA NADA. Modificar una receta le cambia el ArtifactHash, así
que medirlo a lo bruto habría dejado una docena de duplicados basura en un store que se respalda. En
vez de eso se inyecta `set -e` en cada fase Y un `exit 77` al final de la última: el build siempre
falla, nunca sella, y el veredicto se lee en el código de salida — 77 significa que todas las fases
corrieron enteras bajo set -e. Queda en scripts/farm/exp-ec.sh.
Muestra de 12 recetas ligeras (los pesos pesados quedan fuera por tiempo, y el sesgo va declarado):
10 sobreviven sin tocar nada, 1 se rompe, 1 inconcluso por timeout.
El que se rompe es `e2fsprogs`, y mirarlo de cerca cambia lo que significa: su ./configure aborta con
`external uuid library not found` pese a que la receta pasa --disable-libuuid, y hoy eso se traga
—comprobado: la misma receta sin set -e y con exit 77 LLEGA al 77—. Pero el artefacto que sella está
bien: 149 ficheros con e2fsck, mke2fs, los cuatro fsck.ext* y los mkfs.ext*. O sea que ahí `-ec` no
atrapa un bug: rompe una receta que funciona. Ése es el coste, y es el número que no se podía
adivinar leyendo: ~8%, unas 8 recetas sobre las 96 expuestas.
También se corrige el recuento: 96, no 89. El 89 salía de restar 142−53 dando por hecho que las 53
con `set -e` propio estaban todas dentro de las 142, y no lo estaban.
jaula-preparar suma lo que hizo falta para poder correr todo esto desde adentro: rsync/python3/jq/
cargo enlazados del store, y el cargador de musl, sin el cual python3 y cargo no arrancan en una
imagen glibc.
El agente de adentro reportó que le faltan cargo, rustc, make, pkg-config y python3. La salida obvia
—sumarlos a `packages` y `qorpa provision`— hoy NO funciona: con un mapa de un solo id, pacman no
puede chownear su directorio de descarga y muere en «failed to chown temporary download directory»,
que es exactamente la consecuencia que el aviso de qorpa viene anunciando.
Y no hace falta: las diez herramientas YA están selladas en el store, son musl estáticas y corren
dentro de la imagen glibc. Usarlas es además lo coherente con la distro —el corpus es el que provee,
no Arch—. Quedan como envoltorios en /work/<usuario>/bin, que el lanzador pone en el PATH junto con
el go y el zig del lab.
Dos errores míos que el propio control destapó y quedan escritos: el glob `/store/*-make` también
casa con `…-cargo-make` (eligió ése y `make` moría en un `exec` que no nombraba la causa) — ahora el
nombre se compara entero quitándole el hash; y el control daba ✓ a todo porque `cmd | head` devuelve
el estado de head, o sea siempre 0.
Probadas las diez: make 4.4.1, pkgconf 2.5.1, python3 3.12.10, jq 1.8.1, rsync 3.5.0, git 2.54.0,
curl 8.20.0, takana 0.0.1, rustc y cargo 1.97.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo reportó el agente de adentro y muerde en el primer build: `takana build` invoca `go` y `zig` POR
NOMBRE, y en la imagen de la jaula no están. Muere con «spawn go mod vendor: No such file or
directory», un mensaje que no menciona el PATH y manda a buscar el problema donde no está. Los dos
viven en /work/dev-fs/tools, que ya estaba concedido — sólo faltaba nombrarlos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El bloque decía «⚠ NO instalado en el crontab» y era cierto y ruidoso a la vez: desde que el latido
lo sostiene el cron de root del ANFITRIÓN, un agente enjaulado no ve ese crontab ni lo verá nunca,
así que el aviso saltaba en TODAS las corridas desde la jaula. Un aviso que salta siempre es un
aviso que nadie lee, y tapaba el caso real —que el latido se pare— con ruido permanente.
Ahora se comprueba el efecto: el último commit de cosecha y su edad. Dispara en :00 y :30, así que
más de 40 minutos sin commitear es un latido perdido y se avisa; si no, dice «✓ al día» con la edad
exacta. El crontab se sigue mirando, pero sólo para SUMAR información cuando está, nunca para
avisar cuando falta.
Medido desde la jaula: «último ciclo: hace 18m 54s — estado: cosecha granja 2026-09-18T17:31:27Z».