El archive de GitLab recomprime al vuelo ⇒ el sha256 6d9281 (que el canónico
registró) ya no coincide; sirve d221d540. El canónico no lo nota (sellado, no
re-fetchea). Deuda: pinnear a un mirror estable. Por ahora, desbloquea.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cairo-shared murió 'Nonexistent build file boilerplate/meson.build': el release
de cairographics.org strippea boilerplate/; el canónico usa el archive de GitLab
(git tree completo) + el patch HAVE_CTIME_R. Alineo la fuente al canónico.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El link de gjs moría en cairo estático (undefined XML_GetCurrentLineNumber): el
cairo canónico --prefer-static arrastra fontconfig/freetype/pixman/expat como
cadena estática que un consumidor dinámico no resuelve. Fix = cairo DINÁMICO:
- cairo-shared (nuevo): libcairo.so.2 default_library=both, deps al cierre -shared.
- copio zlib/libpng/freetype/fontconfig -shared de incoming-kde (sufijo NO sombrea
el corpus ⇒ NO re-hashea la glib-isla). pixman canónico ya es dinámico.
- gjs usa el cierre -shared: linkea libcairo.so, la cadena resuelve por NEEDED.
Mismo cierre desbloqueará pango/gtk4 (próximo). Confirma la revelación del
comentario de pixman.toml: el muro 'cairo-ft FontConfig' era el cierre estático
no resuelto, no fontconfig faltante.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cairo es dep obligatoria (sin -Dcairo). El link de gjs sólo pone -lcairo, no la
cadena transitiva estática (fontconfig->expat, freetype->png/z, pixman) =>
undefined XML_GetCurrentLineNumber. Inyecto -lfontconfig,-lfreetype,-lpixman-1,
-lpng16,-lexpat,-lz (todas no-GObject: sin doble-estado, a diferencia de
--prefer-static con glib). Se limpia cuando cairo sea isla dinamica (#7).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Muro de link 'undefined symbol: XML_GetCurrentLineNumber' (expat): gjs dinámico
linkea cairo.a→fontconfig.a→expat pero meson no arrastra la cadena estática
transitiva. Es la deuda del cierre C (#7). Desactivo cairo para sellar YA el
hito (motor JS + typelib GjsPrivate bajo musl); los bindings cairo vuelven cuando
cairo sea isla dinámica.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gjs pasó meson setup + compile y murió generando GjsPrivate typelib con
'No module named distutils' — el mismo muro que graphene. py3-setuptools da el
shim. Olvidado en gjs (lo puse en las 4 islas GUI pero no acá).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gjs llegó lejos (glib/gobject/gio/cairo/mozjs-128 todos YES) y murió en el link
de readline: ncursesw/ncurses/termcap NO encontrados. ncurses ya sellado (widec).
Lo declaro ⇒ libncursesw.a hidratada, mantiene la consola JS interactiva.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avanzó tras el cierre .pc de glib; nuevo muro: mozjs-128.pc Requires nspr, no
declarado. nspr ya sellado en el volumen. Patrón .pc→deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
meson setup abortaba 'libpcre2-8 required by glib-2.0 not found': glib-2.0.pc y
cairo.pc traen Requires.private que hammer no hidrata transitivamente. Agrego
pcre2/zlib (glib) + freetype/fontconfig/pixman/libpng/expat (cairo). Patrón .pc→deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gjs pasó spidermonkey (sembrado, cache-hit) y murió en meson.build:89:
'-Bsymbolic-functions not supported'. El linker zig/lld no lo soporta.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primer ciclo del worker: los 5 targets fallaron, 3 muros distintos:
- graphene: compiló+linkeó .so/.a, murió SÓLO en g-ir-scanner con
'ModuleNotFoundError: distutils' (Python 3.12 lo removió). Faltaba declarar
py3-setuptools (el shim de setuptools), como ya hacían los keystones. FIX aquí.
- pango/gtk4: fallan en meson setup 'cairo-ft does not have required FontConfig
support' — la DEUDA DE ALCANCE cobrándose: sin --prefer-static, pkg-config no
resuelve las deps estáticas transitivas de cairo. --prefer-static NO sirve
(embebería glib en pango.so → doble GType → SIGSEGV del keystone). Fix real =
escalar el cierre C (cairo/fontconfig/freetype/harfbuzz) a islas dinámicas.
PENDIENTE (pase grande). py3-setuptools agregado igual (lo necesitarán luego).
- spidermonkey: 'No module named _ctypes' (python3 del rootfs worker sin _ctypes,
skew). No se reconstruye: se SIEMBRA la sellada del laptop (1.3G) ⇒ gjs cache-hit.
Este ciclo esperado: graphene sella + gjs sella (spidermonkey sembrado).
pango/gdk-pixbuf/gtk4 esperan el cierre C dinámico.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cola de build para el worker (patrón onda1): SÓLO los 5 targets listos
(gjs/graphene/pango/gdk-pixbuf/gtk4) + su cierre de deps que vive en
incoming-gnome/ (islas glib/glib-introspected/g-i/py3-setuptools + la cadena
spidermonkey/nspr/nasm/cbindgen). Todas las deps ya selladas ⇒ cache-hit; el
worker sólo mide los 5. Excluye mutter/gnome-shell/gdm (deps no listas) para
no quemar compute en builds condenados. Byte-idénticas al corpus/incoming-gnome
⇒ hashes idénticos (resolver sibling→parent auto-consistente).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoría Fase 1 (laptop, cero compute) de la onda 2 GNOME (gjs → gnome-shell).
Patrón isla dinámica (SHADOW del corpus estático, resolver hermano→padre):
link=dynamic, default_library=both (.a interno + .so p/ scanner/gjs),
introspection=enabled ⇒ producen su typelib. Todas hashean limpio.
- gjs: declara glib-introspected (su g-ir-scanner sobre GjsPrivate necesita los
.gir CORE; hidratación directa, no transitiva). Deps ya TODAS selladas ⇒ listo
para construir — primer nodo de onda 2, no necesita gtk4.
- graphene/pango/gdk-pixbuf: islas + Graphene-1.0/Pango-1.0/GdkPixbuf-2.0.typelib.
- gtk4: isla + Gtk-4.0/Gdk-4.0/Gsk-4.0.typelib. Revierte los hacks del mundo
estático (sed libgtk_dep, fat-archive). ITERATIVA: el scan arrastra el cierre
transitivo (wayland/mesa/libdrm/epoxy) aún estático — walls esperados en worker.
Deuda de alcance declarada: sub-deps C (cairo/harfbuzz/etc.) siguen corpus
estáticas embebidas; si duplican estado en runtime se escalan a isla puntual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El test definitivo del keystone: glib con -Dintrospection=enabled produce los
typelibs CORE (GLib/GObject/Gio/GModule/GioUnix/GLibUnix/GIRepository) usando
g-ir-scanner+compiler sobre GLib misma. SELLA ⇒ g-ir-compiler SÍ sirve para lo
core bajo musl; el SIGSEGV anterior era EXCLUSIVO de los .gir X11/cairo (que no
queremos, Wayland-only). La introspección funciona end-to-end ⇒ gjs→gnome-shell
destrabado.
Rompe el ciclo glib↔g-i: glib-introspected → g-i → glib(stage1, sin introspección),
sin ciclo. Clave: declara glib(stage1) en deps para hidratar libglib-2.0.so.0 y que
g-ir-scanner (que _giscanner.so dlopea) CORRA al configure (hammer hidrata deps
DIRECTAS, no transitivas — diagnosticado con una sonda musl).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El palo largo del stack GNOME, sellado. link=dynamic (consistente con la glib .so
de la isla, evita mezcla de estado GType) + build_introspection_data=false: g-i 1.84
sólo generaba aquí los typelibs EXTERNOS de X11/cairo (cairo/xlib/xft/xrandr/xfixes/
win32) — que NO queremos (Wayland-only) y sobre los que g-ir-compiler hace SIGSEGV
bajo musl. Los typelibs CORE (GLib/GObject/Gio) los produce glib (absorbió
girepository). g-i aquí = las TOOLS (g-ir-scanner/compiler) + libgirepository-1.0.so.
Cadena del muro (6 capas): py3-setuptools → wrapper -static/-shared → wrapper
--export-dynamic/.so → glib dinámica → glib default_library=both → introspection_data
off. El scanner (ejecuta código del target, el miedo real) CORRE bit a bit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
g-i (link=static) compila binarios internos que hacen `-static -lglib-2.0`; con la
glib sólo-shared, zig falla "unable to find static system library glib-2.0" (no hay
.a). La isla necesita AMBOS: el .so para que g-ir-scanner resuelva la introspección
(y gjs lo dlopee en runtime), el .a para el link estático interno de los consumidores.
default_library=shared → both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El fork elegido: isla dinámica para GNOME. Piedra angular = glib como .so real,
porque g-ir-scanner exige "shared libraries" (el typelib mapea a una lib que gjs
dlopea en runtime) y la corpus es sólo libglib-2.0.a.
Variante link=dynamic (-Ddefault_library=shared) en la cola GNOME. NO toca
recipes/glib.toml (estática, la consume KDE/corpus entero — cambiarla re-hashea
cientos). Por el orden del resolver (hermano→padre) SOMBREA la corpus SÓLO para
recetas GNOME; el resto del árbol intacto. Deps internas (libffi/pcre2/zlib) siguen
estáticas embebidas en el .so. introspection=disabled (los typelibs los produce g-i
externamente, evita el ciclo glib↔g-i).
g-i re-hashea a 8c3e5dd1 (dep glib static→dynamic); prueba de fuego en worker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El wrapper anterior (quita -static con -shared) destrabó la extensión Python de
g-i, pero g-i es dinámico en 3 puntos y el fallo saltó a los otros dos:
"error: using shared libraries requires dynamic linking" en (a) las tools que
linkean libgirepository-1.0.so y (b) el dumper del scanner con -Wl,--export-dynamic.
Generalizo la regla del wrapper: -static se quita ante cualquier marcador de link
dinámico — -shared, -rdynamic, --export-dynamic, o un shared object (.so/.so.N) en
la línea. Todos son casos donde -static es contradictorio de por sí. El link
estático normal (exe/.a sin marcadores) y el compile puro quedan byte-idénticos.
Verificado contra las 2 líneas reales que fallaban + 2 casos de no-regresión.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Las extensiones Python (python.extension_module → shared_module) son .so que el
intérprete del sandbox debe dlopen; intrínsecamente dinámicas. El harness inyecta
LDFLAGS=-static para link=static, y meson/autotools lo aplican a TODOS los targets
por igual ⇒ el .so sale ET_EXEC y dlopen da "Exec format error" (el muro de
gobject-introspection: _giscanner.cpython-312.so no cargaba).
Fix estructural (elegido sobre flip-a-dynamic): ensure_layout materializa en el
rootfs dos wrappers (hammer-zig-cc/-cxx sobre zig cc/c++) que QUITAN -static SÓLO
cuando el link lleva -shared. -static+-shared es contradictorio ⇒ correcto por
construcción: transparente (byte-idéntico) para todo link normal, arregla TODA
extensión Python futura, no sólo g-i.
Hash-safe: el ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒
no re-hashea ninguno de los 700+ sellados (cache-hit intacto); sólo cambia builds
nuevos con .so dinámicos. Verificado: rota positional params, preserva orden y
espacios; transparente sin -shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El worker molió la onda 1: gsettings-desktop-schemas SELLÓ (hash 4777231b
reproducido exacto). Los otros 2 fallaron con causas limpias:
- gobject-introspection: meson.build:29 exige `python3 (setuptools)` importable
(instala giscanner como paquete python); el python3 del rootfs no lo trae.
Fix: receta py3-setuptools (puro-python, copia a site-packages, sin pip ni red
— espejo de meson.toml) + declararla en [deps].build.
- gnome-desktop: sub-declaré gsettings-desktop-schemas (schemas_dep en meson:62);
ya sellada ⇒ añadida a [deps].build. Avanza el diagnóstico al siguiente muro
real (iso-codes / xkeyboard-config / libseccomp, frontera genuina de la receta).
Co-Authored-By: Claude Opus 4.8 <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>
Autoradas top-down feature-minimal en incoming-gnome (parte por subagentes, parte
a mano tras un stall de API): gobject-introspection, gjs, gsettings-desktop-schemas,
gnome-desktop, gnome-session, gnome-settings-daemon, mutter, gnome-shell, gdm,
xdg-desktop-portal-gnome. Las 10 parsean (hash dry-run). Versiones GNOME 48 salvo
gnome-desktop (44.5: upstream no corta estable de la lib compartida tras 44.5).
Con las raíces, el perfil escritorio-gnome CIERRA: 70 nodos, 60 YA sellados, 10 en
deuda (las raíces), en 3 ondas topológicas. Contra los 613-694 del cierre nixpkgs:
la sobreestimación 5-10x confirmada en vivo. La frontera GENUINA que aún no tiene
receta va documentada como comentarios FRONTERA en cada TOML (json-glib, libei,
colord, gcr, libsecret, polkit, ibus, at-spi2, accountsservice, linux-pam, upower,
geoclue, libnotify, iso-codes, xdg-desktop-portal, libseccomp; + gtk3/x11 que
g-settings-daemon 48 aún exige incondicional). Deuda de resolver: pipewire/pulseaudio/
alsa-lib/xkeyboard-config existen en incoming-kde pero resolve_dep_path no ve esa cola.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
drenar --perfil escritorio-gnome erraba 'no encontré un grafo (¿corriste
build-state.py?)' — mentira: el grafo GNOME existe, pero NINGUNA raíz del perfil
(mutter/gnome-shell/gdm) está autorada ⇒ 0 nodos etiquetados con el perfil, y la
comprobación por etiqueta en cargar() fallaba. Dos correcciones:
· cargar(): una cola NO-corpus identifica su grafo unívocamente ⇒ matchear por
cola; la etiqueta sólo desambigua dentro de corpus (perfiles solapados).
· main(): ámbito vacío con perfil ≠ 'imagen completa' — es 'SIN CIERRE todavía:
0 raíces autoradas', y apunta a autorar top-down.
KDE (162 sellados) y corpus (769/4-deuda) sin regresión. El yupana-gnome queda
armado en las 4 capas: objetivo, radio/grafo, keystones y drenar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El motor JS de Mozilla, keystone del frente GNOME (gjs→gnome-shell cuelgan de él),
sella b3:c48dbd10: libmozjs-128.so + libjs_static.a + shell js128 + mozjs-128.pc +
550 headers. Compiló el C++ entero a -O3 -j8 sin OOM en el hub (31GB/0 swap, al filo).
Seis capas iteradas desde el borrador:
1-2. FIX DE INFRA: el rootfs builder tenía clang pero NO el paquete (Alpine
separa los binutils LLVM del compilador). Sin llvm-ar/llvm-objdump/llvm-profdata
configure aborta. bootstrap-devfs.sh: +llvm22 en NEEDED + paso 3a-ter que
symlinkea la suite llvm-* a /usr/bin (Alpine la deja en /usr/lib/llvm22/bin sin
exponerla). Aplicado ya al rootfs real (.dev-fs/alpine).
3. triple: mozjs no traduce vendor ; 4. debe COINCIDIR con el host de rustc
(); fijado ese exacto.
5. venv de mach: cada fase es un bwrap con /tmp fresco ⇒ el venv no sobrevivía de
configure a compile. Anclado a /src/.mozbuild (persiste, es scratch).
No re-hashea artefactos (el rootfs no entra al hash). OJO: el worker golden también
necesita llvm22 (ya cubierto por bootstrap-devfs.sh para la próxima provisión).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cbindgen expone lib Y bin con el mismo nombre; el codegen del binario final
(cargo rustc -- -C target-feature=+crt-static -C relocation-model=static) es
ambiguo con dos targets ('extra arguments to rustc can only be passed to one
target'). flags=['--bin','cbindgen'] fija el target. Sella b3:e59e4193 en el hub.
Cierra la wave-1 de leaves GNOME: nasm b3:33383070, nspr b3:d7b71315, cbindgen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El daemon shim D-Bus de org.freedesktop.login1 ya está autorado y maduro en el
monorepo tawasuyu (arje-compat, un binario por servicio). Yo lo había dado por
inexistente grepeando SÓLO el repo hammer, no tawasuyu — error. seatd da asiento
(DRM/input), login1 da sesiones, y login1 = esto: arje-logind-compat implementa el
subset del Manager que GNOME/KDE consultan al boot (ListSessions/Seats/Users,
Inhibit, Can*/PowerOff/Reboot/Suspend, IdleHint) traduciendo al bus interno de arje,
fuera de PID1 y supervisado.
Receta HUB-ONLY (fuente gitea:22 ⇒ worker secretless da Connection refused). Pineada
a 06184e43 (rama selfhost/arje-zero-attest-lockfile): más nueva que la de arje-zero
porque trae el reorg arje-compat + el Cargo.lock del workspace en raíz ⇒ build
--locked reproducible. Sella dry-run: b3:410b6088. No hace falta C-ABI sd-login ni
PAM: la sesión se registra por el bus arje (mirada-greeter→fractal), no por pam_systemd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El keystone del frente. spidermonkey.toml es PRIMER BORRADOR — mozjs no sella a la
primera sobre musl (mach/configure no lo auto-detecta el lab ⇒ fases a mano, iterar
en worker ccx33+). Anclado al APKBUILD musl de Alpine: source del árbol firefox
128.14.0esr (sha real), clang, icu in-tree, nspr+zlib del sistema, shared-js.
Leaves que faltaban (ninguno estaba en el rootfs ni tenía receta):
nspr 4.36 (autotools), nasm 2.16.03 (autotools), cbindgen 0.27.0 (Cargo).
Las 4 parsean (hash dry-run OK) y pueblan el grafo: yupana radio nspr/cbindgen ya
ven a spidermonkey como dependiente. Capa grafo del yupana-gnome VIVA y reckonando.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build-state.py gana --gnome (análogo a --kde): carga recipes/incoming-gnome/ y sale
a build-state-gnome.json. drenar/yupana/seed-graph aprenden ese grafo (keystones,
drenar --perfil escritorio-gnome, radio de nodos incoming-gnome, frontera). Hoy la
cola está vacía ⇒ el grafo trae 0 nodos gnome; se poblará al aterrizar recetas y ahí
drenar/keystones reckonan solos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. targets.toml: [perfil.escritorio-gnome] (9 raíces de sesión, cola incoming-gnome)
⇒ `yupana objetivo` ya lo ve; fluye por targets.py. build-state lo SALTA hasta
que la cola se cargue (--gnome), así declararlo no reporta raíces fantasma.
2. seed-gnome.py OPTIMIZADO: hornea la triage (sustitución/tooling/opcional/espinazo)
como HIPÓTESIS con el caveat "el cierre de nixpkgs sobreestima ~5-10×", marca el
keystone (spidermonkey→gjs→gnome-shell), y deja de mentir con el 465 crudo. El
espinazo (353) se reporta como COTA ALTA, no como lista de build.
Falta (se activa al autorar): grafo build-state-gnome + flag --gnome ⇒ keystones/
drenar nativos sobre GNOME. Hoy la capa OBJETIVO del yupana-gnome está; la GRAFO no.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
seed-gnome.py corre el cierre transitivo de las raíces GNOME sobre metadata de
nixpkgs y clasifica conocida/frontera/nix-ismo/lab. Hallazgo: el cierre de nixpkgs
SOBREESTIMA ~5-10× la frontera de hammer (arrastra códecs ffmpeg, plugins gstreamer,
samba, openjdk, sphinx docs — todo opcional). El número real sólo emerge autorando
top-down feature-minimal.
Sólido: 155 deps ya tienen receta; palos largos = spidermonkey(mozjs)+gjs;
gobject-introspection es build-tooling. Cola aislada recipes/incoming-gnome/.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
La campaña de 40 nodos KDE selló 40/40, 0 fallidas: kio (desbloqueado por 6a0dc40)
arrastró la cascada y completó el escritorio. De 85/162 al abrir el catálogo a
162/162. 41 tiempos reales cosechados para el camino crítico pesado.