De 186 pendientes a 178. Cada veredicto con la prueba en la receta:
libxdamage opcional gtk3/gtk4/mutter con el backend X11 apagado
libsysprof-capture opcional gjs profiler=disabled, mutter profiler=false
protobuf opcional opencv -DWITH_PROTOBUF=OFF (módulo DNN)
fftw-single opcional pulseaudio fftw=disabled (ecualizador)
⚠ DOS OPCIONALES CON COSTE, que no es lo mismo que gratis:
libopus pipewire opus=disabled ⇒ el servidor de audio no codifica Opus. NO afecta a
reproducirlo en el navegador (atuq trae su propio códec, medido en su día).
bluez pipewire Y pulseaudio con -Dbluez5=disabled ⇒ LA DISTRO NO TIENE AUDIO POR
BLUETOOTH. No es un olvido de esta frontera: viene de cómo se construyeron esas dos
recetas. Habilitarlo exige traer bluez y RE-HASHEAR las dos, que no es gratis.
Queda escrito para que sea una decisión explícita y no un descubrimiento.
⚠ DOS HUECOS REALES:
desktop-file-utils provee update-desktop-database, que construye la caché MIME→APLICACIÓN.
Comprobado que NINGÚN artefacto del store lo trae. Con shared-mime-info ya
dentro, el escritorio sabe qué TIPO es un fichero y sigue sin saber CON QUÉ
abrirlo: el «abrir con» no funciona. Compañero natural del hueco de ayer.
purpose el marco de «compartir» de KDE (gwenview, okular, spectacle). La receta de
okular lo degrada con el flag oficial FORCE_NOT_REQUIRED_DEPENDENCIES y su
cabecera dice «los que no tenemos sellados»: un estado, no una preferencia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
bash es RAÍZ del perfil base —el shell de la distro— y cinco recetas repartidas en cuatro colas
(appstream, colord, gnome-shell, modemmanager, swayimg) instalan completados en el directorio
que este paquete define. Sin él NO los instalan y siguen adelante: nada falla, nadie se entera,
y el usuario se queda sin tabulador en las herramientas del sistema. Hueco de RUNTIME puro, de
los que ninguna métrica de deuda puede ver — por eso hizo falta la frontera para encontrarlo.
Va al CORPUS y como raíz de , no a una cola: lo alcanzan las cuatro imágenes de escritorio
y la CLI, y una receta resuelve sibling-first y después el catálogo padre, nunca una cola
hermana. Y entra como RAÍZ porque nadie lo alcanza por deps de build: los consumidores sólo
preguntan por su .pc.
Verificado como consumidor, las dos mitades:
pkgconf --variable=completionsdir bash-completion → /usr/share/bash-completion/completions
el marco carga 131 completados y complete devuelve regla · 522 ficheros instalados
REPRODUCE bit a bit · tarball en el mirror
Triaje: los huecos vuelven a CERO. base pasa de 26 a 27 raíces, clausura 58/58 sellada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Los 193 pendientes que destapó el barrido de sway y cosmic empiezan a bajar. Cada veredicto va
con la prueba en la receta, no con una corazonada:
vala opcional las 5 que lo piden llevan vapi=false explícito
gi-docgen opcional gtk4 documentation=false, pango/libadwaita gtk_doc=false
shiboken6-generator opcional las 8 recetas KF6 llevan -DBUILD_PYTHON_BINDINGS=OFF
xvfb-run opcional sólo envuelve tests, y los tests no se corren
libxinerama opcional gtk3/gtk4/mutter con el backend X11 apagado; es Wayland-only
openjpeg opcional ⚠ CON COSTE: poppler -DENABLE_LIBOPENJPEG=none y swayimg
jp2=disabled ⇒ un PDF con imágenes JPEG 2000 abre pero esas
imágenes NO SE VEN. Decisión tomada, no olvido — pero queda
escrito para que el día que alguien reporte 'este PDF sale con
huecos' la respuesta esté acá.
bash-completion HUECO bash es RAÍZ del perfil base, o sea el shell de la distro. Las 5
que lo piden instalan sus completados en el directorio que ese
paquete define y sin él no los instalan: nada se rompe y el
usuario se queda sin tabulador. Barato: datos + un script.
De 193 pendientes a 186. Quedan los de 3 consumidores hacia abajo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
La lista de grafos que consulta 'frontera' estaba escrita a mano y sólo tenía corpus, kde y
gnome. Consecuencia: 'yupana frontera escritorio-sway' y '... escritorio-cosmic' fallaban con
'perfil sin nodos en ningún grafo de estado (¿regeneraste con build-state.py?)' — culpando a un
grafo viejo con los cinco frescos de hacía minutos. Dos escritorios enteros sin poder responder
'¿qué me falta?'.
⚠ QUINTA vez que cae el mismo cable en este repo: GNOME (2026-07-27), COSMIC (2026-08-03) y
wlr/sway (2026-08-26) en cosecha-cron.sh, keystones y duplicados (2026-09-08) en el mismo sitio,
y ahora acá. Las cuatro anteriores se arreglaron AGREGANDO una línea, que arregla el caso y deja
la trampa puesta. Esta vez el fichero se DERIVA de la cola que el perfil declara: corpus →
build-state.json, incoming-<x> → build-state-<x>.json. Una cola nueva trae su grafo sola.
Verificado con control: 'base' sigue usando build-state.json y dando los mismos 41 candidatos.
Y lo que estaba tapado aparece — sway 175, cosmic 148, gnome 191, kde 276.
El triaje pasa de 172 a 365 candidatos (193 nuevos). No es deuda nueva: es deuda que no se
podía ver. Los que más recetas piden encabezan la cola — gi-docgen y vala con 9 cada uno.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
La frontera queda en CERO huecos y cero pendientes, y el perfil escritorio-kde en 302/302
sellados. Los tres eran huecos de RUNTIME: KDE sellaba 1037/1037 con la autorización muerta,
los controles QML en estilo genérico y ninguna app X11 capaz de arrancar.
polkit-qt-1 0.201.1 — al conectarlo se vio lo que ninguna métrica decía: kauth NO traía plugin
de backend, ni siquiera el directorio. Ahora trae kf6/kauth/backend/kauth_backend_plugin.so con
el símbolo KAuth::Polkit1Backend. Cascada de 26 dependientes reconstruida, 26/26 sin fallos.
Añadirlo cambió la rama del CMake y pidió kwindowsystem y tras él las X11 — cada cosa dicha por
el configure al fallar, no supuesta.
qqc2-breeze-style 6.7.2 (la del stack Plasma, NO la 6.7.4 de nixpkgs: mezclar versiones de un
mismo release es pedir desajuste de ABI en los plugins QML, que se ve en runtime y no al
construir). Enganchado a plasma-workspace como deps.runtime, no build: es un plugin que Qt carga
por la ruta de imports. Verificado que runtime no re-hashea ⇒ cero rebuilds.
xwayland 24.1.13 — resultó ser una CADENA de seis: libfontenc, libXfont2, libxkbfile,
libxshmfence, xkbcomp y el servidor. Ninguna existía. Tres tropiezos con su lección:
· libxkbfile 1.2.0 ya no trae ./configure: es sólo meson. El resto de las libs X11 de la cola
son autotools, así que copiar su plantilla era lo natural y estaba mal.
· «checking for freetype2... no» culpaba a freetype, que estaba perfecta: freetype2.pc declara
Requires: zlib, libpng y pkg-config resuelve transitivamente. El mensaje señala al paquete
equivocado.
· libXfont2 construye una .so y moría en «recompile with -fPIC» contra la libfreetype.a
estática. Van las variantes -shared.
Y xkbcomp es el eslabón que NINGÚN build habría delatado: xwayland lo lanza como subproceso en
runtime (xkb/ddxLoad.c) y su meson lo pide con required:false, así que la receta compila igual
y el servidor arranca sin teclado. Se encontró leyendo la fuente.
⚠ Sin GLX, y no por preferencia: glx/meson.build:43 pide dependency('gl') y ninguna cola publica
gl.pc — las tres recetas de mesa van con glx y glvnd apagados por decisión anterior. Las apps
X11 corren en 2D. ⚠ Y el meson.build RAÍZ engaña: ahí build_glx sólo enciende una tabla hash y
parece gratis; yo mismo di la preocupación por infundada mirando el fichero equivocado.
Verificado: Xwayland arranca y se identifica («The X.Org Foundation Xwayland Version 24.1.13»),
las 6 REPRODUCEN bit a bit, los 5 grafos con deuda 0, y los 6 tarballs en el mirror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Salió de 'yupana frontera' en cuanto nix volvió a correr: kcoreaddons la pide y no había receta.
Es un hueco de RUNTIME, no de build — KDE sella 1037/1037 sin ella, y por eso NINGUNA métrica de
deuda la veía. Sin la base MIME el escritorio no resuelve asociaciones, iconos por tipo ni
'abrir con', y GIO cae a adivinar por extensión.
Va al CORPUS, no a incoming-kde: la necesitan las cuatro imágenes de escritorio, y una receta
resuelve sibling-first y después el catálogo padre, nunca una cola hermana.
Dos cosas que se comprobaron ANTES de construir, no después de que fallaran:
· El tarball de GitLab no trae el submódulo 'xdgmime' — .gitmodules lo declara y el directorio
llega vacío. Leyendo meson.build:26-47 se ve que sólo lo usa la suite de tests y con
required:false, así que con build-tests=false lo único que pasa es un warning.
· 'i18n.merge_file' corre SIEMPRE, no lo gobierna build-translations: no traduce UI, arma el
fichero de datos desde el .in. De ahí el msgfmt, que gettext-tiny sí provee.
'--prefer-static' desde el principio, que es la lección de json-glib de hace un rato: el binario
sale ESTÁTICO con 0 NEEDED y corre en el host.
Y el artefacto trae la mime.cache ya compilada (update-mimedb=true, que upstream trae en false):
sin ella el paquete son datos crudos y alguien tendría que acordarse de correr
update-mime-database al armar cada imagen.
Verificado:
prueba del consumidor update-mime-database genera 157.560 bytes de cache y 26 media-types
binario estático, 0 NEEDED, corre fuera del sandbox
repro REPRODUCE bit a bit (la caché se genera en el build: era candidata)
mirror el tarball subido al Storage Box (ADR 0013)
frontera huecos 4 → 3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'yupana frontera' llevaba desde el 2026-08-10 sin poder correr acá porque el hub no tenía nix.
Instalado desde el repo de Artix (extra/nix 2.35.2). Dos cosas que costaron medirse:
· El paquete quedó ROTO al instalarse: le faltaban libboost_{context,iostreams,url}.so.1.92.0
porque el sistema tiene boost 1.91 y 280 paquetes sin actualizar. NO se hizo 'pacman -Syu':
280 paquetes en un server protegido, sin backup, que hospeda la granja, gitea y el runner de
CI no es una decisión que tome un arreglo de tooling. El ensayo en seco mostró que la
reparación acotada eran TRES paquetes exactos (boost-libs, source-highlight, gdb) sin
cascada, y eso sí se hizo. Control después: gdb 17.2 sigue arrancando.
· El store por defecto iba a ~/.nixstore, o sea a '/', que tiene 21 G libres frente a los 126
del volumen. Llenar '/' no rompe una tanda, rompe la máquina. NIX_ROOT pasa a apuntar a
work/nixstore, que además ya es el sitio de lo pesado-y-regenerable. La caché git de nixpkgs
(~69 M) NO la gobierna esa variable —nix la pone en XDG_CACHE_HOME— así que en gioser está
mudada al volumen con un enlace: ~/.cache/nix -> /mnt/vvv/nix-cache.
Con eso, la frontera vuelve a dar respuesta, y es pequeña como corresponde a un corpus cerrado:
172 candidatos → 150 opcionales, 11 provistos, 5 nix-ismos, 4 HUECOS reales (polkit-qt-1,
qqc2-breeze-style, shared-mime-info, xwayland) y 2 pendientes de clasificar (mesa-libclc,
python3-3.14.7-env).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'yupana frontera <perfil>' —el verbo que responde qué pide upstream y no tenemos— moría con
veinte líneas de traceback terminadas en FileNotFoundError: 'nix'. Eso tiene la cara de un bug
de código y manda a depurar el script, cuando lo único que falta es una herramienta que en esta
máquina nunca se instaló: el hub gioser no trae nix, y los docs/state/seed-*.json del repo se
generaron en otra máquina el 2026-08-10. El coste no es el susto, es el desvío.
Ahora es una precondición explícita que dice qué falta y qué hacer, y sale 2.
⚠ Dos errores míos que los controles cazaron en el acto, y por eso van escritos:
· Eximí a --calibrar de la comprobación 'porque no usa nix'. Lo usa: el control lo mostró
imprimiendo 'nix eval lote 1 (3 nombres)…' y muriendo igual. La exención era una suposición
con forma de hecho.
· Con la precondición delante del uso, 'seed-graph.py' a secas pasó a quejarse de nix en vez
de explicarse. Un fallo de entorno va DESPUÉS del uso.
Verificado en los tres caminos: sin args → uso; --frontera → mensaje de entorno; --calibrar →
mensaje de entorno.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Las 6 que quedaban (fuse3, hwdata, icu4c, libdisplay-info, libelogind, lua) no tenían copia en
el corpus, así que borrar una habría roto su cola: la resolución es sibling-first y después el
catálogo PADRE, nunca una cola hermana. Se promueve una y se barren las 16.
Las 16 eran byte a byte IDÉNTICAS entre sí, con el mismo ArtifactHash — medido con 'hammer hash',
no deducido del nombre. Cada una la comparten entre 2 y 4 imágenes, que es exactamente el
criterio que dejó escrito xkeyboard-config al promoverse: lo que comparten varias imágenes tiene
que vivir en el corpus o no lo alcanzan.
A diferencia de aquella, acá las copias SÍ se barren en el mismo movimiento: sin copia en el
corpus no había dónde caer, y dejarlas sería mantener hasta 4 ficheros que son el mismo hash.
Verificado con la huella de las 1167 recetas antes y después:
ficheros de receta 1167 → 1157 (16 borradas, 6 promovidas)
hashes supervivientes ninguno se movió ⇒ CERO rebuilds
los cinco grafos deuda 0, huérfanas [], sellados == recetas
static-audit MIENTEN 0 de 690
duplicados SOMBRAS REDUNDANTES: 0 ← de 14
⚠ Y una lectura que casi publico mal: mi primera comparación de hashes dijo 'CAMBIÓ ✗' en las
seis. Era el grep de la comparación, que no casaba y dejaba el valor 'antes' vacío — los hashes
nuevos eran idénticos a los medidos minutos antes. Un instrumento roto que grita es tan malo
como uno mudo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Las 14 sombras marcadas 'redundante' dan el MISMO ArtifactHash en todas sus colas — no son
variantes, son el mismo artefacto declarado dos, tres, cuatro y hasta cinco veces. Medido con
'hammer hash' receta por receta, no deducido del nombre.
Se barren sólo las que YA tenían copia en el corpus, porque la resolución es sibling-first: una
dep desde incoming-kde busca incoming-kde/x y, si no está, CAE al corpus/x. Son 8 nombres, 14
ficheros: bzip2-shared, glib-shared, lcms2, mpc, pcre2-shared, sqlite-shared, xkeyboard-config
y xz-shared.
13 de los 14 eran byte a byte idénticos al del corpus. El decimocuarto, xkeyboard-config,
difiere en 14 líneas de COMENTARIO (mismo hash), y ese comentario es el permiso explícito para
esto: quien la promovió el 2026-09-05 escribió que dejaba las copias a propósito y que
'barrerlas es otra unidad de trabajo, no un efecto colateral de ésta'. Ésta es esa unidad.
Verificado con la huella de las 1181 recetas antes y después: NINGÚN hash superviviente se
movió ⇒ cero rebuilds. Y tras regenerar los cinco grafos: deuda 0, huérfanas [], y todas las
clausuras de perfil siguen completas.
⚠ Nota sobre una lectura que casi me confunde: los conteos de 'recipes' NO bajan (kde sigue en
1034) y eso es lo correcto — cuentan nodos alcanzables, no ficheros. Antes incoming-kde/x tapaba
a corpus/x; ahora el del corpus ocupa su lugar. Un nodo por otro.
Quedan 6 nombres (fuse3, hwdata, icu4c, libdisplay-info, libelogind, lua) sin copia en el
corpus: ésos exigen promover una primero, y van aparte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Cuarta vez que cae el mismo cable. Los comentarios del propio fichero ya lo cuentan para GNOME
(2026-07-27), COSMIC (2026-08-03) y wlr/sway (2026-08-26): un frente que el latido no regenera
envejece en silencio, y un grafo viejo miente con la misma cara que uno fresco.
keystones.json y duplicados.json estaban parados en 2026-09-05. Con un agravante que los otros
tres no tenían: keystones es el fichero que responde '¿qué construyo después?', así que rancio
no se limita a envejecer — DESINFORMA. Medido: decía '1 nudo en deuda, camino crítico
corpus/firefox → corpus/atuq' con los dos sellados hacía horas. Regenerado dice 0 nudos en
deuda, que es la verdad.
Van también al 'git add' Y al commit acotado por pathspec: regenerar sin publicar deja el
fichero fresco en el hub y viejo para todos los demás.
Probado con un ciclo real: 'keystones.json ✓ duplicados.json ✓ estado commiteado+pusheado'.
⚠ De paso, dos cosas que este rato dejó claras sobre mis propias comprobaciones: 'pgrep' y
'grep [c]osecha-cron' se autodetectan cuando MI comando menciona el fichero, así que la guarda
'¿está corriendo?' dio dos falsos positivos. La comprobación fiable mira /proc/*/cmdline y pide
que argv[0] sea un shell y argv[1] el script.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'-Ddefault_library=static' gobierna cómo se construye la librería PROPIA, y eso funcionaba: el
artefacto trae libjson-glib-1.0.a. Pero no dice nada de con qué se enlazan los EJECUTABLES, así
que json-glib-format y json-glib-validate salían dinámicos contra libffi.so.8, libz.so.1 y
libc.so — y ese libc.so es la musl de zig, 255 bytes de linker script en el host. O sea binarios
que NO CORREN fuera del sandbox, en una receta que declara link=static. Mismo remedio que
appstream, que ya llevaba --prefer-static.
Medido antes y después:
json-glib-format dinámico, 3 NEEDED → ESTÁTICO, 0 NEEDED
json-glib-validate dinámico, 3 NEEDED → ESTÁTICO, 0 NEEDED
libjson-glib-1.0.a igual (los consumidores no cambian de forma)
Y la prueba del consumidor, que es la que vale: json-glib-format sobre un JSON real corre EN EL
HOST y devuelve {"a":1}. (El ruido de libgvfsdbus.so en stderr es glib intentando dlopen
módulos GIO desde un binario estático: inocuo, la salida sale bien.)
Cascada reconstruida en orden de onda, con el cierre transitivo sacado de yupana y no de un grep
—que no veía a zathura ni a zathura-pdf-poppler por tener el array de deps multilínea, y la regla
del repo es justamente ésa—: zathura, incoming-cosmic/xdg-desktop-portal, zathura-pdf-poppler.
Verificado después, no antes:
static-audit MIENTEN 0 de 690 ✅ toda receta que declara link=static lo cumple
los 5 grafos deuda 0
repro REPRODUCEN 3 · DERIVA 0 · NO-DETERMINISMO 0
⚠ incoming-gnome/json-glib NO se toca: declara link="dynamic" a propósito porque el shell de
GNOME dlopea .so reales para introspección. No miente — es honesta, y su radio de 10 no se paga.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El vigía decía 5 fuentes muertas y sólo UNA estaba en riesgo de verdad: busybox y freetype
tenían el tarball en la caché local Y en el mirror, y firefox-pgo-profile apunta a .invalid a
propósito. La que no tenía copia en ningún lado era libtool (incoming-kde), con ftpmirror.gnu.org
devolviendo 502 — o sea una receta que ya no se podía reconstruir y nadie lo sabía.
Arreglado: el tarball se bajó de ftp.gnu.org, se verificó contra el sha256 PINEADO (coincide
byte a byte), y está en la caché local y en el mirror. La URL de la receta pasa al espejo que
responde, y se midió que eso no re-hashea nada — ANTES y DESPUÉS dan el mismo
b3:5c09bff5..., que es el ADR 0013 comprobado en vez de citado.
Y el vigía ahora cruza cada fuente muerta con la caché local y el mirror, porque mezclar
'upstream caído con el blob guardado' (noticia) con 'caído y sin copia' (emergencia) hace que
el número suba, nadie lo mire, y el día que uno importe esté enterrado entre cuatro que no. A
un guardián lo mata el ruido. Ahora informa EN RIESGO aparte:
== vivas 1175 / 1179 muertas arriba 4 EN RIESGO 0
⚠ Y si el mirror no se puede consultar —el worker no tiene la clave del Storage Box— dice
'indeterminado', NUNCA 'EN RIESGO': un instrumento que no pudo correr no puede parecer un
hallazgo.
Probado con rotura a propósito y con los dos controles, que es lo que separa un guardián que
sirve de uno que nunca saltó:
sha inexistente + upstream muerto → EN RIESGO ✓ salta
blob en caché + upstream muerto → cache-local ✓ no salta
mirror inalcanzable → indeterminado ✓ no alarma en falso
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Con '.fleet' ausente —el hub recién clonado, o sea el único caso que importa— el 'cat' de un
fichero inexistente devuelve 1, y como el script corre con 'set -o pipefail' la tubería hereda
ese estado y el '&& mv' no dispara: el fichero se generaba correctamente y se quedaba sin
mover. Se añaden los '|| true' que faltaban.
⚠ Y por qué mi prueba no lo vio, que es lo que vale anotar: extraje el bloque real del script
para probarlo, pero le puse 'set -u' en vez del 'set -uo pipefail' que el script trae. Copié
el código y no el ENTORNO en que corre, y sin pipefail el caso pasaba. El extractor ahora lee
la línea 'set -' del propio script en vez de que yo la escriba de memoria.
Verificado con un ciclo REAL tras borrar .fleet a mano:
── cosecha-cron arranca
==> dev.gioser.net (154.197.1.13)
siembra ✓
manifiesto ✓ (worker: 764 · total: 4743)
.fleet quedó: dev.gioser.net 154.197.1.13
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'.fleet' está gitignored con razón —los workers efímeros entran y salen, y el reaper lo
reescribe en cada ciclo— pero eso tenía un coste que nadie había pagado: un hub recién
clonado nacía con la flota VACÍA y la granja quedaba desconectada EN SILENCIO. La cosecha
decía 'flota vacía' cada 30 min, los artefactos del worker no volvían, estado-granja.sh
reportaba 'no hay worker vivo' con el worker compilando, y nada fallaba. Así se descubrió
esto hoy, por casualidad.
Se separa lo que debe sobrevivir a un clon (scripts/farm/flota-permanente, versionado) de lo
que es estado de ejecución (.fleet). cosecha-cron siembra .fleet desde el fichero fijo al
empezar cada ciclo, uniendo por nombre. Probado con la siembra extraída del script real:
hub recién clonado (.fleet ausente) → queda dev.gioser.net ← el caso que rompía
con un hworker-3 efímero ya dentro → conviven los dos, sin duplicar
ejecutada dos veces más → idempotente, sigue 1 línea por worker
comentarios del fichero versionado → 0 se cuelan
Y de paso el commit del propio cron pasa a ir acotado por pathspec. Hacía 'git add <rutas>' +
'git commit' a secas, que es justo lo que la regla 2 de CLAUDE.md declara insuficiente: el
índice es compartido y el commit se lleva el índice entero. Pesa más acá que en ningún sitio
porque corre desatendido cada 30 min mientras hay agentes trabajando.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El dato ya estaba en memoria y aun así se perdió dos veces, así que ahora vive en los sitios
que se leen sin buscarlos: regla 1 bis de CLAUDE.md (que carga todo agente en cada sesión) y
la skill 'granja' del repo. Se compila en dev.gioser.net PARA NO GASTAR HETZNER; no se
levantan cajas hcloud salvo petición explícita.
Y se arregla la fragilidad que salió al mirar: el reaper de cosecha-cron expulsa de .fleet
todo lo que no esté en hcloud, y el LXC se salvaba SÓLO porque su nombre contiene la subcadena
'gioser' y caía en una lista negra que existe para proteger al hub de Hetzner — no tiene nada
que ver con él. Medido con el bloque real del reaper contra un .fleet de juguete:
nombre CON 'gioser' → 'en LISTA NEGRA ⇒ intocable' sobrevive
el MISMO host como 'pruebasia-lxc' → 'ya no existe en hcloud' .fleet VACÍO
O sea que la granja se mantenía conectada por una casualidad de nombre, y con otro nombre
volvía a 'flota vacía' en el primer ciclo sin que nada fallara. Ahora el reaper pregunta por
SSH si el host responde, que es preguntarle a la máquina en vez de al nombre. Verificado en
las dos direcciones, con control:
host vivo, nombre sin 'gioser' → 'no es de hcloud pero RESPONDE ⇒ se queda (€0)'
host que no responde → 'no responde ⇒ lo saco de .fleet' (intención original)
Queda anotado también que .fleet está gitignored: un hub recién clonado nace con la flota
vacía y la granja queda desconectada en silencio — la cosecha dice 'flota vacía', los
artefactos no vuelven, y estado-granja.sh reporta 'no hay worker vivo' con el worker
compilando. Es como se descubrió esto hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Segunda vez el mismo día: un firefox colgado de la verificación A/B se quedó con el lock de
build de la granja. La causa es del arnés, no de firefox: 'kill $BW' mata el bwrap EXTERIOR,
que no es el init del namespace de PID, así que el proceso de dentro sigue vivo — y si el
arnés corría bajo flock, hereda el fd del lock y deja a la granja muda hasta que alguien mire.
Medido con control negativo y positivo:
matar sólo el bwrap → queda 1 firefox vivo (reproduce la fuga)
setsid + matar grupo → quedan 0 (arreglado)
Se documenta además que el 'flock' del uso lleva '-o', que no es opcional.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El cp nunca corrió: zsh aborta el comando entero cuando un glob no casa, y ese directorio no
tenía ningún .txt, así que tampoco se copió el .log — y el rm -rf siguiente se ejecutó igual.
Borrar sin comprobar que la copia existía.
Se deja un PERDIDO.md en vez de un directorio vacío, que es lo que la regla 3 de CLAUDE.md
advierte: un directorio vacío no es evidencia, es un nombre. La firma medida antes de la
pérdida (7/1/6, cero screenshot) sí está registrada en la cabecera del cazador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
arnés viejo (sólo CONTENT) 98 segfaults · 1 colgada · 49/50 screenshots
arnés nuevo (los cinco) 0 segfaults · 0 colgadas · 50/50 screenshots
Queda dicho en la cabecera qué prueba esto y qué no. El MECANISMO sí: 98 → 0, exactamente 2
por corrida, en el 100% de las corridas; un efecto determinista se refuta con pocas muestras.
El CUELGUE no: 1 contra 0 en 50 pares no es significativo — con la tasa medida (3 en 130 ≈
2,3%) ver cero en 50 tiene ~31% de probabilidad aunque nada hubiera cambiado, y harían falta
~200 corridas para un negativo convincente.
Lo que sí sostiene: las tres colgadas observadas tienen la MISMA huella exacta (7 fallos de
lanzamiento de pestaña, 1 de rdd, 6 messageManager is null, cero screenshot) y las tres
cayeron donde los ayudantes revientan. Ninguna apareció sin el mecanismo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El lock lo sostiene la descripción de fichero abierta y los hijos la heredan: con 'exec 9>' +
'flock 9' cualquier descendiente que sobreviva al script retiene el lock para siempre, y el
siguiente que lo pida espera sin que nada falle. Medido hoy: dos firefox colgados de una caza
sobrevivieron al kill de su bwrap y dejaron a la granja hora y media sin poder compilar.
Dos formas de defecto ⇒ dos arreglos, y no son intercambiables:
· cosecha-cron, campana-deuda, harvest-harkaq — el lock cubre el script ENTERO, así que se
re-ejecutan bajo 'flock -o' (cierra el fd antes de ejecutar). Ir poniendo '9>&-' comando a
comando ahí es jugar a los topos. '-E 77' separa 'estaba ocupado' de 'el trabajo falló',
que con el 9> no se distinguían.
· farm-worker-loop, latido — el lock vive en un subshell / es a propósito el mecanismo de
vida, así que basta '9>&-' en los hijos. En latido NO se puede usar -o: ahí el fd retenido
ES como --status sabe que el latido vive sin pidfile. Su modo de fallo era el peor de
todos: un nieto fugado dejaba el latido MUERTO PERO APARENTANDO ESTAR VIVO.
Medido antes de elegir, no deducido del manual:
exec 9> + flock 9 → el nieto RETIENE (control negativo: reproduce el fallo)
exec {L}> (fd auto) → el nieto RETIENE (bash NO lo marca close-on-exec)
flock -o / 9>&- hijo → lock LIBRE
Y probados los cinco sobre el fichero real, no sobre una maqueta: copias truncadas justo tras
el bloque del lock, invocadas con RUTA RELATIVA DESDE OTRO DIRECTORIO (que es lo que rompe un
$0 sin resolver — de hecho la primera versión de harvest-harkaq calculaba YO después del cd y
habría fallado ahí). Los tres entran, ninguno deja el lock tomado por el nieto, y la exclusión
mutua sigue funcionando con su mensaje y su exit 0 — que es lo que un arreglo de locks puede
romper en silencio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El lock lo sostiene la descripción de fichero abierta y los hijos la heredan. Medido hoy: dos
firefox colgados de una caza sobrevivieron al kill del bwrap que los envolvía y dejaron a la
granja sin poder compilar durante hora y media, sin que nada fallara — el siguiente flock
simplemente espera.
Comprobado en los dos sentidos con control positivo y negativo:
flock lock sh -c 'sleep 25 & exit 0' ⇒ el nieto retiene el lock
flock -o lock sh -c 'sleep 25 & exit 0' ⇒ lock libre
Se añade también cómo diagnosticarlo (fuser -v sobre el fichero de lock, que nombra al proceso
fugado) y se deja anotado que los scripts de scripts/farm/ usan el estilo 'exec 9>' + 'flock 9',
vulnerable igual, como deuda conocida sin barrer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Cada arnés headless tenía su propio subconjunto de MOZ_DISABLE_*_SANDBOX, elegido en su
momento sin medir: banco 1 de 5, perfilar 4, codecs 3, ruteo 3, nativo 2, inicio 2. Con
cualquier subconjunto incompleto el sandbox de Firefox sigue intentando montar su
user-namespace dentro del de bwrap, falla con 'uid_map: EPERM' y deja un ayudante
'Sandbox Forked' muerto de SIGSEGV por cada intento — en el 100% de las corridas, después
de escribir el PNG y por eso invisible.
Medido: 2 cadáveres por corrida con sólo CONTENT apagado, 0 con los cinco. Verificado sobre
el arnés ya editado: segv=0 EPERM=0 screenshot=sí.
Acá no se pierde seguridad: bwrap ya es la jaula, el sandbox de Gecko es redundante y lo
único que hace dentro es fallar.
⚠ Cambia las condiciones de medición del banco de PGO: las cifras de docs/26 se tomaron con
el arnés viejo. Los cocientes del PGO comparan dos firefox bajo el mismo arnés y deberían
aguantar; la línea base absoluta de arranque no tiene por qué. Anotado en la cabecera.
No se tocan scripts/wlr/dunst-headless.sh (otro agente trabajando en él) ni los cazadores,
que conservan el entorno viejo a propósito para poder reproducir la condición original.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
No era la presión de memoria. La caza bajo presión (4 niveles × 20 corridas, con el nivel de
presión MEDIDO por MemAvailable y PSI) reprodujo el cuelgue por primera vez en 282 corridas
—2 de 80, 2,5%, compatible con el 3,5% original— pero SIN dosis-respuesta: los dos cuelgues
cayeron en el nivel con PSI 0,00 y los dos niveles apretados dieron cero.
Tres hipótesis muertas, cada una con su medición:
· OOM se lleva al hijo → oom_kill de /proc/vmstat no se movió (delta=0) y los hijos vivían
· presión de memoria → sin dosis-respuesta
· fork server de Gecko → A/B del pref: el proceso forkserver desaparece (35 muestras → 0,
o sea que el pref hizo efecto) y siguen los mismos 2 segfaults
La causa apareció comparando el log de una colgada con el de una BUENA, que es lo que faltaba:
en las 6 buenas TAMBIÉN revientan 2 hijos con SIGSEGV, después de escribir el PNG y por eso
invisibles. Muestreando /proc adentro, se llaman 'Sandbox Forked': los ayudantes que el sandbox
propio de Firefox forkea para montar su user-namespace, que no puede montar porque ya estamos
dentro del de bwrap ('writing /proc/self/uid_map: EPERM').
Las tres cantidades bajan juntas hasta cero, que es forma de cadena causal:
sandbox completo segv=7 SIN screenshot EPERM=9 SandboxForked=725
content.level=0 segv=2 con screenshot EPERM=2 SandboxForked=68
los 3 prefs .level a 0 segv=1 con screenshot EPERM=1 SandboxForked=33
las 5 MOZ_DISABLE_* segv=0 con screenshot EPERM=0 SandboxForked=0
Y el sandbox completo cuelga DETERMINISTA (rc=124 a los 180 s). Comparte familia con el
intermitente pero no está probado que sean el mismo: al determinista le faltan los 'Failed to
launch'. Queda dicho como lo que es.
Arreglo para cualquier firefox headless en bwrap (donde el sandbox de Gecko no aporta nada,
porque bwrap ya es la jaula):
MOZ_DISABLE_{CONTENT,GMP,RDD,SOCKET_PROCESS,UTILITY}_SANDBOX=1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2