Imagen 417847948 («hammer-golden-rust-go-2026-08-08») reemplaza a 408909310 como default de
farm-up.
EL PROBLEMA QUE CIERRA: hammer invoca `cargo vendor` y el toolchain Go en el HOST, no dentro
del sandbox — el vendoreo pasa ANTES de entrar a la caja. La golden vieja no los traía, así que
la granja no podía construir NINGUNA receta Rust ni Go: COSMIC entero, las 228 Rust y las 362
Go del corpus. El 76% del catálogo dependía de UNA SOLA máquina, el hub — justo lo que el
respaldo de ayer intentaba dejar de asumir.
VERIFICADO ANTES DE SNAPSHOTEAR, no después: `cosmic-bg` —que minutos antes moría con
`spawn cargo vendor: No such file`— sella en el worker. Un snapshot de una máquina a medio
arreglar es peor que ninguno.
MISMAS VERSIONES QUE EL HUB (cargo/rustc 1.96.0, go 1.26.4) a propósito: introducir un skew de
toolchain nuevo mientras se arregla otro es cómo se fabrican los fallos que nadie reproduce. Y
1.96 es además el techo MSRV del sandbox ya documentado.
NO RELAJA LA HERMETICIDAD: cargo/go son herramientas de FETCH, del mismo orden que `git` para
clonar. El build sigue ocurriendo dentro del sandbox con el árbol ya vendoreado. Es lo contrario
del caso «no engordar el rootfs del worker», que habla del rootfs DEL SANDBOX.
El PATH queda persistido en /etc/profile.d/hammer-toolchain.sh para que sobreviva a los logins
no interactivos de la granja. Y la imagen trae el store del worker al momento del snapshot (KDE
y GNOME reconstruidos hoy), así que los workers nuevos arrancan con más caché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Podé el store por tercera vez hoy: 363 artefactos superados, 24 G. Comparado con la poda de la
tarde: **362 en común**. Volvieron otra vez, pese al arreglo de esta tarde.
LA CAUSA de que el arreglo no sirviera: puse el `--exclude-from` del registro de podados sólo en
`farm-sync.sh`, y el latido NO usa ese script. `cosecha-cron.sh` tiene su PROPIO rsync del store
(línea 71), y corre cada 30 minutos por cron ⇒ una poda de 24 G quedaba deshecha en menos de una
hora. Los tres commits de «cosecha granja» de la madrugada son exactamente eso.
Y al ir a arreglarlo, en vez de parchear y seguir, busqué si había más: **son CUATRO** las rutas
que bajan el store del worker — `cosecha-cron.sh`, `farm-sync.sh`, `harvest-go.sh` y
`farm-down.sh`. Tenía protegida una de cuatro.
El error de método vale más que el bug: arreglar una de varias copias y declarar victoria es
PEOR que no arreglar, porque el síntoma se atenúa lo justo para dejar de mirar. Lo que lo
delató fue comparar los manifiestos de dos podas (`comm -12`), no leer código. Dos podas con el
mismo número no son coincidencia — ya está anotado como regla, y hoy la regla pagó dos veces.
Las cuatro llevan ahora el mismo `--exclude-from` y una nota que dice que son cuatro, para que
la quinta —si aparece— nazca protegida.
Disco: 112 G → 136 G tras la poda de ahora.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fui a mirar los fallos de la granja y el número no cuadraba: de 20 «fallos» de incoming-gnome,
17 correspondían a recetas CON ARTEFACTO VIGENTE en el hub. Primero pensé que eran marcadores
`.fail` rancios, y me equivoqué: `build-farm.sh` hace `rm -rf "$FARM"` al empezar cada ciclo,
así que el status es siempre del ciclo actual. Después pensé que el worker corría recetas
viejas, y también me equivoqué: comparados los md5, hub y worker tienen copias IDÉNTICAS.
LA CAUSA REAL está en `farm-sync.sh`: sube el código EXCLUYENDO /store y baja el store del
worker. El store viaja worker→hub y nunca hub→worker. O sea que **el worker no se entera de
nada de lo que se sella en el hub**, y cada ciclo reintenta —y vuelve a fallar— trabajo ya
hecho. Medido: de las 90 recetas de incoming-gnome, **87 ya están selladas en el hub**. El
worker estaba quemando el 97% de esa cola en repetir lo hecho.
EL ARREGLO no es mandarle los artefactos (gigabytes por un enlace de 8 Mbps) sino la LISTA:
`work/farm-sellados.txt` son los nombres `<hash>-<paquete>` del store del hub, unos kilobytes.
`farm-sync.sh` la regenera en cada sync (para que no envejezca sola) y `build-farm.sh` salta
la receta cuyo hash vigente ya esté ahí. Ojo al detalle de rsync: la subida excluye /work, así
que el manifiesto necesita un `--include` ANTES del `--exclude` — rsync aplica la primera regla
que casa, y sin ese orden el fichero no viajaba y el arreglo no habría hecho nada en silencio.
Y LAS TRES DEUDAS REALES DE LA GRANJA SON UNA. gnome-session, gnome-settings-daemon y gdm
mueren todas en GTK3, que el frente GNOME aparcó a propósito. Verificado contra el meson.build
de cada tag: gnome-session 48.0 lo pide incondicional; gnome-settings-daemon 48.1 pide gtk+-3.0
Y gtk+-x11-3.0 (dos de las tres deudas aparcadas, no una); gdm 48.0 lo tiene condicionado a
`if have_xdmcp` pero da igual, porque muere construyendo gnome-session.
De paso, gnome-session pasaba `-Dsystemd=false -Dsystemd_journal=false`, dos opciones que NO
EXISTEN en 48.0 (sus opciones reales son seis: deprecation_flags, session_selector,
systemduserunitdir, docbook, man, x11). Meson aborta en la primera opción desconocida, así que
la receta ni llegaba a configurar y el fallo real quedaba tapado. Corregido, más -Dx11=false.
El diagnóstico de fondo YA ESTABA en la receta desde el 2026-07-27 y era más completo que el
mío (dice GTK3 **y** libsystemd); quité la nota duplicada que había añadido.
Y en el respaldo: la comprobación inicial de SSH era de un solo intento y tiró la corrida al
relanzarlo llegando a casa, con el wifi aún sin levantar. Es el peor momento para rendirse —
quien relanza un respaldo interrumpido acaba de cambiar de red. Ahora reintenta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Podé el store por la mañana: 362 artefactos superados, 24 G. Lo volví a podar por la tarde:
362 artefactos, 24 G. Comparados los dos manifiestos, **solapamiento del 100%: son los
MISMOS 362**. No era casualidad ni recuento nuevo — es un bucle.
LA CAUSA no estaba en el gc sino en `farm/farm-sync.sh`, que bajaba el store del worker
ENTERO sin filtro. El worker nunca se poda, así que conservaba los superados y nos los
devolvía en cada cosecha. Podar → cosechar → vuelven → podar. El disco pagaba 24 G por
vuelta y el gc informaba éxito cada vez, lo que es peor que fallar: parecía que se avanzaba.
Es la misma familia de fallo que el gc que reportaba borrados falsos, sólo que un nivel más
arriba: entonces el borrado no ocurría, ahora ocurre y se deshace solo. En los dos casos el
mensaje de éxito era cierto y engañoso a la vez.
EL ARREGLO es darle memoria al sistema. `store-gc.sh` acumula lo podado en
`work/store-gc-superados.txt` y `farm-sync.sh` lo usa como `--exclude-from` al bajar el
store del worker. Es seguro porque el store es CAS: los nombres `<hash>-<paquete>` son
inmutables y un artefacto superado no vuelve a ser vigente salvo que una receta retroceda —
y si retrocediera, se reconstruye, que es barato.
Disco: 159 G → 182 G tras la poda de hoy, y esta vez debería quedarse.
De paso, en el respaldo: rsync 24 («ficheros del origen desaparecieron») NO es un fallo, es
justo lo que pasa al podar mientras se sube. El bucle de reintentos lo tomaba por error real
y habría parado el respaldo entero por algo inofensivo — y encima cuando el disco aprieta,
que es cuando menos conviene quedarse sin copia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El default era fósil de cuando `incoming-gnome-onda1` era el frente entero:
listaba esa cola (6 recetas) pero NO `incoming-gnome` (86, donde viven gtk4,
mutter y gnome-shell) ni `incoming-gnome-onda2` (la isla dinámica, 22). El
worker reportaba moler GNOME y molía 6 de 114.
Se agregan las dos y se mueven las tres ANTES de incoming-kde: con 205 recetas
KDE por delante, un ciclo se consumía sin darles turno aunque estuvieran
listadas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Faltaba la mitad del método. SÍ cruzaba incoming-cosmic (lo usé
antes de tocar libdisplay-info, pipewire y glib), pero la campaña no estaba en el
KHIPU: build-state.py sólo conocía --kde y --gnome, y targets.toml no declaraba
perfil.
Y eso no era cosmético. Antes de este commit, decía
21 dependientes transitivos y dos imágenes; ahora dice 31 y TRES —
escritorio-cosmic entre ellas, con incoming-cosmic=10 en el reparto por cola. O
sea que el próximo que tocara libinput, libudev-zero o mesa habría MEDIDO DE
MENOS, que es exactamente el punto ciego que la metodología existe para cerrar.
Tres piezas:
- build-state.py --cosmic (y su build-state-cosmic.json).
- perfil.escritorio-cosmic en targets.toml, con las raíces que cosmic-session
levanta MÁS los datos que ningún [deps] alcanza (iconos, xkb, fuentes, bash,
dbus). Primera medición: 54/61 listo, faltan 7.
- el LATIDO lo regenera, por la misma razón por la que se le agregó GNOME en su
momento: un frente que el cron no regenera envejece en silencio, y un grafo
viejo miente con la misma cara que uno fresco.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Las 3 fronteras que el propio comentario de gnome-desktop declaraba:
iso-codes receta nueva (datos + .pc). Ya no está en download.gnome.org (404);
Salsa es GitLab y su archive no es determinista (lección de cairo-shared),
así que la fuente es el .orig.tar.xz inmutable del pool de Debian.
Sin traducciones: i18n.gettext exige msgfmt completo y sólo hay
gettext-tiny — se vacían los 8 meson.build de dominio.
libseccomp receta nueva (autotools estático, gperf de build-dep real).
xkeyboard-config duplicada desde incoming-kde: fichero idéntico ⇒ MISMO ArtifactHash
(b3:bcf9b766) ⇒ ya está SELLADO. Frontera cerrada con cero rebuild.
Y un punto ciego del latido: cosecha-cron regeneraba build-state.json y el de KDE, pero
NUNCA el de GNOME. El grafo llevaba días mintiendo `never` sobre gobject-introspection y
toda la onda 2, que están selladas. Un grafo viejo miente con la misma cara que uno fresco.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Las 3 raíces de la onda 1 (gsettings-desktop-schemas, gobject-introspection,
gnome-desktop) copiadas a una cola propia + añadida al QUEUES del worker-loop.
Aislar evita que el worker dispare rebuilds de spidermonkey/mutter (onda 2/3).
El cierre real de RECETAS de las 3 es 39 nodos, 0 en incoming-kde: el "gap del
resolver" que temía era de seed-edges (.pc cairo→libX11), NO de deps declaradas.
El espinazo corpus (38 nodos: gtk4/cairo/pango/gdk-pixbuf en recipes/) está 100%
sellado ⇒ el worker cachea todo e sólo construye las 3. Hashes byte-idénticos a
los sellos de incoming-gnome (base_dir no entra al hash).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El loop del worker está SIEMPRE vivo (idle-loopea con la cola vacía). Contarlo
en hay_trabajo() reseteaba los ticks a 0 cada 10 min ⇒ el worker NUNCA acumulaba
idle ⇒ NUNCA se auto-mataba. Quedó 2h17m idle quemando € (2ª vez).
El loop construyendo YA se detecta por su hijo `hammer build`; el loop vacío NO
es trabajo. Se afina el match a `release/hammer.* build ` y se suma `campana-deuda`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cazado con el 1er worker real: el token vive en /etc/hammer-deadman.env (lo carga
systemd como EnvironmentFile), pero corrido A MANO (farm-up --puede-borrar) ese
fichero no se lee solo ⇒ mi cadena de fallback no lo veía ⇒ decía 'sin token' y
farm-up habría DESTRUIDO un worker que sí puede matarse. Fix: sourcear el
EnvironmentFile antes de la cadena de tokens desnudos. Verificado en worker real:
'SÍ: puedo borrar mi id=154276696'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El worker DEBE matarse solo, sin depender de la laptop (es la razón de ser del
dead-man: vive en el worker; el volumen hace que morir no pierda nada). El reaper
del hub queda sólo como último recurso. El dead-man estaba TRIPLEMENTE roto:
1. TOKEN EN EL FICHERO EQUIVOCADO: hay 2 caminos de creación con el token en
lugares distintos (farm-up → /etc/hammer-deadman.env; harkaq-vol → /root/
.hcloud-token). La service lee sólo el primero ⇒ un worker del otro camino
quedaba sin token, disparaba pero salía 1 "sin HCLOUD_TOKEN". Fix: deadman.sh
busca el token EN CADENA (volumen primero, que es lo más persistente).
2. REGEX DEL ID ROTO: la API devuelve JSON con espacio ("id": 126, no "id":126)
y el dead-man usaba '"id":[0-9]+' ⇒ NUNCA resolvía su id, aun con token
válido. Fix: '"id":[[:space:]]*[0-9]+'. (Doblemente roto: por eso NUNCA murió.)
3. FIRING ≠ CAN-DELETE: farm-up sólo verificaba que el timer dispara. Ahora
corre `deadman.sh --puede-borrar` (token en cadena + ve su id en la API) y si
NO puede matarse, DESTRUYE el worker ahí mismo. Un worker que no se autodestruye
es inadmisible ⇒ jamás debe existir.
gioser DOBLE-BLINDADO en TODOS los sitios de borrado (lista negra por nombre +
label role=hammer-worker), igual que farm-down ya tenía: reaper, farm-up-destroy,
deadman, y el helper de harkaq-vol. "Ahí está nuestra vida." Verificado vivo (104d).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sin esto un server borrado se quedaba en .fleet para siempre (describe falla ⇒
'sin label' ⇒ nunca se dropeaba). Ahora si no existe en hcloud, se saca.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
INCIDENTE (2026-07-23): un worker quedó 3.5h idle quemando € mientras el usuario
dormía. El dead-man del worker estaba "activo" (timer disparando cada 10 min)
pero FALLABA con exit 1 en cada tick: llegaba a matarse pero su
/etc/hammer-deadman.env no tenía token válido ⇒ salía 1 "sin HCLOUD_TOKEN" y
nunca se borraba. Verificaron que el timer DISPARA, no que puede BORRAR —
firing ≠ can-delete.
Causa de fondo: la garantía dependía de que CADA worker se auto-provisione bien
un token, y eso falla EN SILENCIO. Un invariante que depende de una provisión
frágil no es un invariante.
FIX en dos capas (defensa en profundidad):
1. REAPER HUB-SIDE (cosecha-cron.sh): el hub tiene un token que funciona y el
latido corre cada 30 min aunque no haya sesión (setsid). Un worker sin trabajo
activo REAPER_MAX=2 ciclos (~1h) se BORRA desde el hub, con el MISMO blindaje
de label role=hammer-worker (gioser jamás pasa). Cubre "dead-man del worker
roto". El dead-man del worker sigue cubriendo "hub caído".
2. farm-up verifica CAN-DELETE, no sólo firing: que el token del worker vea su
propio id en la API (lo mismo que hace al morir). Si no puede, LO GRITA al
provisionar en vez de descubrirlo 3.5h tarde.
Acción inmediata tomada: worker borrado a mano (label verificado), €0 baseline
restaurado — sólo queda gioser (protegido, fijo). Todo estaba cosechado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para
no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba
la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que
el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN
bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge
caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps
de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" →
kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado.
Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store
mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge
falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona.
En el laptop andaba por casualidad (store y su padre en el mismo fs).
`.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times.
cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo
expandiría a copias reales).
Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a
kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una
"sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de
timing sacó a la luz.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El peso que le faltaba a las ondas: sin él, critical-path = nº de pasos; con él,
ETA real en segundos.
INSTRUMENTACIÓN (worker):
scripts/farm/build-timed.sh envuelve `hammer build` midiendo la pared y la
registra en $STORE/.times/<hash>-<host>.json — un SIDECAR del store, NUNCA
dentro del artefacto. La duración es no determinista (varía por máquina/carga)
⇒ no puede tocar el ArtifactHash. Keyed por hash (CAS-safe: dos workers no se
pisan) + host (varias muestras por receta). Viaja al hub con el rsync de store
que ya hace la cosecha; ninguna herramienta del store lo confunde con artefacto.
Sólo builds REALES (pared ≥ UMBRAL 3s) — los cache-hits no envenenan la mediana.
campana-deuda.sh ahora construye vía build-timed.sh (transparente, mismo exit).
CONSUMO (hub): yupana._tiempos() carga name→mediana de segundos; keystones
computa el CAMINO CRÍTICO pesado = longest weighted path del subgrafo de deuda
(peso = segundos de build). La cadena más larga hay que construirla en SERIE
aunque haya ∞ workers ⇒ es la ETA con paralelismo infinito. Degrada con gracia:
sin datos, peso=1 y el camino crítico = nº de pasos (lo que ya daban las ondas).
Verificado: con muestras sintéticas da "ETA 19 min sobre 5 pasos, kio → kparts →
frameworkintegration → breeze → plasma-integration"; sin datos degrada a "5 pasos
(SIN datos)". store/.times está git-ignorado (metadata de máquina, no se commitea).
LÍMITE honesto (en la cabecera de build-timed.sh): `hammer build` arrastra deps ⇒
la pared incluye deps no selladas. En orden topológico (drenar) las deps ya están
selladas y la pared mide sobre todo ESTA receta — cota superior buena para pesar.
La precisión exacta pediría instrumentar el sellado dentro de hammer-build (Rust).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`build-farm.sh` usa `xargs -P$JOBS`; con >1, dos recetas que comparten dep disparan la carrera del
árbol de fuentes del ADR 0012 y el árbol queda roto para siempre. Se vio a escala: al invalidar
libdrm, 118 de las 205 recetas KDE pasaron de cache-hit a rebuild real y ~93 murieron con
"/src/.zwrap/cc is not a full path to an existing compiler tool" — el wrapper de zig que la receta
deja en el ÁRBOL DE FUENTE, barrido por el fetch concurrente de otra receta sobre el mismo qtbase.
El reintento serial de build-farm tampoco las recupera: el árbol ya quedó inconsistente.
La caché estaba ocultando la carrera, no evitándola. Serializar es la única mitigación correcta
hasta que el ADR 0012 elija salida.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
drenar.py parte la deuda en ONDAS topológicas (una receta sólo espera por deps
que también estén en deuda; las selladas ya están en el store). Onda 1 =
construible ya; dentro de cada onda, orden por `unblocks` desc.
Da lo que una lista plana no da: el orden correcto, la PROFUNDIDAD real de la
cadena (pasos secuenciales = el reloj de pared) y qué paraleliza sin pisarse.
base 0 en deuda imagen completa
cli 0 en deuda imagen completa
escritorio-kde 77 en deuda 12 ondas onda 1 = 1 receta: qtbase
escritorio-mirada 4 en deuda 2 ondas onda 1 = 3
HALLAZGO: el escritorio KDE está serializado detrás de UNA receta. qtbase
destraba 117 nodos y es lo único de la onda 1 — hasta que no esté sellada no hay
nada que paralelizar, por más workers que se enciendan. Cambia la pregunta de
"¿cuántas faltan?" (77, poco informativo) a "¿cuál es el camino crítico?".
Dos correcciones salidas de mirar la salida:
· el grafo se elige por la `cola` declarada en targets.toml, NO por cuál tiene
más nodos: incoming-kde SOMBREA recetas canónicas, así que medir
escritorio-mirada contra el grafo KDE medía una imagen que nadie construye
(3 en deuda en vez de 4). El mismo atajo estaba en seed-graph.py --frontera;
corregido en los dos.
· regla de la fuente única implementada en deps_de(): manda la receta si existe,
la semilla sólo si el nodo es `wanted`, marcando procedencia en la salida.
Enchufado al latido: cosecha-cron regenera docs/state/drenaje.json junto al
grafo y lo commitea ⇒ cola derivada y versionada, el git diff entre ciclos
muestra qué salió de la deuda. NO lanza builds: eso cuesta € y sigue siendo
decisión explícita (farm-up + campana-deuda.sh).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El `echo "ciclo: N recetas en $Q"` sale ANTES de pedir el lock, así que con la campaña corriendo el
journal mostraba el anuncio del ciclo y después nada: parece un loop trabajando y es un loop
bloqueado. Es la misma clase de problema que el "no encuentro el ejecutable zig" apuntando al
directorio equivocado — un log que miente cuesta horas de diagnóstico.
`flock -n` primero, y sólo si falla se anuncia la espera y se bloquea. Sin coste cuando el lock
está libre, que es el caso normal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los dos construyen sobre el MISMO work/, y `fetch` nombra el árbol de fuentes de forma determinista
(`work/sources/<receta>-<sha16>`, sin nada que dependa de QUIÉN construye) ⇒ dos procesos que
necesiten la misma DEP apuntan al mismo directorio: uno hace `remove_dir_all` mientras el otro
corre `tar -x`. El árbol queda a medias y devuelve "Directory not empty" (os error 39), y así se
queda hasta que alguien lo borra a mano.
No es teórico: la campaña de deuda y el loop pidieron `mesa` a la vez (el loop lo arrastraba desde
la cola KDE al invalidarse libdrm) y se llevó puesta media cascada GUI, con un error que no nombra
la causa. Casi borro ese árbol a mano antes de ver que había un bwrap montándolo.
Grano: la campaña toma el lock para toda su corrida; el loop, por ciclo de cola. Más fino rompería
el `xargs -P2` de build-farm.sh.
Dos honestidades en los comentarios, para no prometer de más:
- `flock` NO es FIFO. El loop vuelve a pedirlo enseguida y le gana a la campaña que espera:
medido, la campaña entra al terminar TODAS las colas, no entre dos. La ventana real es el
IDLE_SLEEP. Por eso espera con techo (LOCK_WAIT=7200) y sale limpia en vez de colgarse.
- Esto NO cierra la carrera del todo: el `-P2` interno del loop puede correr dos recetas de la
misma cola que compartan dep, y ésas se siguen pisando. El arreglo de fondo es un lock POR
ÁRBOL dentro de `fetch`, que cubriría los dos casos.
Verificado con flock real: exclusión mutua (la campaña entra justo cuando el loop suelta), salida
limpia por timeout con rc=0, y la no-equidad de flock medida en vez de supuesta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regresión introducida ayer al mover el store al volumen persistente (`ln -sfn /mnt/cosecha/store
$REMOTE/store`). El síntoma se leía como deuda de la cascada GUI y era otra cosa.
hammer deriva el lab del PADRE DEL STORE: `BuildConfig::defaults_for_store` hace
`project_root = store_root.parent()` y de ahí `.dev-fs/{alpine,tools/zig,cache}`. Con el store
symlinkeado, resuelve a /mnt/cosecha/store ⇒ busca /mnt/cosecha/.dev-fs, que no existe, y muere con
"no encuentro el ejecutable zig en …" — un mensaje que apunta al lugar equivocado: el zig 0.13.0
está, y está bien, sólo que en /opt/hammer/.dev-fs/tools/.
Golpea SÓLO a las recetas que fijan `zig_version` (20 en el corpus), porque son las únicas que
resuelven un zig hermano del por defecto. Por eso la campaña del 05:41 dio 26 selladas / 11
fallidas y las 11 eran exactamente ésas (tllist pango gtk4 libadwaita gtksourceview fcft
*-hello hammer-edit dwarves), y por eso la de las 19:48 —que era justo la lista de deuda— dio 0/14.
El bind-mount mantiene las DOS invariantes a la vez: los datos siguen en el volumen (sellar =
persistir, que es la razón del volumen) y `$REMOTE/store` vuelve a ser una ruta real bajo $REMOTE
⇒ el padre es /opt/hammer y .dev-fs se encuentra. Verificado en el worker vivo: con el bind-mount
y SIN overrides de entorno, libinput pasa la resolución de zig y falla por su dep real de Python.
Se persiste en fstab para que sobreviva al reboot, y el chequeo de verificación pasa de `test -L`
a `mountpoint -q`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El latido vivía sólo en el crontab y eso resultó frágil: este laptop arranca a veces con
`init=/usr/local/sbin/arje-zero` y a veces con OpenRC (KDE), y no hay systemd en ninguno de los
dos. `cronie` es un servicio OpenRC ⇒ en el arranque arje no existe y el latido no late. Peor: no
late EN SILENCIO. Se destapó tras un corte sucio del 2026-07-22, en el que además se vio el
runlevel `default` quedar a medias (cronie/metalog/acpid caídos, NetworkManager/dbus arriba) ⇒ ni
siquiera arrancando OpenRC era garantía.
El costo del síntoma es caro y callado: sin latido el hub no siembra, el worker agota la cola y se
queda idle quemando € sin moler.
`latido.sh` cuelga el latido de la SESIÓN: cualquier terminal, en cualquier arranque, asegura que
haya exactamente un latido vivo (`--ensure` desde ~/.zshrc, ~5ms y mudo si ya hay uno). Sin root,
sin unidad de servicio, sin mantener lo mismo por duplicado en dos inits. Contra asumido: no late
sin sesión abierta — es un laptop, no un server, y el primer ciclo dispara al instante de abrir la
terminal (el cron esperaba hasta 30 min al próximo tick).
El lock va DENTRO de cosecha-cron.sh, no en el llamador, para que valga venga de donde venga. Eso
además tapa un bug latente que ya existía: nada impedía que dos ciclos se solaparan haciendo rsync
sobre el mismo store y `git commit`+`push` a la vez. Con el lock, el crontab puede quedarse: bajo
KDE dispara cron, bajo arje dispara la sesión, y nunca se pisan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
En serie y no en paralelo: dos `hammer build` simultáneos compiten por el mismo work/sources y por
el watchdog de disco del worker-loop. El paralelismo vive DENTRO de cada build (-j$(nproc)).
En tandas y no en una lista plana: una tanda es una unidad topológica (la cadena GUI, el stack
wayland, los kernels). Cuando una se cae por su raíz —como pango arrastró a 7 recetas— se ve de un
vistazo en el resumen y se re-lanza sola; una lista de 40 nombres no dice nada al fallar.
Espera a que termine la campaña en vuelo, así se encola mientras otra muele.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`saldar-deuda-static.sh` calcula la deuda en vivo con `hammer hash --check` contra el store
LOCAL. En el worker ese store es PARCIAL (el del volumen) ⇒ mide 716 en vez de 37. Es la regla
de siempre con otra cara: el worker MIDE, el hub CLASIFICA. Acá el hub decide (DRY=1) y el
worker ejecuta una lista explícita, con las raíces del stack GUI primero (glib unblocks=14 →
harfbuzz/cairo/pango → gtk4 → libadwaita) para que la cascada caiga cuanto antes.
Incluye el PATH del `go` del store: `go mod vendor` corre host-side y una sesión SSH no
interactiva no carga /etc/profile ni cargo/env. Y HOME por defecto, que systemd-run no hereda
(con `set -u` era fatal).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El anclaje del store al volumen hacía `rm -rf $REMOTE/store` antes de symlinkear. Eso borra
justo el catálogo cacheado que es la razón de ser de la imagen golden ("arranca con TODO el
catálogo ⇒ cache-hit instantáneo"). Consecuencia medida hoy: el worker calculó 716 recetas de
deuda donde el hub medía 37, y se puso a reconstruir media distro (fallando, además, porque
sin `go` en el PATH host-side las recetas Go mueren en `go mod vendor`).
Fix: fusionar en vez de borrar. El store es CAS (nombre = hash) ⇒ `mv -n` al volumen es seguro
por construcción: no pisa lo que el volumen ya tiene, y lo horneado queda disponible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El crontab */30 no dispara cuando el laptop bootea en mirada (PID1=arje-zero, sin
crond). Envuelve el one-shot cosecha-cron.sh en un loop setsid — el mecanismo que sí
corre sin root y sobrevive al fin de sesion (no al reboot).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raíz de una noche de KDE atascada: la golden del 29-jun horneaba un hammer SIN el fix del
overlay lowerdir (e69eaaa, 12-jul) ⇒ toda receta de muchas deps (kio=52, kwin, plasma-*)
desbordaba el límite de 4KB de mount options y el sandbox ni arrancaba. 43 recetas 'en cola'
que en realidad reventaban al instante. El source llega fresco por farm-sync pero nadie
recompilaba. Ahora el worker-loop hace 'cargo build --release --bin hammer' al arrancar
(~24s cacheado). Binario del worker vivo ya rebuildeado a mano y verificado: kio cruza el
mount (funde 52 deps en una capa) y configura.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chequeo manual por si venís antes del cron o no corrió: salud del worker y qué muele,
barra de avance KDE desde el grafo, y últimas líneas del ciclo de cosecha. Filtra el
self-match del pgrep (el ']+.toml' que se colaba).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El worker efímero muele KDE 24/7; este cron en el laptop recoge su store sellado
(worker->laptop, CAS merge seguro), siembra recetas nuevas (laptop->worker) y regenera
el grafo de estado, commiteando SÓLO docs/state/*.json (el avance que el humano sigue).
NO promueve incoming-kde->canónico (decisión de clasificación humana). Robusto a
worker-ausente (dead-man ya lo mató por idle). KDE: 66/206 sellado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Las 7 recetas del stack gráfico (libdrm/mesa/meson/samurai/seatd/wayland/wayland-protocols) +
6 patches eran imports crudos de Alpine, redundantes: las 7 ya tienen receta CANÓNICA en recipes/
(mesa pineada a 24.0.9 iris-only A PROPÓSITO, no la 26.1.1 cruda con FIXME-sha256). Su trabajo
aterrizó por la vía canónica (7131cd4) hace 3 semanas; la cola quedó de cruft rompiendo cada ciclo.
fa45978 las sacó de QUEUES creyéndolas de 'otro agente' (5126a8b). Confirmado que NO hay otro
agente ⇒ borradas. recipes/incoming/ vuelve a ser cola de staging general y REGRESA a QUEUES; el
guard ls-vacío la salta si no hay nada. recipes/incoming/.deferred/ (24 recetas aparcadas con
diagnóstico, git con muro en libgit.a) NO se toca: el glob de QUEUES es top-level, no la muele.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El nuevo 'sin artefacto' del static-audit (deuda de rebuild ya no enmascarada) son 92 recetas
link=static cuyo hash VIGENTE no está sellado: el trabajo reciente (matar-gcc, static, harkaq)
cambió recetas base sin re-sellar, y cada cambio re-hashea en cascada todo lo que las declara.
Este script las salda. Calcula la deuda EN VIVO con 'hammer hash --check' (nunca una lista que
envejece), y construye cada receta NO-SELLADA. NO necesita orden topológico: 'hammer build' arrastra
sus deps recursivamente (construir curl construye openssl+perl primero); las cache-hit son instant.
Pensado para el WORKER (store completo, toolchain que no rompe el stack GUI): SKIP_GUI=1 por default
salta cairo/pango/gtk… que fallan en el laptop por zig-skew. Reparto medido: 76 C-base + 16 GUI.
Verificado end-to-end SIN quemar el laptop: modo DRY lista la deuda; y construí UNA receta diminuta
(which: NO-SELLADO → build 58s → SELLADO, hash vigente idéntico, estática de verdad en el audit).
El ciclo build+re-check del script es correcto. which quedó saldada de paso (deuda 76→75).
Confirma la decisión de usar la granja: 58s × 76 con openssl/gnupg/perl pesadas = 2-4h de laptop.
Cuando levantes la granja: farm-up, correr esto en el worker (SKIP_GUI=0 para incluir el GUI),
farm-down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El smoke de harvest-go.sh ejecutaba cada binario cosechado con el cwd en la RAÍZ DEL REPO, así que
un binario que escribe estado al arrancar dejaba basura entre las fuentes. Pasó: `gocron version`
—y `version` es justo el PRIMER flag que prueba el smoke, así que se disparaba siempre— sembró
config/{config.yaml,db.sqlite} (5 jobs de ejemplo + una sqlite) el 2026-07-04, y quedó sin trackear
hasta hoy. Medido, un flag a la vez, en cwd limpios:
[version] DEJO: ./config ./config/db.sqlite ./config/config.yaml
[--version] limpio [-v] limpio [--help] limpio [-h] limpio
Un `--help` no debería poder tocar el repo. El smoke ahora corre en un mktemp -d que se borra.
Vale para cualquier herramienta futura, no sólo gocron.
Sin regresión en el veredicto: en tmpdir `gocron version` panica (le falta web/index.html), el
smoke ya trata el panic (continue) y pasa a --version, que funciona ⇒ gocron sigue aprobando.
config/ borrado (no trackeado, sin una sola referencia en scripts/recetas/código).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recipes/incoming/ es la cola del stack gráfico tawasuyu, de otro agente. Sus 7 recetas son
imports crudos de Alpine con sha256='FIXME-sha256' y deps sin expandir (clang$_llvmver, _dev,
py3-gpep517) ⇒ NO construibles por construcción. El worker las fallaba en CADA vuelta (CPU
pagada) y farm-down repetía los 6 errores al bajar, con pinta de ser nuestros.
Su trabajo YA aterrizó por la vía canónica (7131cd4 'MESA iris-only CONSTRUIDA — stack gráfico
COMPLETO'), así que la cola quedó obsoleta. Pero NO es nuestra para borrarla: 370e7b7 ya la
parqueó una vez como cruft y 5126a8b tuvo que revertirlo. Dejamos sus ficheros en paz y sólo
los sacamos de QUEUES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Petición explícita del usuario: gioser.net es FIJO y NO TIENE BACKUP. Vive en el
MISMO proyecto hcloud que los workers, y Hetzner no da tokens por-recurso: el
token que el dead-man switch necesita para auto-borrarse puede borrar CUALQUIER
server del proyecto. farm-down era peor: borraba por NOMBRE leído de .fleet sin
verificar NADA — un nombre equivocado en esa lista y adiós.
Dos capas, porque una sola no basta cuando el fallo es irreversible:
1. LISTA NEGRA por nombre (gioser*) — explícita y legible.
2. LABEL role=hammer-worker — sólo se borra lo que NACIÓ de farm-up. Ésta es la
capa fuerte: no depende de mantener una lista al día. Un server que no es
worker no se borra, punto.
VERIFICADO contra los servers vivos, no en teoría:
gioser labels=map[] ⛔ PROTEGIDO
hworker-4 labels=map[role:hammer-worker] BORRABLE
Y .fleet sólo contiene hworker-4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos fallos de raíz, no uno:
1. HABÍA DOS CAMINOS de crear workers. harkaq-vol.sh ya tenía volumen Y
automuerte desde la 1ª campaña; farm-up.sh no tenía ninguna de las dos.
hworker-4 nació por farm-up ⇒ 5 días idle con 37G que nadie salvó. La
protección existía y el camino que usé la esquivaba. Ahora farm-up monta el
MISMO volumen (harkaq-cosecha) ⇒ collect/close cosechan y clausuran ambos.
2. NI harkaq-vol salvaba los artefactos: su rsync excluye /store y el volumen
sólo guardaba verdicts/ y logs/. Los 438 artefactos KDE se habrían perdido
igual.
FIX: el store del worker ES /mnt/cosecha/store (symlink desde /store).
Sellar YA es persistir: cada artefacto está en el volumen en el instante en que
se crea. "Guardar lo generado" deja de ser un paso que puede fallar antes de
morir y pasa a ser la estructura.
⇒ la automuerte puede ser INCONDICIONAL. Quitada la guarda "no borrar si hay
cosecha pendiente": era el bug de hworker-4 con otra cara — el worker se queda
VIVO justo cuando hay trabajo que salvar. Un switch que se desarma solo cuando
más falta hace no sirve. Antes de morir sólo sync+umount: no es guardar (ya está
guardado), es cerrar la puerta al salir.
El volumen sobrevive al server a propósito (~€0.044/GB/mes). El baseline €0 lo
da harkaq-vol.sh close, que es decisión del usuario, no de un timer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hworker-4 estuvo 5 DÍAS idle (carga 0.00, sin crontab, sin loops, 37G de KDE
sin cosechar) quemando dinero. El usuario había pedido esto explícitamente
—"los vps se autoapagan cuando se detectan idle por un tiempo"— y de sus 3
puntos (volumen / auto-apagado / cosecha) implementé el 1 y el 3. Éste faltaba.
EL FALLO ERA ESTRUCTURAL, no un olvido: farm-down.sh existe pero es MANUAL. El
modelo "efímero" dependía de que el agente se acordara de llamarlo — y si su
contexto se corta, o la sesión muere, el server queda vivo para siempre. Un
invariante que necesita que alguien recuerde NO es un invariante. Por eso el
switch vive EN EL WORKER: para apagarse no necesita ni al hub ni a mí.
BORRA, no apaga: en Hetzner un server apagado SIGUE COBRANDO (disco + IP). Un
poweroff daría sensación de ahorro y seguiría facturando. Precio: el token vive
en el worker (/etc/hammer-deadman.env 0600). Riesgo real y consciente; se acepta
porque la alternativa MEDIDA fue peor: 5 días de VPS idle.
Cuenta por INACTIVIDAD CONTINUA, no por antigüedad: cualquier señal de trabajo
(hammer build, worker-loop, heartbeat <30min) resetea los ticks. Guarda
/var/lib/hammer-no-borrar aborta el borrado si hay cosecha pendiente.
systemd timer y NO cron, por la lección medida: un cron "validado a mano" nunca
disparó porque el PID 1 de aquella máquina (arje-zero) no tenía crond. Validar
la LÍNEA no es validar que un demonio la ejecute. y el
farm-up comprueban que el timer quedó ACTIVO — evidencia, no fe.
Cableado en farm-up: todo worker NACE con el switch puesto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
¿Las 47 compiler=gcc que harkaq nombró son factibles de migrar a zig, o hay que
matracar? MEDIDO, no adivinado.
migra-zig.sh: para cada receta, build con zig-cc + SMOKE-TEST del binario —
porque el motivo del gcc era segfault en RUNTIME, no fallo de build; un artefacto
que sella pero segfaultea es peor que gcc. Idempotente, acumula por lotes (probar
47 no cabe en un timeout).
Muestra de 13 C-puras: 11 MIGRAN (bzip2 expat json-c gzip htop less libpng
libyaml zstd libffi xz), 2 MATRACA (pcre2: linker version script que zig ld no
soporta; file: subdir magic). ≈85% yield.
VEREDICTO: es GRANJEADA para las ~36 C-puras. El compiler=gcc era deuda histórica
— marcadas con zig 0.13/0.16 (que segfaultaban C clásicos), zig mejoró, el gcc
quedó por inercia. ~85% migran solas; el residuo (~15%) es matraca individual por
causas concretas del build-system.
Las 11 Rust con sys-crate C son otra evaluación (gcc para el C embebido de
libgit2-sys, no el Rust) — se deja para después.
Rollout = decisión del usuario (re-hashea recetas del índice firmado 748). Correr
migra-zig.sh sobre las 47 en la granja VPS da la lista final; promover las
migrables re-firma el índice. Reporte: tandas/needs-review-harkaq/migracion-zig.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test real (worker + volumen + helper), no en el papel. Las 3 piezas:
1. worker midió lz4+bzip2 → volumen
2. tras 1 ciclo idle SE AUTO-ELIMINÓ (verificado: el server desapareció solo)
3. volumen SOBREVIVIÓ con los 2 verdicts; helper efímero ccx13 los recogió al
hub y se auto-destruyó
3 bugs cazados EN EL TEST (no adivinados):
- falta --exclude /work: work/=69GB colgó el rsync del up horas
- cpx11 'unsupported' en hel1 (AMD sin stock) → ccx13
- collect llamaba harvest con 'local' (rsync a un worker inexistente): ahora
sólo recoge; clasificar es paso aparte (worker mide, hub clasifica)
+ auto-delete por API REST (curl+metadata), NO hcloud CLI (la golden no lo trae).
El ID propio sale del metadata service, no del nombre.
Cierra los 2 huecos de la 1ª campaña: idle cobrando (ahora auto-delete) y
dependencia de que yo esté viva (ahora el volumen persiste). Runbook:
docs/runbooks/harkaq-granja-volumen.md. Volumen clausurado tras el test (€0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La arquitectura que la 1ª campaña pidió a gritos (el worker quedó idle ~4h
cobrando). Tres piezas en harkaq-vol.sh {up|collect|close|status}:
1. VOLUMEN persistente (harkaq-cosecha): los workers escriben verdicts a
/mnt/cosecha, que sobrevive a su destrucción ⇒ NO dependo de estar viva para
no perder el trabajo.
2. AUTO-SHUTDOWN (harkaq-campana-vol.sh): tras IDLE_CICLOS ciclos con 0
mediciones nuevas, el worker SE AUTO-ELIMINA. En Hetzner un server apagado
sigue cobrando ⇒ apagar de verdad es delete. Vía API REST + curl + metadata
service (NO hcloud CLI: la golden Ubuntu no lo trae; curl siempre está). El
ID propio sale del metadata, no del nombre.
3. collect: monta el volumen en un helper barato efímero (o un worker vivo),
rsync al hub, corre el harvest, destruye el helper. close: detach+delete del
volumen (deja de cobrar, ~€0.44/mes mientras exista).
+ dos fixes de la 1ª campaña horneados en el bucle: borra el artefacto -hkm tras
medir (envenenaba la caché) y los dos gates (ABI>=7 + binario-con-jaula).
El token va al worker (para auto-delete): riesgo acotado — worker efímero, sin
servicios entrantes salvo SSH-por-clave, destruido al terminar.
Bug cazado antes de gastar: las llaves {..} dentro del mensaje de ${1:?...}
cerraban la expansión (CMD salía 'status}').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:coreutils elfutils expat file flex gawk
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:gettext-tiny giflib git gnupg gperf gzip htop iproute2 jq kbd libassuan libcap libevent libffi libgcrypt libgpg-error libksba libnl libpng libsass libsodium libssh2 libudev-zero libusb libwebp libxml2 libyaml linux-generic linux-headers linux-metal linux-pam linux lz4 mandoc mtools musl nano npth openssh openssl parted pciutils pcre2 pigz procps-ng python3 readline rsync samurai ca-certificates curl doas dosfstools e2fsprogs fontconfig freetype
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La golden hornea un target/release/hammer viejo y el rsync excluye /target ⇒ si
el cargo build del provisioning no corre, el worker construye alegremente SIN
JAULA y produce cero veredictos.
PASÓ: la primera corrida molió 2 recetas con un binario del 29/06 (0 ocurrencias
de 'harkaq' en strings) y las marcó 'sin veredicto' como si fuera culpa de las
recetas. Ocho horas así son la noche entera perdida, y el log habría dicho
'salteada' — nunca 'estoy ciego'. Es el falso Hermetico de D9 mudado al
orquestador: la ausencia como respuesta tranquilizadora.
Ahora aborta si strings no encuentra harkaq en el binario. Mismo criterio que el
gate del ABI: no medir es mejor que creer que se mide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El problema real no es el coste de la tanda: es que una pregunta a las 3am son 8
horas de idle. Estas 3 piezas están hechas para que la noche no se desperdicie.
harkaq-campana.sh (worker) bucle autónomo: construye cada receta bajo la
jaula y guarda los veredictos crudos. Idempotente, saltea
lo medido, duerme y re-escanea. NUNCA bloquea esperando: un
build que falla no detiene la cola (y su veredicto interesa
igual — es cuando más importa saber qué tocó fuera). Gatea
por ABI>=7: moler la noche para producir "no sabemos" es
peor que no moler.
harvest-harkaq.sh (hub, cron) hermano de harvest-go.sh: baja veredictos,
clasifica contra el store COMPLETO, aplica las deps
DECLARABLES y commitea con `git add` explícito. La deuda
IRREDUCIBLE NO se toca (declarar algo que el store no
provee rompe el build en vez de arreglarlo) → needs-review.
harkaq-add-deps.py editor de recetas conservador: idempotente, preserva las
deps existentes, y ANTE LA DUDA NO TOCA (valida que el
resultado siga parseando como TOML y no encoja). Corre de
noche sobre la fuente de verdad del catálogo: un fichero mal
editado a las 3am no da un error, da una receta corrupta que
nadie mira hasta el lunes.
POR QUÉ NO HAY REGEX EN EL CAMINO CRÍTICO: intenté sacar la lista de "a quién le
falta declarar make" con uno y falló dos veces en la misma tarde. `\bmake\b`
colaba `cargo-make` (el guión ES límite de palabra). La versión estrecha perdía
LAS CINCO recetas donde harkaq había medido la deuda de verdad (`compile = "make
…"`: el carácter previo es una comilla). El número bailó 69→45→99 según el
retoque. Construí una herramienta de medición y después intenté adivinar con un
regex — la lista buena la da el kernel. El regex queda sólo para ORDENAR la cola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El VPS va a ser el compilador permanente ⇒ harkaq TIENE que correr ahí o el
frente queda como decoración de laptop. Verificado de punta a punta en un worker
efímero desde la golden, no en el papel:
antes: kernel 6.8.0-134 → Landlock ABI 4 → sin audit → SinEvidencia siempre
después: kernel 6.17.0-40 → Landlock ABI 7 → AUDIT ✓
`apt install linux-image-generic-hwe-24.04` (archivo estándar de Ubuntu 24.04, sin
PPA ni cambiar distro), reboot, y la cadena COMPLETA de evidencia corre igual que
en el laptop:
same-exec 2 registros · new-exec 0 (el ciego) · new-exec-logon 4
domain=14e9f83c2 blockers=fs.read_file path="/etc/passwd" dev="sda1" ino=133259
El store del catálogo (103 artefactos) sobrevivió el bump intacto.
NUEVA GOLDEN: snapshot 408909310 "hammer-golden-harkaq-6.17-2026-07-15".
farm-up.sh pasa a usarla por defecto. La vieja (405120842) queda como fallback.
+ harkaq-uapi.h — EL HALLAZGO QUE IMPORTA para un compilador continuo: **el kernel
y los headers envejecen por separado**. El worker corre 6.17 pero su
linux-libc-dev es 6.8 y NO define NADA de lo necesario: ni los flags de log de ABI
7/8, ni AUDIT_LANDLOCK_ACCESS/DOMAIN (¡los tipos de registro!), ni IOCTL_DEV de
ABI 5. El kernel puede; el compilador no sabe pedírselo.
Es el mismo error de §3.1 al revés: allá, deducir el ABI de la versión del kernel;
acá, de la versión de los headers. NINGUNA de las dos dice la verdad — la única
fuente es el syscall en runtime. Estas constantes son números de contrato de UAPI,
estables por definición, seguros de fijar con #ifndef.
Sin esto harkaq sólo compila en distros con headers al día, que es justo lo que un
compilador continuo NO puede exigir. Y el modo de falla habría sido el peor: sin
AUDIT_LANDLOCK_ACCESS el lector filtraría por un número que no conoce y vería CERO
denegaciones — el falso `Hermetico` de D9, esta vez por headers viejos.
Nota: ABI 7 da el audit (lo que el proyecto necesita); TSYNC (ABI 8) pide 7.0 y no
está — es robustez opcional de D5, no un bloqueo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dalfox/templ/errcheck/ineffassign/unconvert (Go) + dprint (Rust) + kyverno (Go),
construidos en un worker efímero hcloud, cosechados y firmados. Binarios musl-static
verificados: dalfox 3.1.2, templ v0.3.1020, dprint 0.55.1, ineffassign, y los
analizadores Go corren. feroxbuster/jless/mdcat/mise quedan staged (frontera *-sys:
openssl-sys/xcb desde fuente).
Fix farm-down: el promote final ahora recorre TODAS las colas del worker
(incoming/incoming-go/incoming-clib), no sólo recipes/incoming — los artefactos de
las otras colas se cosechaban pero no se firmaban.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Los workers efímeros reciclan IPs de Hetzner. accept-new RECHAZA una clave de host
cambiada ⇒ el 'until ssh true' de farm-up colgaba para siempre en una IP reusada.
Como el modelo es hub-and-spoke (worker sin secretos, compute descartable) no hay
superficie MITM: saltamos verificación de host y no ensuciamos known_hosts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encadena farm-up + farm-down con detección de fin-de-ciclo por el marcador 'ciclo
terminado' del worker-loop (cuenta ocurrencias en el journal, robusto a skew de reloj
y builds largos). 'Subí la cola, corré esto, se apaga solo al terminar.'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migración del worker pet-24/7 a workers efímeros on-demand. farm1 (ccx23 persistente que
idleaba quemando plata) → snapshot golden 'hammer-golden-2026-07-05' (img 405120842: toolchain
+ store sellado horneados) → destruido. Baseline ahora 0 cajas / €0.
- farm-up.sh [N]: crea N workers desde el snapshot (cache-hit instantáneo del catálogo baked),
los alinea con la cola actual del laptop, registra la flota en scripts/farm/.fleet (gitignored).
- farm-down.sh [name...]: cosecha el store (CAS, merge seguro) al laptop y DESTRUYE; promueve+firma.
Orden seguro: sólo destruye si el pull de store salió bien.
- Validado punta a punta: up 1 → boot+cache+toolchain OK → down (cosecha+destruye) OK.
- Cron laptop harvest-go contra farm1 removido (la cosecha ahora la hace farm-down).
El worker sigue sin secretos (hub-and-spoke): compute puro y descartable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>