Consulta solo las recetas que HOY no tienen licencia, asi que su cosecha MENGUA
a cada pasada: lo sembrado ayer ya no sale en --faltan. Y reescribia el fichero
entero con la cosecha del dia. Hoy una pasada con CERO detecciones dejo
docs/licencias-detectadas.tsv en 0 filas y se llevo las 610 que documentaban de
donde salia cada licencia ya sembrada.
Salio con exit 0 diciendo «escrito» y «sembrar con». Un vacio que llega hasta el
final afirmando que todo fue bien — la regla 3 de CLAUDE.md, esta vez sobre el
registro de procedencia en vez de sobre un artefacto.
Ahora FUSIONA con lo registrado (la fila nueva gana) y se niega a sobrescribir
si el resultado pierde filas: un registro de evidencia que encoge es un fallo,
no un resultado. Con CON=0 lo dice y no invita a sembrar nada.
Y las cuatro de HashiCorp quedan declaradas: BUSL-1.1.
No es adivinado ni sale de la API —que devuelve NOASSERTION en las cuatro, y por
eso llevaban meses sin licencia—: es el texto del LICENSE del TAG QUE LA RECETA
PINEA, no el de la rama por defecto. consul v1.22.7, vault v1.21.4, nomad
v1.11.3 y packer v1.15.4 abren con «Business Source License 1.1».
⚠ BUSL-1.1 NO es una licencia libre: prohibe el uso en produccion que compita
con el producto de pago, y solo pasa a MPL-2.0 cuatro años despues de cada
version. Cuatro paquetes del catalogo publicable no son redistribuibles como si
fueran libres.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn
Media `df` sobre el repo y nada mas. Pero las recetas Rust no gastan solo ahi:
`cargo vendor` copia DESDE `~/.cargo/registry`, que en gioser esta en `/` (74 G)
y no en `/mnt/vvv` (255 G). Hoy eso eran 138 G libres segun el vigia y 19 G de
verdad — luz verde mientras el que se llenaba era el otro, y llenar `/` no
rompe una tanda, rompe la maquina.
Ahora toma el MINIMO de los dos FS (el del repo y el de CARGO_HOME). Si
comparten FS, `df` devuelve lo mismo dos veces y es inocuo.
La purga tambien se queda corta y crece:
- `work/repos/*`, los clones --mirror de gitea de las hub-only. Se re-clonan.
- `$CARGO_HOME/registry` como ULTIMO recurso y CON GUARDA: el act-runner de
tawasuyu corre como el mismo usuario y comparte ese ~/.cargo. Purgarlo con un
`cargo` ajeno en vuelo le arranca los ficheros a SU compilador. Se pregunta
por el codigo de salida de `pgrep`, no por su stdout.
De paso queda dicho en la cabecera que CARGO_HOME se vigila: en gioser el
driver se lanza con el suyo propio dentro del volumen, y asi hammer deja de
gastar `/`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn
La tanda del 2026-08-13 ABORTO con 7 G libres despues de haber purgado a los
25: una sola receta se comio >18 G entre purga y purga. Son los cargo vendor
de las recetas Rust grandes, que sueltan varios GB cada una.
Con 40/15 la purga ocurre ANTES de que una receta gorda agote lo que queda,
en vez de justo despues. El guardian de aborto hizo su trabajo —paro sin
llenar el disco y sin corromper nada— pero llegar a el significa perder la
tanda; el objetivo es no llegar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cadena probada de punta a punta (2026-08-12):
1. Se añadio `bzip2-dev` al lab para que CPython tuviera sus modulos.
2. freetype AUTODETECTA bzip2 —la receta desactivaba harfbuzz y brotli de
forma explicita, pero no este— y lo grabo en su freetype2.pc:
`Requires: zlib, bzip2, libpng`.
3. Toda receta que enlaza freetype con `-all-static` resuelve ese Requires
via pkg-config y pide `-lbz2`.
4. Los `-dev` de Alpine traen `.so` pero NO `.a` ⇒ «unable to find static
system library 'bz2'».
5. Cayo fontconfig y con el 27 recetas: KDE, GNOME y COSMIC dependen de el.
Verificado: el freetype nuevo vuelve a `Requires: zlib, libpng` y fontconfig
SELLA (8290b402...).
Se arregla en la receta y NO quitando bzip2-dev del lab: esto mueve el hash
de freetype y sus dependientes; lo otro re-hashearia el corpus ENTERO por
tercera vez en un dia. freetype solo usa bz2 para fuentes PCF comprimidas.
⚠ Metodo, porque me costo tres hipotesis fallidas: hay CINCO artefactos de
freetype en el store y mirar uno al azar dio el `.pc` equivocado. Y al
apartar bzip2.pc el error cambio a «freetype2 not met» — eso era evidencia A
FAVOR (pkg-config no podia resolver el Requires) y lo lei como si fuera en
contra. Comprobar el hash VIGENTE, no el primero que lista `ls`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
util-linux —perfil BASE— no construia: `zig: error: unsupported option
'-mcpu=' for target`. En modo `-dM -E -` zig traduce `-mcpu=baseline` a un
`-mcpu=` VACIO y lo rechaza, asi que `errnos.h` no se generaba.
La receta YA trae un apaño para esto: parchea sus generadores all_errnos y
all_syscalls para filtrar el flag. Pero filtra `"$@"`, y el flag NO viene ahi
— lo inyecta el wrapper DESPUES, en el `exec`. El apaño quedo inerte cuando
se introdujo el wrapper, y no hay forma de esquivarlo desde una receta.
Omitirlo con `-E` es inofensivo por construccion: preprocesar no genera
codigo, asi que no hay CPU al que apuntar.
⚠ MISMO PATRON QUE make/--export-dynamic: el wrapper cambia lo que producen
los builds FUTUROS sin mover ningun hash (no esta en hash_inputs), asi que la
rotura quedo congelada por cache-hit hasta que el corpus se reconstruyo. Este
cambio tiene la misma propiedad ⇒ se hace AHORA, con el corpus a medio
reconstruir y casi nada sellado, no dentro de un mes.
Verificado: util-linux sella (370530ff...).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Con un worker el corpus son ~7 dias (medido: ~9 min/receta sobre 1161).
Repartirlo es la unica forma de bajarlo, y no hace falta planificador
central: cada worker construye SU parte y las deps que le falten se las
construye `hammer build` solo. Al cosechar todo converge en el mismo store
porque las direcciones coinciden — eso lo PRUEBA el paso 4 de
farm-lab-sync.sh, no se supone.
El reparto es por INDICE (i-1, i-1+N, ...) y no por bloques contiguos: el
orden ya esta por objetivo, asi que bloques contiguos le darian a un worker
todo el tramo GUI —el mas lento— y a otro solo hojas rapidas. Intercalando,
todos avanzan por el mismo terreno a la vez.
⚠ Hay trabajo DUPLICADO y es deliberado: dos workers con recetas que cuelgan
de qtbase lo construyen los dos. Sale a cuenta frente a serializar, pero con
4 workers no son 4x, son ~3x.
Un log por shard, o se pisan. Verificado: los 4 shards suman 1161 exactas y
rechaza 5/4 y `abc`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sincronizar el codigo nuevo al worker y llamar a cargo NO basta: rsync -a
preserva el mtime del hub, asi que el fuente recien llegado puede quedar mas
VIEJO que el binario que el worker compilo hace un rato. cargo dice
«Finished in 0.09s», el worker se queda con el hammer anterior, calcula la
huella vieja y el paso 4 falla sin que se vea por que.
Paso el 2026-08-12 al empujar el lab con los -dev: el worker tenia el lab.rs
correcto Y el rootfs correcto, y aun asi divergia. Se arregla con un `touch`
a los fuentes antes de compilar.
El guardian del paso 4 hizo su trabajo: detecto la divergencia y NO dejo
construir. Sin el, el worker habria molido horas sellando en direcciones que
el hub nunca iria a buscar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sha256 3c9c1ef3... reemplaza a db8a3252... La anterior producia un python3 sin
_ctypes ni _curses y con el se caia la familia GNOME/KDE entera.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decision del usuario. CPython construye sus modulos opcionales como .so
COMPARTIDOS y las .a del corpus NO son PIC («relocation R_X86_64_PC32 against
symbol 'stdscr'»), asi que declararlas en [deps] NO funciona: se probo una
por una. Las libs del rootfs son compartidas y si sirven.
Añadidos: libffi-dev ncurses-dev readline-dev sqlite-dev bzip2-dev xz-dev
expat-dev. Resultado VERIFICADO: el guardian de python3 importa los OCHO
modulos (_ctypes _curses readline sqlite3 bz2 lzma zlib pyexpat) y pasa. De
paso `libffi` sale de [deps]: ya lo aporta el lab.
openssl-dev NO va, deliberado: se de-Alpinizo porque el host-tool del kernel
enlaza el openssl del corpus desde el overlay; devolverlo podria cambiar el
artefacto del kernel. python3 sigue sin `ssl`.
Y SE CIERRA EL AGUJERO QUE ESTO ABRIA. Esas libs del rootfs ahora entran en
el CONTENIDO de un artefacto, asi que su version es parte de su identidad ⇒
van a TOOLCHAIN_PREFIXES. Sin eso habriamos reabierto en pequeño el mismo
fallo que 58d3161 cerro: instalar los -dev subio ncurses 6.5→6.6 y readline
8.3.1→8.3.3 —sin tocar gcc/rust/musl, lo confirmo el lock— y ese salto habria
cambiado el python3 producido SIN mover su direccion.
Coste: la huella del lab cambia ⇒ el corpus se re-hashea otra vez. Se hace
AHORA a proposito, con 194 artefactos, no con el corpus a medio construir.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`du -s hammer/store/*` incluia `.dmerge`, la cache transitoria de fusion del
store: 48 GB en el box el 2026-08-12. Recorrerla hacia que --listar tardara
lo bastante como para cortarse por timeout, y entonces el box "devolvia 0
artefactos" — el guardian de los 0 hizo bien su trabajo y se nego a pisar el
manifiesto, pero la causa no era el enlace.
Con `store/[0-9a-f]*` el listado baja de cortarse a 3 s, y de paso descarta
en el origen `.bootstrap-tmp`/`.mirror-tmp`, que nunca son artefactos. Un
artefacto SIEMPRE empieza por hex: filtrar en el origen sale mas barato que
traerse la basura y descartarla despues.
Verificado: 2037 artefactos, 0 vacios.
⚠ Aparte: que .dmerge ESTE en el respaldo es en si un problema — son 48 GB de
cache reconstruible ocupando el box, y el propio script la excluye al subir
(`--exclude /.dmerge`). Llego por otra via. No se toca aqui: borrar en el
destino es una decision deliberada y aparte.
Y una limitacion del guardian de vacios que conviene conocer: durante una
SUBIDA en curso, los directorios a medio escribir son indistinguibles de los
rotos. Reporto 101 vacios a mitad del rsync y 0 al terminar. No es un falso
positivo del filtro: es que la pregunta no tiene respuesta estable mientras
se escribe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sus deps.build resuelven contra el directorio hermano, que en .deferred no
existe: fallan al instante con «no pude cargar la dep de build 'zlib' de
'git' (recipes/incoming/.deferred/zlib.toml)». Son 25.
Intentarlas no es solo inutil: mete 25 FALLA en el log que parecen roturas
del corpus y esconden las de verdad. Un log lleno de fallos esperados es un
log que nadie lee.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Estado VERIFICADO: con libffi el build sella y el guardian de la fase install
imprime «modulos opcionales: OK» ⇒ _ctypes y zlib importan de verdad.
El guardian es la pieza que mas vale de este commit: `configure` de CPython
OMITE en silencio cada modulo opcional cuyos headers no encuentra, `make`
termina 0 y el artefacto se sella mutilado. El error aparece dias despues en
OTRA receta (gjs, gdm) como ModuleNotFoundError y no se parece a su causa.
Importarlos en install lo convierte en un fallo en la receta correcta.
Lo que NO se pudo añadir, probado uno por uno y documentado en la receta:
openssl → el del corpus es libcrypto ESTATICA sin threads; CPython aborta
con «OPENSSL_THREADS is not defined». Sin modulo `ssl`.
ncurses → `.a` no-PIC: _curses_panel.so no enlaza
(«relocation R_X86_64_PC32 ... against symbol 'stdscr'»).
sqlite/bzip2/xz/readline/expat → mismo muro no-PIC (_lzma lo destapo).
CPython construye sus modulos opcionales como .so COMPARTIDOS, y las .a del
corpus no son PIC. El python3 viejo tenia _ctypes Y _curses porque se
construyo contra las libs COMPARTIDAS del rootfs de entonces; el lab anclado
trae los runtime pero no los headers.
⇒ _curses sigue roto y con el la familia GNOME. Arreglarlo pide DECIDIR:
meter los -dev en la imagen del lab (re-hashea el corpus otra vez, barato
AHORA que van 48 de 1186) o hacer recetas PIC de esas libs (frente nuevo).
Queda para el usuario.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Destapado reconstruyendo el corpus: gjs y el build de mozjs morian con
`ModuleNotFoundError: No module named '_ctypes'`. La causa no estaba ahi: el
artefacto de python3 se sella SIN ese modulo y arranca perfectamente; el
fallo aparece mucho despues, en otra receta. 310 recetas dependen de python3.
El rootfs del lab trae libffi (runtime) pero NO sus headers — no hay
libffi-dev en el lock. El configure de CPython no encuentra ffi y omite
_ctypes sin fallar. recipes/libffi.toml si instala headers y .pc, y pkgconf
—ya declarado— los ve en la capa overlay.
Por que aparece AHORA y no antes: el artefacto viejo de python3 tenia _ctypes
porque el rootfs de entonces traia los headers, y el cache-hit lo mantuvo
congelado hasta que el corpus se re-hasheo. Mismo patron que
make/--disable-load: reconstruir destapa lo que la cache sostenia.
Se arregla ahora y no mas tarde a proposito: mover el hash de python3 mueve
el de sus 310 dependientes, y con solo ~48 recetas selladas eso es barato.
Dentro de dos dias no lo habria sido.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Primera vez que el armador se usa para lo que existe, con artefacto real al final:
b3:8ffd57109a2f7102c1bbbbc91cf0e729047312eee1a4e3fdb8b178680cc94264-linux-gioser
linux-metal derivada gioser
bzImage 14,5 MB 11,1 MB (-23%)
símbolos encendidos 1805 1647
diff-back contra el artefacto ... 35 cumplidos, 0 INCUMPLIDOS
gate contra el artefacto ........ ningún dispositivo en uso sin driver
La base la eligió el probe, no la intuición: linux (el de QEMU) apaga USB, HID e INPUT a
propósito y gioser tiene xhci_hcd, usbhid e i8042 bindeados ⇒ linux-metal. 13 bundles y 3
perillas, 34 banderas, clausura de 3054.
Segunda validación de la clausura contra el olddefconfig real, ahora sobre otra base y con
13 bundles: 154 aciertos, SOBRA 0, faltan 18 (todos de la clase «apagado ahora ≠
inalcanzable» del §12).
LA PRUEBA DE 30 s PREDIJO EL BUILD DE 45 MIN. El .config del artefacto y el de la prueba en
seco difieren en 14 líneas y NI UNA es funcional: son las siete versiones del toolchain que
el kernel graba en su propio config (CC_VERSION_TEXT, GCC_VERSION, AS_VERSION, LD_VERSION,
RUSTC_VERSION, RUSTC_LLVM_VERSION, PAHOLE_VERSION). Cero símbolos de diferencia ⇒ la prueba
barata es un sustituto fiel para todo lo que el armador decide.
Y de regalo, la mecánica exacta de por qué el lab TIENE que estar en hash_inputs: el kernel
graba la versión de su compilador DENTRO del .config, y el .config va dentro del artefacto.
No es que el lab «influya» en el resultado — es que el lab está literalmente en el
contenido sellado. Se ve línea a línea: gcc 15.2.0 del lab anclado vs 16.1.1 del host.
PAHOLE_VERSION=0 confirma además lo que dice la cabecera de linux.toml.
La receta derivada NO se deja en recipes/: build-state.py y yupana barren recipes/ y
recipes/incoming-*, así que dejarla ahí la mete en el grafo compartido como deuda. Va en
docs/state/kernel-plans/ con el comando que la regenera; para construir se copia a
recipes/incoming-kernel/ temporalmente y se saca después.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El gate que había mira lo que apaga el PLAN, así que sólo ve regresiones que introduce el
plan: un hueco que ya venía en la receta base le pasa por debajo. El modo nuevo compara
DOS configs y define regresión como «funcionaba y dejó de funcionar», que es la
formulación literal del #6 del handoff:
hammer kernel gate --config <producido> [--baseline /proc/config.gz] --objective X
El referente por defecto es /proc/config.gz: el kernel que arrancó esta máquina es la
prueba viva de qué hace falta para arrancarla.
Y comparar DOS configs, en vez de mirar sólo el nuevo, mata de raíz un falso positivo que
tenía: los nombres de módulo cortos colisionan. El driver que /sys llama `usb` mapea a
QE_USB (el USB de las QUICC Engine de Freescale) y `port` a PORT_CHAN. Mirando sólo el
config nuevo aparecen como perdidos y el gate bloquearía un plan sano; exigiendo que
estuvieran encendidos en el referente, el falso positivo se cae solo. Con test.
Probado contra gioser (Hetzner vServer, 38 drivers bindeados) partiendo de linux-metal:
destapó cuatro pérdidas que NINGÚN bundle causaba — aer, iTCO_wdt, lpc_ich y pcspkr están
encendidos en el kernel que corre y linux-metal no los enciende nunca. El gate viejo no
podía verlas por construcción.
De paso, un mensaje que mandaba a buscar donde no está: sin culpable atribuido decía «lo
apaga una perilla», cuando la causa es que la receta base no lo enciende.
Catálogo, tres entradas nuevas nacidas de medir esta máquina:
· bundle sin-gpu-intel — DRM_I915 es de los drivers más grandes del kernel y no sirve en
una VM con virtio-gpu. NO apaga DRM: el vídeo sigue por simpledrm/EFI o virtio-gpu.
· knob invitado-virtio — VIRTIO_BALLOON y HW_RANDOM_VIRTIO no vienen en el defconfig y
ninguna receta del repo los enciende; en gioser los dos están BINDEADOS. Un kernel sin
ellos arranca, pero la VM pierde el globo de memoria y la entropía del anfitrión.
· knob plataforma-pc — PCIEAER, LPC_ICH, INPUT_PCSPKR y el watchdog ITCO_WDT. El
watchdog necesita además WATCHDOG, que linux-metal apaga a propósito: por eso va en una
perilla y no en la base. En una máquina sin acceso físico, el watchdog es lo que la
reinicia cuando se cuelga.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dos fallos que se destaparon montando el primer worker con el lab anclado
(2026-08-11).
1. `--exclude /work` con barra inicial ancla SOLO la entrada `work`, no su
contenido. Con `--include '/work/'` delante, rsync entraba al directorio y
sus hijos no casaban con ninguna regla ⇒ se copiaba work/ ENTERO: 3,7 G,
incluidos los mirrors de git A MEDIO COPIAR porque el propio rsync abortaba
antes de terminarlos. El worker fallaba cada receta con `git fetch exit
128`. Un work/ a medias es peor que ninguno: parece que esta. Se ancla con
`/work/**`.
Eso mismo causaba el abort: con --delete, rsync moria con «cannot delete
non-empty directory» sobre los vendor/ de cargo, que quedan de SOLO
LECTURA. Sin tocar work/, no hay nada que borrar ahi.
2. El `set -e` hacia que ese fallo abortara el script ANTES de armar el
dead-man switch ⇒ server VIVO que no puede autodestruirse, que es
exactamente lo que este script declara inadmisible. Ahora el rsync no es
fatal: avisa y sigue hasta armarlo. El orden ideal es armar el dead-man
ANTES de cualquier paso que pueda fallar; queda anotado en el codigo.
Verificado en seco contra un worker real: el patron nuevo no toca work/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
farm-sync excluye /.dev-fs a proposito (el lab es del entorno, no del repo).
Eso valia cuando el toolchain no entraba en el hash. Desde 58d3161 si entra,
asi que un worker con el lab horneado de la golden (rustc 1.96) sella en
direcciones DISTINTAS a las del hub (1.97): no es que compile distinto, es
que lo guarda donde el hub nunca lo va a buscar.
Tampoco basta bootstrap-devfs.sh en el worker: su paso 0 solo trae la imagen
si NO hay rootfs, y la golden trae uno. Se reemplaza a la fuerza.
La imagen se EMPUJA por scp desde el hub en vez de bajarla del Storage Box,
para no poner la llave del box en el worker: el modelo hub-and-spoke dice que
el worker es compute puro sin secretos.
El paso 4 no es 'extraje la imagen', es comparar el hammer hash de una receta
testigo entre worker y hub. Si divergen FALLA: un worker que sella en otra
direccion quema dinero produciendo artefactos que nadie encuentra.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sintoma: al reconstruir el corpus, bash/binutils/bison/busybox/zlib morian
con exit 139. dmesg lo nombraba: `traps: make general protection fault in
ld-musl-x86_64.so.1`.
Causa exacta, leida de la linea de link real:
hammer-zig-cc -g -O2 -Wl,--export-dynamic -static -o make src/*.o
GNU make 4.4.1 soporta cargar objetos en runtime (directiva `load`), asi que
su configure anade -Wl,--export-dynamic. El wrapper hammer-zig-cc tiene la
regla —correcta— de que `-static` CEDE ante cualquier link que exija dinamico,
y --export-dynamic es uno de esos marcadores ⇒ le quitaba el -static y se
sellaba un make dinamico que segfaultea. Como toda receta autotools invoca
make, se propagaba a casi todo el corpus.
El arreglo va en la receta y NO en el wrapper: el conflicto es de origen
—pedimos estatico de un proyecto que pide symtab dinamica para una funcion
que no usamos—. Ensenarle al wrapper a ignorar --export-dynamic lo romperia
para gobject-introspection, que si lo necesita.
POR QUE NO SE VIO ANTES, que es la leccion: el artefacto viejo de make era
estatico y estaba congelado por cache-hit, probablemente desde antes de que
existiera esa regla del wrapper. Nadie lo reconstruyo hasta que el corpus se
re-hasheo entero. Un cambio que "no re-hashea nada sellado" igual cambia lo
que producen los builds FUTUROS, y eso queda invisible hasta que algo fuerza
la reconstruccion.
Verificado: make ESTATICO, corre (GNU Make 4.4.1), y zlib —que fallaba con
139— sella.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hace falta cada vez que cambie la imagen del lab, porque desde 58d3161 el
toolchain entra en hash_inputs y eso mueve el hash de TODO el corpus. Es el
precio explicito de cambiar de compilador; antes se pagaba sin enterarse.
EN SERIE bajo el flock compartido (ADR 0012): dos builds que compartan una
dep se pisan el arbol de fuentes y queda roto para siempre.
REANUDABLE sin estado propio: pregunta `hammer hash --check` (~2 ms) en vez
de llevar un fichero de progreso. Matarlo y relanzarlo continua donde iba.
POR OBJETIVO, no alfabetico: hay 1186 recetas y solo 168 alcanzan una imagen.
Ordenado por targets.toml primero, cada perfil que cierra es utilizable ya;
por orden alfabetico, tras un dia de maquina no habria ni una imagen.
Dos correcciones en el camino: usar los nodos de build-state marcaba 1161 de
1161 como prioritarias (ese grafo ES el corpus entero, no las clausuras), y
`glob('recipes/**/*.toml')` se dejaba 25 ficheros fuera — os.walk da 1186,
que es lo que cuenta `find`.
La purga de work/sources va ENTRE recetas, con nada corriendo. Purgar con un
build en vuelo le arranca los ficheros al compilador: paso el 2026-08-10 y se
llevo un kernel a medias.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Al entrar el toolchain en hash_inputs (58d3161) se abrio un agujero
operativo: los repos del rootfs apuntan a Alpine edge, que es RODANTE, asi
que dos maquinas que corran bootstrap-devfs.sh en fechas distintas resuelven
toolchains distintos y NO COMPARTEN NI UN ARTEFACTO. Ni cosecha de granja, ni
mirror pull, ni un cache-hit. Cada worker efimero nacia con un lab propio.
`apk add` contra edge es irrepetible por diseno (edge sirve solo la ultima
version). La salida no es repetir la resolucion, es no repetirla: el rootfs
se construye UNA vez y se TRAE pineado por sha256 — el mismo trato que ya
reciben alpine-minirootfs y zig en este script.
imagen: 294 M comprimida (1,1 G extraida)
sha256: db8a32526ad41fbcc76223842dbb4eb65c8ed576570b82152dc7c5691fc2a349
EL TAR ES DETERMINISTA A PROPOSITO (--sort=name --mtime=@1 --owner=0
--numeric-owner): sin eso el sha depende del orden del directorio y del uid
de quien empaqueta, dos empaquetados del MISMO rootfs darian imagenes
distintas y no habria forma de verificar que una imagen es la que dice ser.
Comprobado: dos pasadas, mismo sha.
EL REEMPLAZO ES POR ROTACION, no extrayendo encima: a medias quedaria una
MEZCLA de dos rootfs, que es justo lo que esto existe para evitar.
VERIFICADO de punta a punta: bajada del box, sha intacto tras el viaje,
extraida, y la huella de lab que produce es IDENTICA a la del rootfs vivo
(zlib -> b3:c7a7338f... en las dos). O sea que una maquina que arranque de
esta imagen comparte store.
--from-scratch conserva el camino viejo: es como se FABRICA una imagen nueva.
Reconstruir el corpus es el precio de ese cambio, y ahora es explicito.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decision del usuario tras la medicion de los 4 kernels: dos labs con distinto
rustc producian bytes distintos en la MISMA direccion, y el store no tenia
como notarlo. Ahora el toolchain es una entrada del ArtifactHash.
QUE ENTRA: 29 paquetes del rootfs cuya VERSION puede cambiar los bytes —
compiladores/enlazadores (gcc, clang, llvm, binutils, rust, cargo), las libs
de codegen de gcc (gmp, mpfr4, mpc1, isl), el runtime que se enlaza (musl,
libgcc, libstdc++, libatomic, libgomp) y los headers que se compilan dentro
(linux-headers, fortify-headers). NO entra el rootfs entero: cada paquete de
mas invalida el corpus en cada bump, y con edge rodante curl se actualiza sin
que cambie una sola instruccion emitida.
Quedan fuera a proposito, y no es una afirmacion de que no influyan: los
autotools y las shells pueden cambiar ficheros generados. Es una decision de
coste. Si algun dia se ve una divergencia que rastree ahi, se anaden — y ese
dia el corpus se re-hashea otra vez.
DE DONDE SALE: del apk db del rootfs REAL (cfg.rootfs, que respeta
HAMMER_LAB/HAMMER_ROOTFS), no de docs/state/lab-toolchain.lock. El lock sigue
siendo el registro legible que viaja por git; hashearlo permitiria sellar con
un lab distinto del declarado. Derivar la ruta del padre del store se
descarto: esa suposicion ya rompio al worker cuando su store se anclo a un
volumen (ver defaults_for_store_with_lab).
SIN CAMINO SILENCIOSO: el parametro es obligatorio, no Option. Sin rootfs
falla y dice que hacer. Un default aqui reintroduciria la divergencia que
esto cierra.
Trae test de regresion de un fallo MUDO: la primera lista de prefijos llevaba
el guion de version (`gcc-`) y en el apk db el campo P: es solo el nombre
(`gcc`) ⇒ no casaba ninguno y la huella salia la del conjunto vacio. Un hash
valido, constante e inutil, que mirando el hash no se nota.
COSTE, medido y no estimado: sealed 768 -> 0, debt 777. Los 1745 artefactos
del respaldo quedan SUPERADOS, no perdidos. Ninguna imagen queda lista.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sealed 764 -> 768, deuda kernel=4 -> 0. Los cuatro reconstruidos en gioser y
subidos al respaldo.
La prueba pedida era: como el .config no cambia, el bzImage deberia salir
identico en una direccion nueva. NO sale identico, y la causa NO es la
receta.
Los cuatro dan la MISMA firma. El config instalado difiere en exactamente 4
lineas y ninguna es del cambio:
CONFIG_RUSTC_VERSION=109600 -> 109700
CONFIG_RUSTC_LLVM_VERSION=220103 -> 220108
Es rust 1.96 (laptop) vs 1.97 (gioser, resuelto de Alpine edge). Las lineas
de USB4/THUNDERBOLT/REISERFS son identicas viejo vs nuevo en las cuatro ⇒ la
correccion de receta es inerte, como se habia predicho. Y Rust NI SIQUIERA
ESTA ACTIVADO en estos kernels: Kconfig sondea el rustc del entorno y graba
su version igual.
Vale para las DOS ramas (6.16.12 y 7.1.2), asi que es del sistema, no de una
version de kernel.
Lo que esto destapa importa mas que el experimento: el toolchain del rootfs
NO esta en hash_inputs. Si no se hubiera tocado la receta, gioser habria
sellado bytes distintos en la MISMA direccion que el laptop, y el store no
tiene como notarlo. docs/state/lab-toolchain.lock avisa de la divergencia
pero no la impide.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hasta acá el armador era análisis puro: la clausura decía qué debía morir y nadie lo había
contrastado con el resolvedor real. NO hace falta construir un kernel para hacerlo: lo caro
es la fase compile (35-60 min); toda la cadena del armador vive en configure y corre en
segundos.
Método: árbol 6.16.12 entero, `make defconfig` de base (el config de linux.toml NO sirve de
base: ya apaga wifi/audio/fs a mano, así que el lado disable no apagaría nada y la
predicción no se pondría a prueba — fue mi primer error), el fragmento del plan, y
olddefconfig. Verdad de campo = los símbolos que pasaron de encendidos a apagados.
sólo depends on ....... clausura 1775 aciertos 64/68 SOBRA 0 falta 4
+ huérfanos select .... clausura 1794 aciertos 65/68 SOBRA 0 falta 3
+ comparaciones ....... clausura 1795 aciertos 65/68 SOBRA 0 falta 3
SOBRA 0 en las tres: el predictor nunca dice que muere algo que sobrevive, que es la única
dirección en la que puede equivocarse sin fabricar un ladrillo.
Dos refinamientos que salieron de la medición, cada uno con su test:
· HUÉRFANOS DE SELECT. Un símbolo sin prompt no se marca a mano: sólo entra por select.
Si caen todos sus selectores, cae él, aunque nadie dependa de él (caso ACPI_NHLT). Se
exige >=1 selector: sin ninguno entra por un default, y darlo por muerto mataría media
tabla.
· `X = y` SÍ ES DEPENDENCIA DURA. Medio drivers/video/fbdev declara su dependencia de FB
como `depends on (FB = y) && ARM`. Tratar toda comparación como opaca dejaba esos
drivers fuera. `X = n` sigue fuera a propósito: con X en n es VERDADERA. De regalo, las
fugas select sin declarar de sin-graficos cayeron de 8+ a 1.
Los 3 que faltan NO son un fallo, son otra pregunta: CRYPTO_LIB_ARC4, REGMAP y
SYSTEM_DATA_VERIFICATION tienen selectores FUERA de la clausura (PPP_MPPE, 111 usuarios más
de REGMAP…). Se quedaron sin usuarios en ESE config; encendés PPP y ARC4 vuelve.
«Inalcanzable» y «apagado ahora» no son lo mismo, y la clausura contesta la primera.
Y un bug que sólo aparece corriendo el resolvedor: el diff-back contaba como promesa
incumplida todo símbolo pedido ausente del .config. Pero Kconfig NO EMITE un símbolo cuyas
dependencias no se cumplen ⇒ un `-d WLAN` cuya raíz ya cayó simplemente no sale. Con esa
cuenta un plan perfecto se reportaba roto (2 falsos incumplidos de 16). Ahora: ausente +
se pedía apagar = éxito; ausente + se pedía encender = fallo. La corrida real sale 14
cumplidos, 0 incumplidos.
GUARDIÁN NUEVO, y hacía falta: las cuatro recetas de kernel NO son el mismo kernel — linux
y linux-metal van por 6.16.12, linux-metal-dual y linux-generic por 7.1.2. Planear una
contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y
saldría SIN RUIDO. KconfigTree lee ahora su versión del Makefile de arriba y `plan` FALLA
si no coincide con la de la receta (los diagnósticos sólo avisan). Aviso de la sesión de
granja/store, verificado antes de implementarlo.
Catálogo: las dos fugas que destapó la clausura más grande quedan declaradas con motivo
(FB_SYSMEM_HELPERS_DEFERRED por HID_PICOLCD_FB; DRM_DISPLAY_DP_TUNNEL_STATE_DEBUG por
DRM_I915_DEBUG), y las notas sobre THUNDERBOLT/REISERFS_FS pasan a pasado: ya se
corrigieron en 2602218.
Runbook §4.bis: cómo probar un plan entero en 30 s en vez de 40 min.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sealed 768 -> 764, debt 9 -> 13, clase kernel=4. Consecuencia esperada del
cambio de hash: las recetas son correctas, los artefactos del store son de
la version anterior. Reconstruirlos deberia dar un kernel identico byte a
byte en una direccion nueva — util como prueba de bit-repro, ya que el
.config no cambia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los dos simbolos ya no existen en 6.16.12, asi que `-d THUNDERBOLT` y
`-d REISERFS_FS` eran no-ops silenciosos: scripts/config los escribia y el
olddefconfig siguiente los tiraba por desconocidos.
No son el mismo caso y por eso no reciben el mismo trato:
- THUNDERBOLT se FUNDIO, no se fue. drivers/thunderbolt/Kconfig declara
`menuconfig USB4` = "Unified support for USB4 and Thunderbolt". El simbolo
cambio de nombre, la intencion de apagarlo sigue viva ⇒ se renombra a
`-d USB4`, que ademas pasa a ser un guardian de verdad: si un dia la base
o un `select` lo encienden, ahora si lo apaga.
- REISERFS_FS se RETIRO del kernel. No hay nada que apagar ⇒ se quita.
El .config resultante NO cambia hoy: USB4 no aparece en x86_64_defconfig y su
Kconfig no trae `default`, asi que ya estaba en n. Lo que cambia es el
ArtifactHash de los cuatro, porque el texto de la fase configure entra en
hash_inputs. Los cuatro artefactos viejos siguen en el respaldo como
superados; no se pierde nada.
Verificado contra el arbol real (work/kconfig-6.16.12/), no de memoria.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
«no encuentro el ejecutable zig en …» no quiere decir que falte zig: falta ESA versión.
hammer lo resuelve en .dev-fs/tools/zig-x86_64-linux-<ver>/, ni el symlink tools/zig ni el
PATH cuentan.
Medido para el caso concreto del kernel: linux.toml NO pinea zig_version, pero tres de sus
deps.build sí — flex, openssl y elfutils, las tres a 0.13.0 — y la receta derivada las
hereda enteras. En gioser están 0.13.0 y 0.16.0, así que el eje está cubierto.
Aviso de la sesión de granja/store; verificado contra las recetas antes de escribirlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El repo lo trabajan varios agentes a la vez y las dos formas conocidas de
destruir el trabajo ajeno no estaban escritas en ninguna parte que un agente
lea al arrancar.
1. Todo `hammer build` envuelto en flock work/.farm-build.lock. hammer
comparte work/sources/<dep>-<sha>: dos builds que compartan una dep se
pisan el arbol y queda ROTO PARA SIEMPRE (ADR 0012, sin decidir; ~93 de
205 recetas KDE murieron asi al invalidar libdrm). Los scripts de la
granja ya toman ESE fichero, asi que nos serializa tambien con ellos. No
va dentro de hammer build a proposito: se bloquearia contra esos scripts.
2. Nunca `git add -A`: arrastra los ficheros a medias del otro.
Y la regla general del vacio, que es de donde salio todo lo de hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
work/.farm-build.lock, el mismo que toman farm-worker-loop.sh y campana-deuda.sh.
hammer build comparte work/sources/<dep>-<sha> entre todas las recetas: dos builds
simultáneos que compartan una dep se pisan (uno hace fetch y borra el árbol mientras el
otro lo usa) y el árbol queda roto PARA SIEMPRE — reintentar no lo arregla. Medido: al
invalidar libdrm, ~93 de 205 recetas KDE murieron por el wrapper .zwrap/cc barrido del
árbol de fuente. ADR 0012, sin decidir.
El runbook era el único sitio de este frente que mandaba a construir. El resto del armador
no toca el store: plan calcula el ArtifactHash con cómputo puro sobre las recetas, y
probe/gate/bundles/closure sólo leen.
Aviso de la sesión de granja/store, que comparte el repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paso 3 del §8 del SDD 22, y cierra el orden que fijaba: reversa, clausura, gate, diff-back.
La pieza que faltaba no era el gate sino el MAPA driver → símbolo. El kernel sabe qué
driver tiene bindeado cada dispositivo, pero no de qué CONFIG_* salió: esa relación sólo
existe en los Makefiles de kbuild. modmap.rs lee 15.789 reglas obj-$(CONFIG_X) += y.o en
3182 Makefiles. Dos trampas de nombres, cada una con su test:
· el módulo cargado usa _ donde el fichero usa - (snd-hda-intel.o → snd_hda_intel)
· un módulo puede salir de VARIOS símbolos, y sobrevive si sobrevive cualquiera
Sin resolverlas el gate no encontraría nada y diría que todo está bien, que es el peor
resultado posible para un portón.
POR OBJETIVO, no global. La regla es "todo dispositivo en uso debe seguir teniendo
driver"; aplicada global rechazaría recipes/linux.toml, que apaga USB, HID e INPUT A
PROPÓSITO por ser el kernel de QEMU con consola serie. El gate NO CORRE sin --objective, y
un allow_bundles con un id mal escrito es error de CARGA del catálogo (si no, autorizaría
nada y bloquearía sin que se entienda por qué).
Medido con el mismo plan (sin-usb + sin-entrada-humana + sin-graficos + sin-wifi) y el
hardware real de gioser:
qemu-serial ........ PASA — 5 pérdidas autorizadas
metal-escritorio ... BLOQUEA — las mismas 5 como regresiones, con el bundle culpable
Ése es todo el punto del §5.
Y lo que el gate no puede comprobar, lo dice: de los 38 drivers bindeados, 15 no se
mapearon a ningún símbolo (pcieport, serial8250 — built-ins cuyo nombre de driver no
coincide con el del módulo). Quedan listados como SIN COMPROBAR, nunca como aprobados.
hammer kernel hw vuelca la huella y los drivers de la máquina DESTINO, que no tiene por
qué ser la de build — el SDD lo pedía explícitamente.
Cuatro objetivos en el catálogo (qemu-serial, servidor, metal-escritorio, portatil) y un
runbook nuevo: docs/runbooks/armador-de-kernel.md, con el ataque de punta a punta y una
lista honesta de lo que todavía NO está (sonda en VM, atestación por huella, bisección,
curación del delta con modelo, perillas side=recipe).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paso 4 del §8 del SDD 22, y el corazón del armador.
hammer kernel plan --recipe recipes/linux.toml --bundle sin-wifi --bundle sin-audio
--bundle solo-ext4 --knob jaula-y-eio-moderna
base b3:cb926743…
derivada b3:47a52b2e… (16 banderas, 1775 símbolos con su clausura)
El config vive en la fase `configure` y las fases entran en hash_inputs ⇒ el config ES la
identidad del artefacto. Por eso el plan emite una RECETA DERIVADA y no finge que el
kernel sea un binario parametrizable. Y como el plan DETERMINA el artefacto, el JSON lleva
su ArtifactHash: la UI puede decir "esto ya está construido y firmado" sin construir nada.
Tres decisiones que no eran obvias:
· Se emiten RAÍCES, no clausuras: 16 banderas, no 1775 líneas. La clausura la calcula el
olddefconfig del propio kernel. hammer la sabe sólo para poder explicarla — la app
nunca escribe un .config.
· La fase derivada AÑADE una segunda ronda (…&& scripts/config … && make olddefconfig)
en vez de reescribir la base: no hay que parsear el shell de nadie, olddefconfig es
idempotente, y la base sigue siendo literalmente la de siempre en el diff.
· Los conflictos se RECHAZAN, no se ordenan. Resolver por orden de aparición sería una
respuesta plausible y arbitraria. Y hay un segundo conflicto que el símbolo solo no
delata: encender algo que cae DENTRO de la clausura de lo que otro bundle apaga —
olddefconfig lo descartaría sin decir nada.
diff-back: la mitad que faltaba del §6 del handoff. Clasifica cada símbolo pedido en
cumplido / INCUMPLIDO (el .config dice otra cosa) / ausente (el kernel ni lo menciona: la
bandera fue un no-op), con la procedencia de quién lo pidió, y sale != 0 si el config no
honra el plan. Probado contra /proc/config.gz de gioser: 3 incumplidos, 1 ausente.
La procedencia por símbolo (#7 del handoff) sale de regalo: cada bandera carga quién la
pidió y por qué (raíz del bundle / fuga select cerrada / perilla).
Y el orden del fragmento es estable a propósito: ese texto entra al hash, así que un orden
que dependiera del recorrido daría dos hashes para el mismo plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paso 1 del §8 del SDD 22 (va después de la clausura porque necesitaba el grafo). Sigue sin
compilar ni escribir nada.
hammer kernel probe — lee /proc/config.gz (o /boot/config-<release>) y muestra el kernel
que YA CORRE por el lente de los bundles: cuánto de cada uno rige, qué capacidad carga
esta máquina y no usa, y dónde el hardware CONTRADICE a un bundle aplicado. Corrido en
gioser: 10.551 símbolos, 20 dispositivos PCI, 38 drivers bindeados, 15 bundles, 7 con
capacidad que este hardware no usa.
hammer kernel bundles [--check] — el catálogo con las clausuras resueltas contra un árbol
concreto, y el control de frescura.
Piezas nuevas en hammer-core/src/kernel/:
catalog.rs bundles N1 y perillas N2. El campo `side` NO es decorativo: la mitad de N2
son variables de receta, no símbolos; mueven el hash igual pero se aplican en
otra fase y fallan distinto. Una perilla side=recipe sin recipe_field es
error de carga, porque es un diff que la UI no podría explicar.
hw.rs huella DMI+PCI+flags de CPU. El USB se LEE y se REPORTA pero NO se hashea: un
pendrive no puede cambiar la clase de hardware bajo la que se cachea un
kernel. Tampoco entra el serial: la huella agrupa máquinas, no las identifica.
Tres tests fijan esas tres propiedades.
reverse.rs el análisis. Y una tercera salida que no estaba pedida: cada fuga `select`
que entra a un bundle y NO está declarada en el catálogo es un símbolo que
upstream agregó y nadie revisó ⇒ la mitad barata de la curación del delta
(§3 del handoff) sale de comparar grafo con catálogo, sin IA.
docs/state/kernel-bundles.toml — 15 bundles N1 y 8 perillas N2, cada fuga resuelta a mano
una vez: `close_leaks` (se apaga también al que la provoca) o `accept_leaks` (se deja
abierta a sabiendas, con el motivo escrito). Ejemplo de por qué hacían falta las dos:
"sin-audio" NO cierra — DRM_I915/NOUVEAU/AMD_DC hacen select del códec HDMI, y cerrarlo
sería quedarse sin GPU. Se acepta y queda por escrito.
LO QUE DESTAPÓ EL CONTROL DE FRESCURA: las CUATRO recetas de kernel (linux, linux-metal,
linux-metal-dual, linux-generic) apagan `THUNDERBOLT` y `REISERFS_FS`, y 6.16.12 NO TIENE
NINGUNO DE LOS DOS. Thunderbolt se llama USB4 desde que upstream lo fundió con USB4;
reiserfs fue retirado. Los dos `-d` son no-ops silenciosos: el driver USB4 sigue entrando
por el defconfig mientras la receta dice que está apagado. NO las toco — cambiarlo mueve
el ArtifactHash de los cuatro kernels y es una decisión, no una limpieza.
Y el propio probe destapó un desajuste que ahora avisa: el config vivo de gioser es de la
serie 7.1 y el catálogo se revisó contra la 6.16 ⇒ las clausuras son aproximadas. Se dice
en vez de callarlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Alpine no publica snapshots datados de edge y el repo sólo sirve la última
versión, así que ese camino no estaba pendiente: estaba cerrado. Apunta al
lock del paso 3a-quater, que es lo que sí se puede hacer hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los repos del rootfs apuntan a Alpine edge (bump deliberado por el techo
MSRV). Es RODANTE: el mismo script en dos fechas da compiladores distintos.
Medido el 2026-08-10 al bootstrapear gioser — el laptop tenia rust 1.96 y
aqui edge resolvio 1.97.0-r0. Para un proyecto cuyo invariante es reproducir
eso es deriva del LAB, y no aparece en build-state.json.
No es un pin y no puede serlo: edge sirve solo la ultima version (`apk policy
rust` lista unicamente 1.97.0-r0), asi que `apk add rust=1.96.0-r0` rompe en
cuanto edge avanza, y Alpine no publica snapshots datados de edge. Anclar de
verdad pide espejar APKINDEX + los .apk, que es un frente aparte.
Lo que si se puede hoy: registrar los 92 paquetes resueltos en
docs/state/lab-toolchain.lock —que viaja por git, que es como los dos hubs lo
comparan— y AVISAR con el diff delante cuando la maquina difiere. Aceptar el
cambio es deliberado: --relock. Un lock que no se puede imponer sigue
valiendo si al menos nombra lo que cambio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Primer paso del SDD 22 (armador de kernel), en el orden que fija su §8. Regla dura
respetada literalmente: hammer LEE el grafo de Kconfig, no lo resuelve — el .config lo
sigue produciendo el olddefconfig del propio kernel.
hammer-core/src/kernel/: lector tolerante (18.212 símbolos, 1646 ficheros, 0 avisos de
parseo sobre 6.16.12) + lector de .config. hammer kernel {stats,closure}.
La semántica de arista, que el §3 pedía definir antes de escribir el predicado:
· dependencia dura = símbolo en posición CONJUNTIVA (en "A && (B|C)" sólo A). La
disyunción, la negación y las comparaciones no aportan. Conservador a propósito:
apagar de menos se nota, apagar de más hace un ladrillo.
· símbolo con varias definiciones ⇒ INTERSECCIÓN entre ellas, no unión.
· select es el portillo, no una arista más: fuerza el destino IGNORANDO sus depends.
select_leaks las enumera; closure_off_fixpoint cierra el bundle contra ellas y REPORTA
el precio en vez de aplicarlo solo.
La medición que decide §2.1, contra el bundle N1 hecho a mano de recipes/linux.toml:
clausura estricta de WIRELESS ......................... 350
punto fijo (3 fugas: WLAN, IWLEGACY, GELIC_WIRELESS) .. 406, cierra en 1 ronda
bundle a mano ......................................... 421
SOBRA 0 · falta 15
Los 15 son todos RFKILL, que no es wifi sino el interruptor de radio compartido con
bluetooth y NFC. El humano apagó DOS bundles en la misma línea ⇒ el catálogo necesita
"sin radios" como entrada propia. §2.1 es viable.
Y el punto fijo también dice cuándo no: cerrar "sin audio" exige tragarse DRM_I915/
NOUVEAU/AMD_DC, que hacen select del códec HDMI. En linux.toml sale gratis porque los
gráficos ya están apagados; en un escritorio sería una decisión.
De regalo: linux.toml apaga REISERFS_FS, que 6.16.12 ya no tiene. Un -d a un símbolo
inexistente se pierde HOY en silencio — justo lo que el diff-back (paso 4) va a atrapar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dos eslabones de la misma cadena, que el 2026-08-10 dejo cuatro recetas con
OK sin producir un solo fichero.
Store::has era path_of(..).is_dir(): un directorio vacio contaba como
sellado, asi que build() hacia cache-hit y devolvia Ok sin construir. Ahora
exige al menos una entrada. NO exige el sidecar .hammer/recipe.toml aunque
seria mas expresivo: ese lo escriben los llamantes, no seal(), y
hammer-bootstrap sella sin el ⇒ pedirlo lo haria reconstruir siempre. Va con
test de regresion.
--listar armaba el manifiesto con `ls`, que lista NOMBRES: un vacio es
identico a uno bueno, y de ahi build-state.py lo daba por sellado. Ahora usa
`du -s` (8,6 s sobre 1751, frente a un ls instantaneo), separa los vacios a
work/respaldo-vacios.txt y los DICE siempre, tambien cuando son 0.
Cuidado con el orden en ese awk: recortar la ruta antes se come el contador
de bloques y el filtro compara el nombre en vez del tamano — daba 406 vacios
falsos. Primero filtrar por numero, despues recortar.
Quedan 3 vacios sin curar en el respaldo (dbus x2 y un libxkbcommon de hash
superado); no caen en ninguna clausura construida.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selladas 6: wayland, wayland-protocols, libxkbcommon, mesa, mirada-compositor
y mirada-greeter. Corpus 766 -> 768, deuda 11 -> 9, y la clase rust
desaparece. El perfil escritorio-mirada pasa a 31/31.
Las cuatro primeras figuraban como selladas y no lo estaban: el respaldo
tiene 7 artefactos VACIOS (mesa, wayland, wayland-protocols, libxkbcommon x2,
dbus x2), --listar los cuenta por nombre de directorio, build-state los toma
por buenos y hammer build hace cache-hit sobre el directorio vacio. Sellaba
sin construir y salia 0. Un ausente falla ruidosamente; un vacio llega hasta
el final diciendo que todo fue bien.
Para construirlas hacian falta dos piezas del lab que gioser no tenia: zig
0.13.0 (lo pinean 84 recetas, y se busca por directorio versionado, no por
el symlink) y el paso apk del rootfs, que trae rust/cargo. Quedan bloqueadas
gnome-session (no hay receta de GTK3 en el corpus) y gnome-settings-daemon
(geocode-glib existe, esta en deuda y no figura en su [deps]).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Estaba fijo en ssh://…git.tawasuyu.net:2345/… mientras el clon de gioser
fetchea de gitea@git.gioser.net:… — la misma máquina, pero otra forma de URL
y otro puerto. Correr el script tal cual hacía --unset-all y le cambiaba el
destino canónico al clon sin avisar: un push que se va a donde no era y no
se nota hasta que importa.
Ahora deriva del origin de cada clon y el hardcodeado queda de fallback,
así sirve igual en el laptop y en gioser. Sigue siendo idempotente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sin TTY, --info=progress2 reescribe con \r y el progreso se acumula en
miles de copias de la misma línea. El respaldo corre DESATENDIDO, así que
ese log es la única forma de saber qué pasó; a 2,6 MB no se lee.
Mismo defecto y mismo arreglo que en worker-depositar.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Montando gioser como segundo hub salió el defecto: `sealed_remoto` cuenta
cuántos sellados NO están en el disco de QUIEN calcula. El laptop tiene 219
artefactos y gioser 19, así que el mismo corpus da 700 y 751. Metido en un
fichero commiteado, las dos máquinas se lo pisarían en cada regeneración,
para siempre.
El estado del corpus es compartido; cuánto de él tiene esta máquina en el
disco, no. Se sigue diciendo en el resumen, donde es útil y no genera churn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1. build-farm.sh — vulkan-loader fallaba con «io: File name too long (os
error 36)» en TODOS los ciclos y sella sin una queja ejecutado en serie
(comprobado a mano en el worker). La firma no estaba en la lista de
colisiones del reintento serial, así que nunca se reintentaba: fallaba,
contaba como deuda, y al ciclo siguiente fallaba igual.
Un fallo que se repite IDÉNTICO no es intermitente: es uno que nadie
está reintentando.
Se añade la firma, pero el defecto de fondo era que faltarla fuese MUDO
— la lista sólo puede crecer si alguien se entera de que se quedó corta.
Ahora lo no reintentado se dice, con las primeras 120 letras del log.
2. farm-up.sh — no sembraba work/farm-sellados.txt. Un worker recién
creado arrancaba con el manifiesto rancio horneado en la golden: 1172
contra 1241 del hub, así que reconstruía lo que el hub ya tenía sellado
(vulkan-loader entre ellos, y encima fallando).
Es el mismo error de método que farm-sync.sh ya se documentó a sí mismo:
«lo puse allí, di el bucle por cerrado, y la churn siguió porque hay DOS
rutas». Había dos otra vez y sólo una estaba arreglada.
Tras el arreglo las cuatro colas dan 1/1 construyen (0 fallan) y el perfil
escritorio-kde cierra 162/162.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cadena completa medida hoy: el worker depositó 63 artefactos (1,34 GB) en
8 s a 153 MB/s, y el hub los promovió con mv. Respaldo 1542 → 1605, buzón
en 0, guardián cuadrando. Contra los 440 kB/s del laptop, 348×.
Dos fallos de la prueba, que importan más que el resultado:
1. `ssh` cortaba con «Host key verification failed» porque la granja REUSA
IPs y la host key cambia con razón. Como el stderr iba a /dev/null, el
error salía como «no obtuve la pública del worker»: culpaba al worker
cuando el problema estaba en el known_hosts del laptop. Ahora va por
ssh_worker(), que purga la entrada vieja — correcto sólo acá, porque la
identidad del worker es su label hcloud, no su llave.
2. verificar-buzon daba «aislado ✓» sin haber probado que escribe. El
aviso de host key se comía el head -1, y el testigo se llamaba .btest,
que `ls` no muestra. Un testigo invisible no prueba nada. Ahora exige
las dos mitades y las dos están verdes: escribe en su buzón, y
../hammer/store no existe para él.
Y el progreso de rsync sólo con TTY: sin terminal reescribe con \r y deja
miles de copias de la misma línea. 8 segundos bastaron para un log
ilegible; en el latido sería cada media hora.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los permisos de subcuenta del Storage Box son BINARIOS: --readonly sí o no.
No hay append-only ni write-sin-delete (verificado en la API, no recordado).
Así que la contención se hace por alcance: el worker escribe en un BUZÓN
cuyo home es incoming/, y hammer/store sencillamente no existe para él. Lo
máximo que puede destruir es lo que él mismo depositó y aún no se promovió,
que sigue estando en el volumen. Un permiso puede estar mal puesto; un
directorio fuera de tu home, no.
La promoción es un mv DENTRO del mismo filesystem: un rename, instantáneo,
cero bytes por la red. Eso es lo que la hace viable con un uplink de
440 kB/s — el laptop manda órdenes, los datos van worker→box por dentro de
Hetzner (122 MB/s, 280×).
Medido de la shell del box, que NO es un bash:
· no hay `for` ⇒ los lotes se arman en el hub
· `a; b` no encadena de fiar ⇒ una orden por conexión
· `mv a b c dest/` sí acepta varios orígenes ⇒ cientos por conexión
· `find` no existe y devuelve 0 EN SILENCIO ⇒ parece «no hay respaldo»
Y la trampa que motivó partir el trabajo en dos conjuntos: `mv A dest/` con
dest/A ya existente NO falla, mete A DENTRO y deja dest/A/A. Como el store
es CAS, un artefacto ya respaldado es idéntico ⇒ no se mueve, se borra del
buzón. Los dos caminos probados de punta a punta con un artefacto falso,
recuento verificado y sin anidar.
Defensa en profundidad aparte: plan de snapshots diario (03:17 UTC, retiene
7 de 10) y la carpeta ZFS visible para recuperar ficheros sueltos sin
rollback. La subcuenta no las alcanza: viven en la cuenta, con el token que
el worker no tiene.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El store de trabajo se mudó al volumen de la granja y el laptop quedó con
219 artefactos: los de fuente privada (que la granja no puede rehacer) más
lo que todavía no está respaldado. 979 artefactos verificados en el box se
borraron de acá, y con ellos el caché .dmerge de 92 G que sostenía sus
bloques: 128 G → 8,2 G, 115 G libres.
Eso rompía tres cosas que leían el disco local como si fuera la verdad:
- build-state.py habría reportado `never` sobre ~950 sellados y el latido
lo habría COMMITEADO. Un grafo recién escrito miente con más autoridad
que uno viejo. Ahora resuelve la presencia contra la unión del store y
los manifiestos, y expone `sealed_remoto` para que «el store se mudó»
no se lea nunca como «el corpus creció».
- farm-sync.sh armaba el manifiesto con `ls ./store`, así que le habría
dicho al worker «el hub tiene 220» y el worker habría rehecho ~950. Es
el bucle de churn que el propio fichero documenta, al revés. Ahora es la
unión, con un guardián que aborta si el manifiesto encoge.
- cosecha-cron.sh bajaba el store entero cada 30 min: habría deshecho la
mudanza sola, como la poda de 24 G que se deshacía en 2026-08-07. Ahora
baja sólo la lista de nombres.
Los cuatro grafos regenerados dan idéntico a antes del recorte
(766/11/2), que es la prueba de que no se perdió nada.
Queda abierto: los artefactos nuevos viven SÓLO en el volumen hasta que
alguien corra una pasada de respaldo. El volumen tiene borrado protegido,
pero es una copia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dos cosas medidas al traer los 167 GB.
1. `rsync -a` SIN `-H` no preserva los enlaces duros, y el store comparte ficheros entre artefactos
justamente así. Llegó inflado: 76 G en el box → 159 G en el volumen, con **0 ficheros de nlink>1**
al llegar — ésa es la prueba de que se perdieron, no una sospecha. (El 76 G del box es además ZFS
comprimido, así que las dos cosas se sumaban y parecía peor.) El contenido es correcto —es CAS y
los hashes casan—, pero ocupa de más y el volumen no sobra.
2. El respaldo conserva artefactos que el hub YA podó. Son SUPERADOS: existe otro con el hash
vigente, y no pueden dar cache-hit nunca porque la receta que los nombraba cambió. Eran 544 de
1536 · 31 G. Podados con work/store-gc-superados.txt como lista.
El guardián es RECONTAR tras el rm: «borré 544» y «hay 544 menos» son afirmaciones distintas, y
sólo la segunda es la que importa. Cuadró.
Volumen: 992 artefactos vigentes, 95 G libres de 246.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MEDIDO, no supuesto: el uplink de la oficina da 440 kB/s ⇒ los 127 G del store son 82 horas. Subir
el store desde el laptop no es lento, es imposible. Pero el Storage Box está en hel1 y el worker
está en hel1: por la red interna la misma copia va a 122 MB/s. Son 280×.
⇒ El laptop deja de ser el camino de los datos. Sólo manda las RECETAS (26 M).
POR QUÉ SUBCUENTA Y NO LA CUENTA PRINCIPAL. El worker es efímero y se borra solo; darle la
credencial principal sería darle permiso de borrado sobre el ÚNICO respaldo que existe. Un respaldo
al que puede escribir la máquina de la que hay que protegerse no es un respaldo. La subcuenta acota
el daño a cero por construcción: --readonly, --reachable-externally=false, home acotado a hammer/.
LA VERIFICACIÓN ES NEGATIVA. «Sólo lectura» es una afirmación sobre lo que el sistema IMPIDE, así
que leer no la prueba. Hay que intentar escribir y borrar y exigir que fallen:
rm → «Read-only file system» · scp → «dest open: Failure» · respaldo intacto.
El script trae ese paso como subcomando `verificar` y sale ≠0 si la subcuenta resulta escribir.
Dos gotchas que costaron:
· la API exige contraseña con mayúscula+minúscula+número+símbolo aunque después se use llave;
· la shell del Storage Box es RESTRINGIDA: acepta ls/mkdir/rm pero NO redirección, así que
`echo k > authorized_keys` falla EN SILENCIO (crea el directorio, no el fichero). Va con scp.
La llave se genera EN EL WORKER (nunca viaja una privada desde el laptop) y muere con él.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El bloque del volumen es una cadena entrecomillada que se manda al worker, así que un backtick ahí
no es tipografía: es SUSTITUCIÓN DE ÓRDENES. Mis comentarios de la commit anterior citaban
`volume attach` y `blkid` con backticks ⇒ la shell intentó EJECUTAR «volume attach» («volume:
command not found») y a partir de ahí el resto del bloque se mandó mutilado: mkfs.ext4 sin
dispositivo, mountpoint sin argumento.
Por eso los comentarios originales de ese mismo bloque escapan los backticks con \`. Yo escribí los
míos con el estilo del resto del fichero, que es correcto FUERA de la cadena y venenoso dentro.
Arreglado quitándolos: en un comentario que viaja por SSH, la comilla no vale lo que cuesta.
El volumen no sufrió: sigue con su filesystem del 17 de julio y montado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>