El archive de GitLab es NO-determinista (cada fetch = otro sha256, imposible de
pinnear). Cambio al release de cairographics.org (hash fijo 445ed820); strippea
boilerplate/ (sólo tests) ⇒ configure sed-ea subdir('boilerplate'). Fuente estable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>