El emisor lee la receta que viaja DENTRO del artefacto sellado (el sidecar
.hammer/recipe.toml) y no de recipes/, porque seal_product_rootfs sirve a las
DOS vetas —product desde recetas locales y product_from_repo desde el repo
firmado— y sólo una tiene recipes/ a mano. El sidecar es la única fuente que
ambas comparten, que es lo que sostiene que converjan al mismo product-rootfs.
LA TRAMPA QUEDA ARMADA, NO CERRADA: el sidecar no entra al ArtifactHash, así que
un artefacto anterior al bloque [[service]] es un cache-hit válido que no se
reconstruye solo. Si el emisor devolviera lista vacía sin protestar, el producto
saldría booteando, verde y SIN SSHD. Falla ruidosamente, y hay un test que exige
que el mensaje siga explicando la causa Y dando el arreglo (regla 3 del
CLAUDE.md: un ausente falla ruidosamente, un vacío llega hasta el final diciendo
que todo fue bien).
LA MIGRACIÓN, CON CONTROL, y el hecho nuevo que dejó: openssh no estaba
certificado como reproducible ("el criterio es construye y corre", dice su
receta). Manifiesto de 33 sha256+modos ANTES de tocar nada, respaldo por
hardlink, borrado, rebuild bajo flock -o, comparación. Resultado: 33/33
ficheros, modos idénticos, y la ÚNICA diferencia en todo el árbol es
.hammer/recipe.toml. openssh REPRODUCE BIT A BIT.
Gotcha que costó el primer intento: store/ es bind-mount de /dev/sdb y work/
vive en /dev/sdc ⇒ `cp -al` cruza mounts y muere con EXDEV. El respaldo va
DENTRO del mount del store; un dot-dir ahí es invisible para store-gc.sh, que
sólo mira dirs ^[0-9a-f]{64}-.
`vigia-sonames.py` corría por defecto SÓLO sobre los cinco perfiles de escritorio
(`p.startswith("escritorio")`). O sea que `base`, `cli` y `servidor` —el objetivo de la mudanza—
**nunca se miraron**. Apuntándolo a ellos a mano:
== servidor 147 nodos · 183 sonames provistos · 1 sin proveedor
FALTA libffi.so.8 ← lo piden: python3
`python3` no arrancaba en las tres imágenes, por el mecanismo EXACTO que `musl-shared` y
`zlib-shared` arreglaron dos días antes: la receta canónica de libffi es `--disable-shared`, ningún
artefacto del cierre publica ese soname, y dentro del lab el agujero lo tapa el rootfs de Alpine —
así que el build pasa y la imagen sale rota. `libffi-shared` ya existía en el corpus y ya estaba
declarada en otros dos perfiles; sólo faltaba en `base`.
servidor 148 nodos · 186 sonames · **0 sin proveedor** (base y cli, igual)
## Y el vigía pasa a mirar TODOS los perfiles
⚠ **Es la tercera vez en dos días que un guardián de este repo mide menos de lo que su resumen
afirma**: `static-audit.sh` sólo globeaba `recipes/` y no las 5 colas (3 mentiras ocultas); el bloque
de raíces sucias sólo corría al hidratar un perfil y no veía las 649 recetas fuera de toda imagen
(qdrant con 69 M de basura); y éste dejaba fuera medio sistema. **Un «0 huecos» sobre la mitad del
sistema se imprime igual que uno sobre todo él.**
⇒ Al leer un guardián, leer su SCOPE antes que su veredicto: el glob, el bucle, de dónde saca la
lista. Los ocho perfiles dan 0 sin proveedor, así que ampliar el alcance no mete ruido — sólo deja de
esconder.
Corrida real contra gioser: los 21 repos fallaron al crearse y el guión dijo «no se pudo crear» A
SECAS, porque mandaba el stderr de `gh` a /dev/null. La causa era el LÍMITE DE LA API DE GITHUB
agotado —que se resuelve esperando— y sin el mensaje eso parecía un problema de permisos, de nombre o
del propio guión: hubo que reproducirlo a mano para enterarse de algo que la herramienta ya sabía.
Dos arreglos:
· El error de `gh` se muestra, recortado a su primera línea.
· Ante «rate limit» se CORTA en el primero: los 20 restantes van a fallar igual y cada intento gasta
cuota. Dice a qué hora se repone (leído de `gh api rate_limit`) y recuerda que reintentar es
seguro, porque el guión es idempotente.
No quedó estado parcial de la corrida fallida: el `pushurl` se agrega DESPUÉS de crear el repo, así
que los 21 clones siguen con su remoto original intacto. Verificado en tres de ellos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
El vigía escribe `docs/state/subcomandos.txt` en cada ciclo del latido desde ayer, y decía
`TOTAL: 44 herramientas selladas que no se pueden invocar` a secas. Un número grande, constante y sin
explicación al lado entrena a saltearse el informe — y entonces el hueco NUEVO, que es el único que
pide acción, pasa desapercibido entre el ruido del viejo. Es la misma trampa que ya tienen resuelta
`static-audit.sh` (deuda decidida) y `--auditar-raices`.
Los 44 son todos del mismo hueco, y ahora el informe dice cuál y por qué:
• falta el binario `cargo` (el toolchain de Rust)
PENDIENTE DE DECISIÓN, no olvido. `cargo`/`rustc` viven hoy en el LAB y en la cadena de
selfhost (mrustc→1.91.1), no como receta del catálogo. Empaquetarlos es una decisión de
TOOLCHAIN —qué Rust publica la distro, y desde qué cadena— no trabajo de granja.
TOTAL: 44 … (44 de hueco CONOCIDO, 0 NUEVAS) ⇒ exit 0
⚠ **«PENDIENTE DE DECISIÓN» y no «deuda decidida»**, que es distinto: en el audit estático las tres
mentiras tienen un arreglo evaluado y descartado por su coste; acá nadie decidió nada todavía. La
etiqueta tiene que decir cuál de las dos cosas es, o dentro de tres meses se lee como cerrada.
El código de salida ahora refleja sólo los huecos NUEVOS. Probado con los dos controles: sacando
`cargo` de la tabla salen `44 NUEVAS` y exit 1; restaurándola, `0 NUEVAS` y exit 0.
⚠ Y el primer intento de ese control estaba MAL y daba verde: escribí `CONOCIDOS = {} or {…}` para
vaciar la tabla, y en Python `{}` es falsy ⇒ la expresión devuelve el segundo dict y la tabla seguía
llena. El control «pasaba» sin probar nada. Rehecho renombrando la clave.
Hace por un árbol entero lo que `scripts/espejo-setup.sh` hace por takana: `pushurl` doble sobre
`origin`, de modo que cualquier `git push origin` que ya exista empiece a espejar sin tocar ningún
script. Crea el repo en GitHub como PRIVADO.
Cuatro decisiones:
· **Por defecto NO hace nada**: imprime qué haría; hay que pedirlo con `--apply`. Crear repos y
empujar código a un servicio externo no es algo que deba pasar por correr un script sin leerlo.
· **Varios clones del MISMO origen van a UN repo remoto.** En gioser hay cinco de
`tawasuyu/tawasuyu`; cinco repos distintos multiplican la confusión y empujarlos todos al mismo
`main` los haría chocar. El primario empuja normal; un secundario sólo se empuja —bajo
`refs/heads/clon/<nombre>/*`— SI aporta historia propia, y eso se comprueba preguntándole al
primario si ya tiene esos objetos (`cat-file -e`). Medido: los cinco tienen CERO commits que no
estén en su remoto, así que los cuatro secundarios se saltan y se dice por qué.
· **Sólo agrega; nunca toca el `fetch` de `origin`**, que sigue siendo el canónico.
· **Lo sucio no viaja, y se dice.** Un espejo guarda historia commiteada, no el árbol de trabajo, y
`tawasuyu` tiene 980 ficheros sin commitear. Creer que está a salvo cuando la mitad del trabajo no
está commiteado es justo el fallo que este guión existe para evitar.
⚠ Y un criterio que escribí mal y corregí midiendo: el primario era «el clon con más commits
alcanzables», y contra los cinco de tawasuyu dio un EMPATE EXACTO en 9343 — `rev-list --all` cuenta
las refs de `origin`, que los cinco comparten, así que la métrica estaba dominada por lo común y no
medía nada. El desempate quedaba en el orden del `find` y elegía `tw-deploy` (HEAD del 6/9) sobre
`tawasuyu` (del 12/9), que es el que se está trabajando. Ahora manda la fecha del HEAD en ISO, que
distingue y ordena como texto.
Ensayo sobre gioser: 25 repos sin copia fuera ⇒ 21 a espejar, 4 saltados por redundantes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
Tras devolver protobuf y abseil a `zig-cc`, qdrant cayó a deuda y se reconstruyó en el worker:
2 ficheros, sólo `usr/`, 70 M, `qdrant 1.19.1`. La limpieza del `/out/src` —el DESTDIR global que
heredaba el `make install` anidado de protobuf-src— aguanta el cambio de dep. REPRODUCE bit a bit.
El corpus queda en 907/907 sellado con deuda 0.
networkmanager b3:c36414ece23e322ef007d0acc7ce6f65b8b822ae55c38b38c9afa4b3eb340adb (52 MB)
Arreglo del segundo hueco del barrido: `networkmanager` figuraba SÓLO en escritorio-kde, y desde
GNOME, COSMIC o sway no se alcanza (cola hermana). Pero promover la receta de KDE tal cual habría
sido peor que no hacerlo, y sólo se vio mirando el ARTEFACTO:
· `-Dwifi=false` ⇒ un gestor de red que no maneja interfaces inalámbricas. Allá es correcto
(esa receta existe para dar libnm.so a plasma-nm y nada más); acá habría puesto en tres
imágenes un demonio que arranca, dibuja el applet y no lista una sola red.
· sin `nmcli` ⇒ ni una herramienta con la que manejarlo. En sway eso es la diferencia entre
tener red y no tenerla.
La variante del corpus cambia tres cosas, y la tercera hizo falta MEDIRLA:
1. `-Dwifi=true`. Habla nl80211 por libnl (ya era dep) y delega el handshake WPA2 en
`wpa_supplicant`, que desde hoy está en perfil.base ⇒ va también en [deps] runtime.
2. `readline` en [deps] build.
3. `-Dnmcli=true -Dreadline=libreadline`. Con sólo la dep, nmcli NO se construyó: `-Dnmcli` vale
true de fábrica pero `-Dreadline` es un COMBO que por defecto va en `auto`, y sin decirle
dónde mirar resolvió a «none» ⇒ meson apagó el CLI EN SILENCIO. Comprobado sobre el store.
Y `readline-shared` en runtime, cazado con provee.py sobre las NEEDED del binario: nmcli sale con
`NEEDED libreadline.so.8` y la readline canónica del corpus es sólo .a. La dep de BUILD y la de
RUNTIME son artefactos distintos — tercer caso del día tras cmus y bluez. No mueve el hash.
⚠ Se queda `-Dcrypto=null`, y es una limitación real escrita en la receta: sin cripto no valida
802.1X / WPA-Enterprise (el WiFi de una oficina). WPA2-PSK funciona porque ése lo hace el
supplicant. Encender nss o gnutls arrastra dos cadenas enormes: unidad de trabajo aparte.
── targets.toml ────────────────────────────────────────────────────────────────────────────────
escritorio-gnome, escritorio-cosmic, escritorio-sway += networkmanager (+ servicio)
escritorio-kde, escritorio-sway += wireplumber (+ servicio)
⚠ networkmanager NO se declara en escritorio-kde: allá plasma-nm ya arrastra la variante de la
cola, y dos NetworkManager distintos en la misma ruta es la enfermedad de las dos glib. Que la de
KDE vaya sin WiFi es deuda del frente KDE, y queda escrita donde toca.
libtool b3:5c09bff565b24c0a8edb306e0d4ca4e5e8e2a8c8e4a2b8ac047c8816b6883dcb
libndp b3:51be8f4336084b181e94a7f4a4178b87602cd39e975c2ddbd3125e7b782b9189
Entran como escalón para promover `networkmanager` al corpus (el barrido de perfiles midió que
GNOME, COSMIC y sway no alcanzaban ningún gestor de red: la receta vivía sólo en incoming-kde, que
desde ellos es una cola hermana).
La comprobación que decide una promoción NO es `diff` de las recetas sino `takana hash`, porque la
resolución de deps es hermano→padre y dos ficheros idénticos en colas distintas PUEDEN sellar
distinto. Acá sellan IGUAL desde las dos partes ⇒ misma dirección del store ⇒ las dos entraron por
CACHE-HIT, cero builds y cero rebuilds. (networkmanager, en cambio, sí difiere: resuelve varias
deps contra el corpus en vez de contra la cola de KDE.)
Las copias de `incoming-kde` se DEJAN donde están, a propósito: sellan en la misma dirección, así
que no hay dos artefactos ni colisión de nombre, y retirar ficheros de la cola que otro agente está
moliendo ahora mismo no es algo que este barrido tenga que hacer. Jubilarlas es trabajo del frente
KDE, el día que le convenga.
libglvnd b3:e27d3b2833a6aa6a6b4a4caad5ef2121e7be31a3c7ae59d0db22b01d696ffc99
obs-studio b3:29d95ddf50ef79692b48cae493758def564202ef81eba236b6cb279757621403
── libglvnd: se apagan egl, gles1, gles2 y headers ─────────────────────────────────────────────
La receta se escribió para la campaña completa —glvnd toma libEGL y mesa pasa a ser vendor detrás—
y la campaña NO SE HIZO: las tres mesa siguen con -Dglvnd=false. El resultado, medido en
/mnt/cosecha/escritorios/kde-rootfs ya hidratado, era el cuadro que la propia receta anunciaba:
/usr/lib/libEGL.so.1 -> libEGL.so.1.0.0 1.440.624 B (mesa: el driver)
/usr/lib/libEGL.so.1.1.0 323.144 B (glvnd: despacho, HUÉRFANO)
Dos libEGL.so.1 distintos en la MISMA ruta, más libGLESv2.so.2 y los headers GL/, EGL/ y GLES2/
duplicados y con BYTES DISTINTOS (gl.h: 79.877 vs 80.393). Quién gana depende del orden de
hidratación y nada lo guarda.
Y no hacía falta: OBS, que es la razón de que la receta exista, NO usa glvnd en runtime.
libobs-opengl.so sale con NEEDED libEGL.so.1 y nada más, y sus indefinidos son todos egl* —ni un
gl[A-Z] sin resolver— o sea que su glad resuelve por eglGetProcAddress contra la EGL de mesa. Lo
único que glvnd le da es satisfacer el OpenGL::GL de CMake AL ENLAZAR.
Ahora sella libGLdispatch.so.0 + libOpenGL.so.0 + opengl.pc y nada más: CERO ficheros en común con
mesa. Radio medido: 1 dependiente.
⚠ Esto quita el DAÑO, no cierra el muro: libOpenGL.so.0 enlaza y en runtime sigue sin haber vendor
(no hay libEGL_mesa.so.0 ni egl_vendor.d). Cerrarlo es re-sellar las TRES mesa con -Dglvnd=true en
un movimiento, y el precio está medido: `yupana radio mesa` = 148 directos, 154 transitivos, 153
sellados que caen a deuda, las CINCO imágenes. Decisión de distro, no arreglo de paso.
── obs-studio: el cache-hit tapaba una regresión ───────────────────────────────────────────────
Al forzar su rebuild, el configure murió pidiendo `qrcodegencpp`, que no es receta de ninguna cola.
La causa NO era libglvnd. La receta dice «[source] de takana NO clona submódulos ⇒ sus directorios
llegan VACÍOS» y por eso hacía `touch plugins/obs-websocket/CMakeLists.txt`. Esa premisa dejó de
ser cierta: los submódulos YA se materializan, así que `touch` no vacía nada —sólo toca la mtime de
un fichero que existe— y el CMakeLists de verdad entra pidiendo su dependencia.
Corregido a `mkdir -p` + `: >` (truncar), que funciona con submódulos y sin ellos: la receta deja
de depender de una premisa que puede cambiarle bajo los pies. Construye y sella en el hub.
wireplumber b3:bd5471f00f5cbd229fcb252ceced41c35547725733708426bf7a42ea3e39a7bd
Arreglo del hueco que destapó el barrido de servicios. `pipewire` está en los cuatro escritorios y
arranca, pero se construyó con `-Dsession-managers=[]`: sin gestor de sesión no hay política de qué
es entrada, qué es salida ni qué se enlaza con qué, y el sink que ve la aplicación es `auto_null`
—el nodo de descarte—. Audio que parece vivo y no suena.
wireplumber existía en `incoming-gnome` y en `incoming-cosmic`, y desde KDE o sway NO SE ALCANZA:
una receta resuelve sibling-first y después el catálogo PADRE, nunca una cola hermana. Por eso la
copia va al corpus, que es el único sitio desde el que las cuatro imágenes la ven — el mismo
argumento por el que viven acá `mpv` y `atuq`.
Medido antes de copiar, no supuesto:
- `yupana radio wireplumber` = **0 dependientes** ⇒ esta copia no fuerza un solo rebuild ajeno.
- Las tres variantes sellan DISTINTO (gnome b3:71afcef4…, cosmic b3:7ad25184…, corpus b3:bd5471f0…).
No es la enfermedad de las dos glib: ninguna imagen hidrata dos wireplumber, cada una toma la
suya. Es la figura de las tres `pulseaudio`, que también se quedaron a propósito.
- Consolidar las tres en una es el trabajo siguiente y NO se hace de paso: el método está probado
(es lo que se hizo con pipewire el 2026-09-03) pero GNOME y COSMIC están validados y cambiarles
el artefacto de audio sin probarlos sería pagar con su imagen una limpieza que no urge.