Commit Graph
204 Commits
Author SHA1 Message Date
sergioandClaude Opus 4.8 142c417d36 farm: que el loop DIGA que está esperando el lock, en vez de parecer que trabaja
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>
2026-07-22 17:21:24 -04:00
sergioandClaude Opus 4.8 1b436a717f farm: lock compartido entre campana-deuda y el worker-loop
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>
2026-07-22 17:06:42 -04:00
sergioandClaude Opus 4.8 ebd8e7f929 farm: anclar el store con bind-mount, no symlink — el ln -s rompía toda receta con zig_version
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>
2026-07-22 16:26:52 -04:00
sergioandClaude Opus 4.8 f3206d7520 latido: colgar el latido de la sesión, no del init (+ lock anti-solapamiento)
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>
2026-07-22 16:21:27 -04:00
sergioandClaude Opus 4.8 e09a48a69e farm: tandas.sh — encadenar campañas en orden, sin solaparse
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>
2026-07-21 22:51:11 -04:00
sergioandClaude Opus 4.8 a8bb83add5 why-differs-barrido: no llamar "no-reproducción" a lo que no está probado
El artefacto guarda su recipe.toml pero NO los hashes de sus deps, y el ArtifactHash sí los
incluye ⇒ dos sellados con el mismo recipe.toml pueden diferir LEGÍTIMAMENTE porque cambió una
dependencia. La primera versión gritaba "NO-REPRODUCCIÓN REAL" sobre 128 paquetes; eso era un
superconjunto, no una prueba.

Ahora separa por la CAUSA, que sí discrimina: si TODO lo que difiere son metadatos que un cambio
de dep no explica (MTIME de gzip, cabecera ar, ruta de build, secciones ELF informativas), es
no-determinismo probable; si difiere .text/.data o hay ficheros de más, es indistinguible de un
cambio de dep sin reconstruir. 128 → 92 probables + 36 no concluyentes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:42:32 -04:00
sergioandClaude Opus 4.8 895401b479 cierre #2 del SDD 17: hammer why-differs — el diffoscope propio
Cuando un artefacto no reproduce, el store sólo sabe decir "el hash no coincide" y el resto es
trabajo artesanal. Esto responde POR QUÉ, en términos de la CAUSA y no del byte:

- gzip con MTIME embebido (bytes 4..8)      → remedio: `gzip -n`
- cabecera `ar` de un `.a` (mtime/uid/gid)  → remedio: modo determinista (`ar D`)
- secciones ELF, con lectura experta: sólo `.comment` ⇒ otra versión de compilador; sólo
  `.debug_*` ⇒ rutas de build; sólo `.symtab`/`.dynsym` ⇒ orden de símbolos (código idéntico);
  sólo build-id ⇒ residuo, no causa raíz. En un `.a` dice QUÉ MIEMBRO difiere.
- ruta del árbol de build embebida, texto (línea que difiere), y bytes como último recurso.

Y sobre todo trae la EVIDENCIA, no sólo la hipótesis: para las secciones de texto extrae las
cadenas que están en un ELF y no en el otro. Caso real que lo motivó (alsa-lib): la
interpretación decía "típicamente rutas de build" y la evidencia mostró
`/src/target/release/build/libsodium-sys-<hash-cargo>/out/…`. Sin la cadena era una corazonada;
reproducir eso a mano cuesta varios readelf, la herramienta lo da en 40ms.

Descenso, no comparación total: sólo baja donde los hashes difieren (el cruce con format/
reconcile del SDD 17). Sin dependencias externas — parsers gzip/ar/ELF propios, como manda el
ADR 0004: un diffoscope de verdad se apoya en medio mundo de binarios ajenos.

`--json` para el bucle agéntico; exit 0 si reproduce, 1 si diverge (encadenable en scripts).
`scripts/why-differs-barrido.sh` lo pasa por todo el store y separa los dos casos que se
confunden a ojo: recipe.toml distinto (divergencia esperada) vs recipe.toml IDÉNTICO y artefacto
distinto (no-reproducción a investigar).

5 tests nuevos; los 142 de hammer-core siguen en verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:35:03 -04:00
sergioandClaude Opus 4.8 6b38b46293 farm: campana-deuda.sh — el HUB dicta la lista, el worker sólo muele
`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>
2026-07-21 22:10:31 -04:00
sergioandClaude Opus 4.8 c21602b707 farm: fix — farm-up tiraba el store HORNEADO de la golden (rm -rf) y el worker medía media distro como deuda
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>
2026-07-21 22:10:31 -04:00
sergioandClaude Opus 4.8 081fdd8a34 kde/metal: fix — el prompt nacía en el SERIAL; getty en tty1 (por eso el USB "no abría")
Síntoma: el USB de escritorio arranca, se ve el kernel, "arje-zero: despierta como PID 1",
el remount de la ext4 y el stack-depth de netup… y ahí la pantalla se congela para siempre.

Causa: el CMDLINE horneado es "console=tty0 console=ttyS0,115200 …" y el kernel hace
/dev/console = la ÚLTIMA console= ⇒ ttyS0. La seed card de la base metal supervisa un solo
getty, sobre `console` ⇒ en una laptop sin puerto serie el shell nace INVISIBLE. Los printk
sí van a las DOS consolas: por eso se ven los mensajes del kernel y después silencio. El
sistema nunca estuvo colgado — reproducido en OVMF: por el serial hay shell root (PID 98,
sshd arriba); en pantalla, nada. Nunca se vio antes porque toda validación fue -nographic,
donde el serial ES la pantalla (y el comentario de install-image-efi.sh afirmaba lo contrario:
"console=tty0 al final ⇒ el stdout va a la PANTALLA").

Fix en el script de imagen, no en el cmdline: un getty sobre tty1 es independiente del
cmdline (en metal siempre hay VT) y no obliga a recompilar/re-sellar el kernel (~60min).
Se conserva el getty de `console` para debug por serial.

- seed card del rootfs fundido += nodo tty1-getty (clona console-getty, argv → tty1)
- /usr/bin/console-login: muestra el motd y exec sh. Exporta PATH: arje-zero lanza el getty
  con envp VACÍO ⇒ sin él no se resolvía ni `cat` ni `plasma-start`.
- ambos escriben rompiendo el hardlink (os.replace / rm -f): $MERGED es cp -al de la base,
  escribir in-place mutaba work/metal-rootfs y toda imagen que comparta el inodo — ya había
  pasado con /etc/motd.

Validado en OVMF con pantalla (no -nographic): motd + prompt "/ #" visibles, y `ls /dev/dri`
tecleado por QMP responde card0 (teclado + PATH OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 21:32:06 -04:00
sergioandClaude Opus 4.8 0a3a00eb6e kde/metal: fix — la imagen dual usa linux-metal-dual (no linux-generic, que no bootea por EFI-stub)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:52:31 -04:00
sergioandClaude Opus 4.8 746d2dedd1 kde/metal: andamiaje escritorio dual-GPU (Intel i915 + NVIDIA nouveau) por software
Objetivo: USB "live" que arranca el escritorio KDE en metal y aguanta ambas máquinas
(TigerLake/iris y Pascal/nouveau), render por software (mesa-llvmpipe soberano) sobre
el KMS del kernel — uniforme en cualquier GPU, reusa los fixes validados en QEMU.

- recipes/linux-metal-dual.toml: = linux-generic (dual-GPU i915+nouveau+radeon) + el
  CMDLINE de pivote de linux-metal (initrd=/initramfs.cpio.gz rdinit=/init) para bootear
  por EFI-stub directo desde el ESP (root en disco, no RAM). Ni linux-generic (sin pivote)
  ni linux-metal (nouveau OFF) servían solos.
- scripts/kde/metal-desktop-image-dual.sh: arma la imagen — base metal + KDE + inyección
  mesa-llvmpipe/libLLVM/musl + firmware nvidia gp106 (nouveau modeset Pascal) + kernel
  linux-metal-dual.
- scripts/kde/plasma-start-metal-sw.sh: launcher software-GL para metal, detección de GPU
  real (i915/nouveau/amd), lanzamiento MANUAL (bringup seguro), con los fixes de la sesión
  QEMU (GBM_ALWAYS_SOFTWARE, USE_MODIFIERS=0, KWIN_COMPOSE=Q, FORCE_SW_CURSOR, cursor/iconos
  breeze, D-Bus sesión+sistema).

Pendiente: build del kernel (~60min, en curso) → rebuild imagen → validar OVMF → quemar USB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:32:46 -04:00
sergioandClaude Opus 4.8 44d50a76b5 kde/qemu: KWIN_FORCE_SW_CURSOR=1 — cursor visible y estable
El tema de cursor ya cargaba (sin "Failed to load cursor theme") pero el cursor
seguía apareciendo/desapareciendo: kwin usaba el plano de cursor por HARDWARE del
DRM, que en virtio-gpu con render software no se presenta bien. KWIN_FORCE_SW_CURSOR=1
⇒ kwin compone el cursor dentro del framebuffer principal (software) ⇒ visible y
estable. Verificado por screendump: la flecha breeze aparece compuesta en el scanout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:36:28 -04:00
sergioandClaude Opus 4.8 abf4e25c8f kde/qemu: tema de cursor breeze — mata "Failed to load cursor theme"
kwin buscaba el tema "default" (inexistente) ⇒ cursor invisible. El rootfs trae
breeze_cursors (115 cursores). Fix: XCURSOR_THEME=breeze_cursors + XCURSOR_PATH +
kcminputrc [Mouse] cursorTheme. El error desapareció del log (0 ocurrencias).

Estado final verificado por screendump: escritorio Plasma 6 completo (wallpaper +
panel con launcher/pager/bandeja + reloj), estable (139=0; el run previo aguantó
7.5 min a +450s con kwin=1 plasmashell=1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:30:32 -04:00
sergioandClaude Opus 4.8 d724db6556 kde/qemu: -vga none — el escritorio por fin SE VE (no más TianoCore congelado)
El escritorio booteaba y kwin pintaba por dentro (serial: kwin vivo), pero el scanout
REAL (verificado por `screendump` del monitor QEMU → PNG) seguía congelado en el logo
TianoCore. Causa: sin -vga none, QEMU añade una VGA estándar por defecto ADEMÁS del
virtio-gpu-pci. OVMF pinta su GOP en la VGA estándar ⇒ el kernel la envuelve con
simpledrm (card1) y ESE es el scanout que se muestra; kwin pinta en virtio-gpu (card0),
otro device que no se ve. Como son devices distintos, virtio no expulsa simpledrm ⇒
pantalla clavada en el frame EFI. (Con VARS reusados el timing lo tapaba; al resetear
los VARS quedó expuesto.)

Fix: -vga none ⇒ virtio-gpu-pci es el único display ⇒ OVMF pinta ahí ⇒ virtio-gpu-DRM
expulsa simpledrm (mismo device) ⇒ un solo card0, y el modeset de kwin toma la pantalla.
Verificado por screendump: se ve el wallpaper de Plasma 6 (1592x960), no TianoCore.

Herramienta nueva: -monitor unix socket en run-qemu-desktop.sh ⇒ `screendump` captura
el framebuffer REAL del guest (oráculo independiente de la ventana gtk, que puede
congelarse si mirada reinicia su Xwayland).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:22:07 -04:00
sergioandClaude Opus 4.8 82cbdb7e54 kde/qemu: KWIN_COMPOSE=Q (QPainter) — esquiva el segfault del rasterizador llvmpipe
El "pinta y vuelve a negro" NO era el output config: era kwin SEGFAULTEANDO (139) a
los segundos de pintar y respawneando. Core dump (core.llvmpipe-0, 176MB) + gdb del
host: el crash está en un hilo rasterizador de llvmpipe —
  #0 util_fill_rect  #1 util_fill_box  #2 lp_rast_clear_color  #3 rasterize_scene
  #4 thread_function (llvmpipe worker)
llvmpipe peta al limpiar el color buffer (bug de stride/geometría o vectorización del
fill en este entorno virtio+kms_swrast).

Fix: KWIN_COMPOSE=Q ⇒ kwin compone con QPainter (raster de Qt directo al buffer, SIN
armar escenas llvmpipe) ⇒ nunca entra a lp_rast_clear_color. (El segfault viejo con =Q
era el de gbm-init, ya resuelto por GBM_ALWAYS_SOFTWARE; llvmpipe sigue disponible para
gbm/EGL.) Verificado: QPainter compositing initialized, 139=0, kwin vivo a +15s/+30s.

+ STATUS monitor arreglado (busybox no tiene pgrep -c ⇒ pgrep|wc -l) con timestamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:09:38 -04:00
sergioandClaude Opus 4.8 2f99446ca1 kde/qemu: un solo output virtio (KWIN_DRM_DEVICES) — mata el negro-tras-pintar
Tras arreglar el FB (modifiers), el escritorio pintaba con plasmashell y volvía a
negro. Causa: kwin veía DOS outputs — virtio-gpu ("QEMU Monitor") Y el efifb/simpledrm
("Unknown-1", framebuffer EFI read-only que no fue expulsado al cargar virtio-gpu).
El conflicto entre ambos rompía el import/destroy de buffers EGL (spam
eglDestroyImageKHR EGL_BAD_PARAMETER) y tiraba la conexión de plasmashell
("error in client communication") ⇒ negro.

Fix: plasma-start detecta el card cuyo driver es virtio_gpu (robusto al numerado
card0/card1 que cambia entre boots) y lo pasa por KWIN_DRM_DEVICES ⇒ kwin ignora el
efifb. Resultado: un solo output, atomic modeset, y TODOS los errores correlacionados
con el negro a 0 (client-comm, eglDestroyImage, FB fail, output disabled). kwin carga
efectos normalmente.

+ monitor de estado cada 15s al serial (kwin/plasmashell vivos) para diagnóstico.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:01:16 -04:00
sergioandClaude Opus 4.8 6c0ef0867d kde/qemu: KWIN_DRM_USE_MODIFIERS=0 — mata la pantalla negra (FB rechazado por virtio)
Con el segfault ya resuelto, el escritorio salía NEGRO: kwin encontraba el conector
(1280x800) pero "Failed to create a framebuffer: Invalid argument" ⇒ "Failed to find
a working output layer" ⇒ sin scanout. Se veía el wallpaper un instante (el modeset
inicial usa un buffer dumb sin modifiers) y luego negro (los buffers del compositor
van por drmModeAddFB2WithModifiers).

virtio-gpu ADVIERTE el CAP de modifiers pero rechaza el modifier explícito (aún
LINEAR=0) con EINVAL. Fix: KWIN_DRM_USE_MODIFIERS=0 ⇒ kwin usa drmModeAddFB2 plano
(modifier implícito) que virtio sí acepta. "Failed to create a framebuffer" pasó de
2526 a 0; input Qt fluyendo; sin crash.

También: quitado KWIN_DRM_NO_AMS (era corazonada del segfault; atomic es el path
sólido en virtio) y arreglado el stream vivo del kwin.log al serial (busybox sed no
soporta -u ⇒ tail -f directo, sin sed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:52:59 -04:00
sergioandClaude Opus 4.8 6e5bc48c58 kde/qemu: system D-Bus — mata el spam udisks2 y completa el escritorio
plasmashell/solid spameaba "kf.solid.backends.udisks2: Not connected to D-Bus
server" en cada poll: sólo había bus de sesión, no de sistema. Ahora plasma-start
levanta dbus-daemon --system (socket estándar /run/dbus/system_bus_socket).

- qemu-desktop-image.sh: siembra el usuario messagebus:81 en passwd/group
  (system.conf hace setuid a él; el passwd base no lo traía). Rompe el hardlink
  read-only con `mv` de un temp, como el patch del seed.
- plasma-start-qemu.sh: arranca el system bus antes del de sesión + copia
  machine-id a /var/lib/dbus.

No hay udisksd/upowerd reales en el rootfs ⇒ sin discos/batería, pero el bus
existe y solid deja de errorear. Verificado: "Not connected" pasó de spam
continuo a 0; system bus ON; kwin+plasmashell+KSplash+kioworker vivos; 139=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:43:34 -04:00
sergioandClaude Opus 4.8 b53a822b42 kde/qemu: MURO 2 caído — GBM_ALWAYS_SOFTWARE=1 y el escritorio PINTA
El segfault-139 de kwin en QEMU NO era llvmpipe ni el composite: diagnóstico
por core dump + gdb del host (backtrace #0 0x0 ← dri2_initialize_drm ← eglInitialize
← EglDisplay::create ← DrmBackend::initialize; call *0x30(vtable swkmsMesaCoreExtension)
= queryCompatibleRenderOnlyDeviceFd = NULL en el driver software).

virtio-gpu-pci sin 3D expone un nodo KMS-only (no render-capable). En mesa 24.0.9
gbm sólo pone software=true vía dri_screen_create_sw, al que sólo se llega con
GBM_ALWAYS_SOFTWARE o si dri_screen_create falla. Con MESA_LOADER_DRIVER_OVERRIDE
kms_swrast, dri_screen_create tiene éxito ⇒ software=false ⇒ dri2_initialize_drm
llama el vtable NULL ⇒ SIGSEGV. Fix: GBM_ALWAYS_SOFTWARE=1 en plasma-start.

- plasma-start-qemu.sh: + GBM_ALWAYS_SOFTWARE=1 (el fix); + andamiaje de diagnóstico
  gated por DIAG=1 (logging kwin/mesa/egl verboso, stream vivo del kwin.log al serial,
  core dumps a /core + sync, sleep 3600 para congelar el serial).
- run-qemu-desktop.sh (nuevo): run reproducible (DISP=none/gtk, TIMEOUT, VARS OVMF rw).
- qemu-desktop-image.sh: restaurado bit +x.

Verificado: headless (139=0, sin queryCompatible) y ventana gtk en DISPLAY=:0 —
kwin_wayland + plasmashell + KSplash + kioworker vivos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:30:45 -04:00
sergioandClaude Opus 4.8 1c6b4ed54b kde/mesa: cadena llvmpipe soberana — LLVM18+mesa-llvmpipe from source, GBM cruzado
mesa-llvmpipe con gcc/libstdc++ (matchea el ABI de libLLVM; zig-cc usaba libc++ ⇒
undefined symbols en el JIT). qemu-desktop-image inyecta libLLVM.18+libgcc_s+libstdc++.
HITO: kwin toma DRM master de virtio-gpu, crea el GBM device (llvmpipe, ya no softpipe),
expone compositor y ACEPTA clientes. Muro restante: kwin segfaultea (139) al componer la
1ra superficie con llvmpipe — necesita backtrace (gdb) para diagnosticar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:15:21 -04:00
sergioandClaude Opus 4.8 dafd97be79 kde: pipeline escritorio QEMU (qemu-desktop-image + plasma-start-qemu)
Imagen QEMU booteable con Plasma auto-lanzado: merge base+kde-metal-rootfs, inyección
del loader musl (la imagen metal nunca pudo correr KDE dinámico — base estática + KDE
sin libc), parche swrast, seed patcheado (console-getty ejecuta plasma-start), PATH/dbus/
fuentes/iconos. HITO: kwin toma DRM master de virtio-gpu y expone wayland-0 (lo que el
anidado no podía). Muro pendiente: GBM/EGL de software — mesa es softpipe, falta llvmpipe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 14:21:37 -04:00
sergioandClaude Opus 4.8 7964e45bfa kde: nested-app siembra kdeglobals + activa KDEPlasmaPlatformTheme (iconos+Breeze)
Sin kdeglobals Qt caía al tema de iconos hicolor (vacío) ⇒ dolphin pelado. Ahora
siembra [Icons]Theme=breeze + QT_QPA_PLATFORMTHEME=kde ⇒ aplica iconos + estilo
Breeze + colores. El motor SVG (libqsvgicon/libqsvg/libQt6Svg) ya estaba hidratado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 22:03:48 -04:00
sergioandClaude Opus 4.8 4d418cd07c kde: nested-plasma.sh — sesión Plasma COMPLETA anidada en mirada (kwin+plasmashell)
kwin_wayland como compositor anidado en Xwayland :0 (backend X11; el wayland-nested
segfaultea en gbm/renderD128) + compositado por SOFTWARE (llvmpipe/swrast) + QtQuick
software backend (sin él plasmashell no crea contexto GL/EGL). plasmashell carga la
corona org.kde.plasma.desktop y renderiza. Cliente wayland, no DRM master ⇒ seguro
sobre la sesión viva. Verificado los 3 procesos vivos + Desktop.qml cargado.

Requirió hidratar al rootfs: kactivitymanagerd (daemon aparte de plasma-activities),
mesa-swrast, y GRAFT del módulo qml org.kde.kwindowsystem (ver deuda en recipe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:28:18 -04:00
sergioandClaude Opus 4.8 f78fdeef94 kde: QT_QPA_FONTDIR + SHELL en nested-app — mata los tofu
qtbase se construyó SIN fontconfig (libQt6Gui no enlaza libfontconfig) ⇒ Qt usa su
motor básico que busca fuentes en /usr/lib/fonts (vacío) → glyphs tofu. Atajo sin
rebuild: QT_QPA_FONTDIR=/usr/share/fonts/dejavu hace que el motor básico cargue los
ttf directo. SHELL=/bin/sh da shell a konsole (zsh no está en el rootfs). Fix 'correcto'
(qtbase con FEATURE_fontconfig) = rebuild que re-hashea el escritorio → deuda para la granja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:13:12 -04:00
sergioandClaude Opus 4.8 b2d80e6f69 kde: lanzador anidado (nested-app.sh) — apps KDE hidratadas como ventana en mirada
Corre una app KDE del rootfs hidratado como cliente wayland ANIDADO en el compositor
mirada vivo (WAYLAND_DISPLAY=/run/mirada-wayland), vía bwrap overlay (alpine+kde-rootfs
como /). Cliente, no master: renderiza por renderD128/iris, cero riesgo al display.
Resuelve dbus de sesión (root-en-userns + machine-id + /run/dbus). konsole y dolphin
verificados corriendo en vivo. Fix portabilidad: run-plasma-headless.sh += --setenv PATH.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:04:16 -04:00
sergioandClaude Opus 4.8 50d51bb4a8 granja: cosecha-heartbeat.sh — latido de cosecha bajo mirada (arje-zero sin crond)
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>
2026-07-18 11:52:16 -04:00
sergioandClaude Opus 4.8 19a3cda8ca granja: el worker recompila hammer al arrancar (evita el fósil de la golden)
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>
2026-07-18 05:07:13 -04:00
sergioandClaude Opus 4.8 20666643e2 granja: estado-granja.sh — foto on-demand (worker+avance KDE+cron) sin esperar el latido
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>
2026-07-17 21:55:44 -04:00
sergioandClaude Opus 4.8 2a52d7804f granja: cron infinito de cosecha+siembra (recoger/sembrar cada 30min) — latido sin tokens
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>
2026-07-17 20:12:09 -04:00
sergioandClaude Opus 4.8 4357cd7b0f estado: buscar el artefacto por el name INTERNO, no el basename del fichero
El grafo buscaba store/<hash>-<basename> pero el artefacto se sella por el name INTERNO de la receta
(qtbase.toml → name=qt6-qtbase → store/<hash>-qt6-qtbase). Las qt6-* KDE salían 'debt' aunque
estuvieran selladas. Fix: nodo lleva iname=d['name']; state_of busca por iname. Las aristas de deps
siguen por basename (así resuelve resolve_dep_path). qtbase ahora sale sealed correctamente.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:32:14 -04:00
sergioandClaude Opus 4.8 ff79dc5e28 estado+kde: el grafo ahora cubre la cola incoming-kde; base KDE desbloqueada en el laptop
MOTIVO: 'estábamos haciendo kde'. La cola recipes/incoming-kde/ (206 recetas) era punto ciego del
grafo (sólo miraba recipes/ top-level). Modo --kde: build-state.py/build-state-view.py cubren la cola
KDE (sombreando canónicas homónimas sibling-first, como el sandbox) → build-state-kde.{json,html}.

CAUSA de la deriva (193/206 en deuda): fui YO — esta sesión re-sellé pkgconf y samurai (arreglos
static), y 194 recetas KDE declaran pkgconf, 142 samurai ⇒ re-hash en cascada de todo el árbol. El
sistema determinista funcionando: cambia un input base, cascan los dependientes. Deuda legítima.

DESBLOQUEO (el 'GUI rompe en el laptop' era sólo cairo/pango, NO la base): construida en el laptop
toda la base KDE guiado por el orden de ataque del grafo — extra-cmake-modules, util-macros,
xorgproto, xcb-proto, wayland, libdrm, dbus, icu4c, util-linux, fontconfig, pixman, glib, freetype,
harfbuzz, fribidi, libXau/libXdmcp, la cadena X11 (xtrans/libxcb/libX11/libXext/libXrender/libXfixes/
libXi), la familia xcb-util, libxkbcommon, wayland-protocols, mesa (24.0.9 iris-only, sin LLVM), y
los -shared. KDE 9→39 selladas. qtbase (↑117, la raíz) quedó DESBLOQUEADO y compilando.

DISCO: el build llenó el disco (100%). Liberados 82G borrando work/sources (57G, se re-extrae solo)
y .farm-harvest (26G, verificado 100% redundante en el store, CAS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:27:55 -04:00
sergioandClaude Opus 4.8 a267cf4f48 estado: clasificación Rust corregida (había ~180 Rust escondidas como C) + musl
Señal fuerte para Rust: la receta copia su binario de target/release/ en las fases. Medido 225
recetas, 0 con dep go ⇒ sin falsos positivos con Go (findutils entra bien: es uutils/findutils, find
en Rust). LÍMITE: un crate BuildSys::Cargo PURO sin fase install propia (zellij) no deja rastro en el
TOML y cae a 'c' — sólo se ve bajando la fuente, que el generador no hace.

Efecto: clases c 322→141, rust 45→226 (la mayoría eran Rust auto sin fase cargo explícita). Y la
LECTURA de la deuda cambia: la deuda C REAL es sólo 8 (casi toda saldada esta sesión), no 74. Lo que
queda es Rust (15, CLI pesados → granja), GUI (29 → granja), Go (5 → worker), kernel (4).

musl sellado (base). Avance: sealed 703→704, debt 53→52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 12:36:23 -04:00
sergioandClaude Opus 4.8 a30e0d3146 estado: orden de ataque por impacto de desbloqueo (lectura accionable del DAG)
El grafo ahora computa, sobre las dependencias, dos campos por receta:
  blocked_by — deps que están en deuda (lo que impide construirla ya)
  unblocks   — cuántas recetas EN DEUDA la declaran como dep (su impacto de desbloqueo)

Una receta en deuda sin blocked_by es construible YA; ordenadas por unblocks desc, dan el orden que
libera el grafo más rápido. La vista muestra la pastilla ↑N en las filas listas y ordena por ahí.

Top de impacto (todo el stack GUI concentrado, más zstd/perl de C-base):
  glib ↑14  wayland ↑13  pixman ↑12  libdrm ↑11  fontconfig ↑11  zstd ↑10  libxkbcommon ↑9  perl ↑8

Es la guía para cuando se levante la granja: construir esas primero desbloquea el grueso.

De paso, diagnóstico de las 8 'never' (ninguna es cruft ni bug): dwarves BLOQUEADA-documentada
(necesita libdw, elfutils da sólo libelf a propósito — sub-proyecto elfutils-libdw); llimphi-counter
es un EJEMPLO/plantilla intencional; las otras 6 son imports Go/Rust que van al worker. Y confirmado
parseando: 0 recetas con FIXME real en el campo sha256 (el grep decía 43, todas comentarios) — otra
vez grep miente, el grafo (parsea) dice la verdad.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:48:28 -04:00
sergioandClaude Opus 4.8 e19fb6347f estado: vista humana del grafo (build-state-view.py → build-state.html)
Dashboard self-contained (sin recursos externos, tema claro/oscuro) que rinde build-state.json para
VER y SEGUIR: barra de avance del corpus, tiles por estado, reparto de deuda por clase, y los huecos
en ORDEN DE ATAQUE. Ese último es el uso accionable del grafo: cada receta en deuda muestra sus deps
coloreadas por estado; una fila 'lista' tiene todas sus deps al día (construible ya), una 'espera N'
depende de N recetas también en deuda. Foto actual: 117 en deuda, 71 listas ahora.

Publicado como Artifact para verlo en el navegador; el HTML también queda en el repo (abrir con
file://). Regenerar ambos: scripts/build-state.py && scripts/build-state-view.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:34:02 -04:00
sergioandClaude Opus 4.8 7f89a635a4 estado: grafo de build persistido y versionado (scripts/build-state.py → docs/state/build-state.json)
El estado real del build vivía disperso —en mi cabeza, en docs que envejecen (matar-gcc decía 47,
eran 16), en el store (que guarda TODOS los sellados históricos, no el vigente)— y cada medición a
mano mentía distinto. Este generador lo deriva de la ÚNICA fuente de verdad (recetas + hammer hash
+ store) a un JSON firme. El git diff de ese fichero ES el avance entre dos corridas: qué se saldó,
qué se rompió, qué cambió de estado.

NODO = receta {name, class, link, compiler, deps[], hash, state}:
  sealed  — el artefacto del hash VIGENTE está en el store (al día)
  debt    — hay sellados históricos pero ninguno vigente (cambió, falta rebuild)
  never   — sin ningún sellado
  unhashable — hammer hash falló (hueco real)
ARISTA = dep de build. El grafo CIERRA (0 deps huérfanas) y topo-ordena sin ciclos.

Foto inicial (764 recetas): sealed 647 | debt 109 | never 8. Deuda por clase: c=74 go=5 gui=29
kernel=4 rust=5. Clases: go 362, c 322, rust 45, gui 30, kernel 5.

Validado contra lo que sé de esta sesión: samurai/which sealed, curl/openssl debt, helix sealed
(rust/gcc), naabu sealed (dynamic/go), mesa debt (gui), linux debt (kernel). Todos correctos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:30:41 -04:00
sergioandClaude Opus 4.8 fa8dff08b1 granja: borrar la cola gráfica obsoleta de incoming/ (no había otro agente)
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>
2026-07-17 11:26:42 -04:00
sergioandClaude Opus 4.8 54b2cd69ce granja: saldar-deuda-static.sh — reconstruye la deuda que destapó hammer hash
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>
2026-07-17 11:18:54 -04:00
sergioandClaude Opus 4.8 1e823d377b hammer hash: dry-run que faltaba — cierra el sobre-reporte del static-audit de raíz
El agujero que dejé señalado 3 veces esta sesión: nada podía saber el sellado VIGENTE de una receta
sin construirla, así que static-audit.sh auditaba el más reciente por mtime (ls -dt) y acusaba a
recetas ya sanas (dbus/libnl) por un sellado anterior a sus flags.

FIX = subcomando `hammer hash <receta> [--check]`. Calcula el ArtifactHash puro sobre las recetas
(source_id + compiler/target/link + patches + flags + fases + hashes de deps recursivos) SIN bajar
fuentes ni compilar. artifact_hash() ya era pub; el CLI sólo lo expone. Cero cambios en la lógica
de hashing ⇒ NINGÚN sellado se mueve.

Verificado: `hash` da EXACTAMENTE el mismo hash que `build` (samurai, byte a byte); `--check` sobre
receta editada → NO-SELLADO en 2ms, exit 1, sin construir nada.

static-audit.sh ahora selecciona el artefacto VIGENTE por hash, no el más reciente por mtime. Con
fallback a ls -dt si hammer no está compilado. Efecto en el store completo: las ~65 recetas cuyo
sellado no es el vigente pasan de 'auditadas' (falsa cobertura) a 'sin artefacto' (deuda de rebuild
REAL, ahora visible): estáticas de verdad 615 | MIENTEN 0 | sin artefacto 123. Corre en 15s, sin
build. El '58 sin medir' de antes estaba enmascarando ~65 recetas más.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:08:29 -04:00
sergioandClaude Opus 4.8 117f8e6dee granja: el smoke-test del cron escribía en el repo (gocron sembró config/)
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>
2026-07-17 09:39:49 -04:00
sergioandClaude Opus 4.8 fa459784e4 granja: dejar de moler la cola del OTRO agente (los 6 'No such file' de cada farm-down)
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>
2026-07-17 09:22:01 -04:00
sergioandClaude Opus 4.8 c0a473589b granja: BLINDAJE de gioser — el token de la granja podía borrarlo
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>
2026-07-17 09:06:17 -04:00
sergioandClaude Opus 4.8 949a1d657a granja: automuerte TOTAL + el store VIVE en el volumen (sin ventana de pérdida)
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>
2026-07-17 09:01:41 -04:00
sergioandClaude Opus 4.8 0d35044b53 granja: DEAD-MAN SWITCH — el worker se borra solo tras 1h idle
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>
2026-07-17 08:00:24 -04:00
sergioandClaude Opus 4.8 372baed4f6 static-audit: el '28 mienten' estaba INFLADO por mi propio bug (ls|head -1)
El audit tomaba 'ls -d store/*-<n> | head -1' = orden ALFABÉTICO POR HASH, no el
artefacto vigente. El store guarda TODOS los sellados de una receta (expat tenía
5, de junio a hoy), así que auditaba uno de hace tres semanas. Mismo bug de
clase que comparar libpng.a con libpng16.a por find|head -1.

Con 'ls -dt' (más reciente): 28 → 11. Tres de las supuestas mentirosas
(cargo-audit, git-absorb, yazi) nunca lo fueron: su artefacto reciente ya era
estático y el audit leía el viejo.

Lo que NO cambia: el hallazgo central es real y verificado a mano. libtool
ignora el -static del lab, el curl sellado NO arrancaba en el host, y de las 14
que arreglé cada una se verificó contra sus artefactos previos (expat: los 4
viejos con NEEDED=1, el nuevo con 0). Estaban rotas de verdad.

HONESTIDAD escrita en el script: 'más reciente' ≠ 'vigente'. Lo vigente sería el
artefacto cuyo hash corresponde a la receta de HOY, y hammer no lo expone sin
construir (no hay hammer hash / dry-run). Por eso el audit va DESPUÉS del
rebuild, nunca antes.

Quedan 11: file, helix, libcap, libgcrypt, pcre2, pkgconf, samurai, tuc,
util-linux, xz + naabu (nuevo: Go con libc.so.6 de GLIBC, otro caso).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 05:01:40 -04:00
sergioandClaude Fable 5 9395fbfc5c piloto trace END-TO-END: build real de zlib trazado en store desechable — el hash REPRODUCE bit a bit el sellado (b3:2623a403), poda filosa (make 1/8 ficheros, busybox 1/2), taxonomía de ruido medida (+/cache al normalizador); harkaq-trace-build.sh orquesta (tracer por fase vía /proc/pid/root)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 04:08:08 -04:00
sergioandClaude Opus 4.8 c707c1e13f static-audit: link="static" era mentira en 28 recetas selladas (libtool lo ignora)
Destapado migrando procps-ng. libtool lee el -static del lab como "preferí mis
.a", NO como flag al linker: el binario sale dinámico y la receta jura que es
estático. Con gcc pasaba igual — no es regresión de zig, es un agujero que
nadie había medido.

Tres consecuencias medidas, no teóricas:

1. NO CORREN. El curl sellado en el host: "Error relocating /lib/libz.so.1:
   __snprintf_chk: symbol not found". Su NEEDED libc.so es el soname de la musl
   de zig; en el host /lib/libc.so son 255B de linker script. El artefacto sólo
   funciona dentro del sandbox.
2. ARRASTRAN GCC. helix, yazi, git-absorb, cargo-audit y tuc traen libgcc_s.so.1
   de Alpine DENTRO del binario. Deuda de matar-gcc que ningún compiler="gcc"
   declara: invisible para el frente entero hasta ahora.
3. Un NEEDED es una entrada no declarada — lo que harkaq mide en build, pero en
   RUNTIME. Rompe el cono de affected.py (SDD 17 §4): un CVE en zlib no
   alcanzaría a un curl que se declara estático.

Script, no gate duro: fallar hoy rompe 28 selladas de golpe, varias del sistema
base (curl, util-linux, sudo). Primero se arreglan con evidencia, después se
cierra la puerta. Exit 1 mientras haya mentirosos ⇒ sirve de gate en CI cuando
la lista llegue a cero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:12:59 -04:00
sergioandClaude Fable 5 91496007ca harkaq-trace: la traza POSITIVA de la clausura (plan-freebsd T1.1-T1.2, prior art filemon/META MODE)
- harkaq-trace.c: fanotify FAN_MARK_FILESYSTEM — agnóstico de mount-ns, el
  tracer en el HOST ve los open() de dentro del bwrap (ptrace lo deniega el
  seccomp D4; audit sólo emite denegaciones). Crudo y tonto a propósito
  (reparto Q1c); D9: sin fanotify ⇒ exit 3 SinEvidencia, jamás traza vacía.
- harkaq-trace-norm.py: canoniza (drop /proc /sys /dev /tmp /out /src, dedupe,
  orden estable — la lección nº1 de META MODE) + atribución por dep con
  --resumen = la señal de PODA (T1.4): dep declarada con 0 toques = grasa.
- VALIDADO: modo --fs como root (worker, 5s, sin tocar colas): capturó
  exactamente zlib.h + 3×libz.a de un cat/head; laptop sin privilegio ⇒
  SinEvidencia honesto. Piloto completo (traza de un hammer build real +
  resumen de poda) pendiente de un rebuild natural en el worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:51:17 -04:00
sergioandClaude Opus 4.8 83d40af35c harkaq: el clasificador nunca propone runtime base como declarable (fix de raíz de busybox)
El bug que declaró `busybox` en 29 recetas y rompió binutils: harkaq-suggest veía
`/bin/busybox` denegado, encontraba `recipes/busybox.toml` en el store y decía
"declarar dep: busybox" — sin mirar que la política YA lo concede (`ro /bin/busybox`:
el sandbox corre `sh -c` y /bin/sh→busybox ⇒ es CONTRATO, no dep).

Fix: harkaq-suggest ahora lee los paths CONCEDIDOS de la política (`ro`/`rw`/`list`),
no sólo los `# expect`. Si un path está concedido ⇒ categoría RUNTIME BASE: no se
declara, aunque el store lo provea.

Las 4 clasificaciones verificadas tras el cambio:
  /bin/busybox              → RUNTIME BASE (ya concedido; NO declarar)   ← el fix
  /usr/bin/make             → DECLARABLE (declarar dep: make)
  /usr/lib/libstdc++.so     → IRREDUCIBLE (nadie lo provee)
  /usr/bin/gcc en broot     → DEUDA DE COMPILADOR (compiler=gcc)
  /usr/bin/gcc en zlib      → sin deuda (sonda esperada; zlib compila con zig)

Con esto puesto, busybox JAMÁS se habría declarado. Cierra el TODO que dejó el
revert: la campaña automática ya no puede cimentar contrato como dependencia.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:27:57 -04:00
sergioandClaude Opus 4.8 d99ed58850 harkaq: revertir busybox (era runtime base, rompía builds) + coordinar el §3 con Fable 5
DOS ERRORES MÍOS, uno de dirección y otro técnico, los dos en la cosecha automática:

1. DIRECCIÓN: declaró `busybox` como dep en 29 recetas — cuando la Etapa C lo está
   ELIMINANDO (USERLAND_COMPONENTS=[uutils,findutils,…], ya cerrada; joyas-reusables
   §5 lo confirma: "uutils… Ubuntu 25.10 los envía como default"). Estaba cimentando
   la deuda que el roadmap borra.
2. TÉCNICO: busybox YA está en el runtime base de harkaq (`ro /bin/busybox`, `ro
   /bin/sh` — el sandbox corre `sh -c` y /bin/sh→/bin/busybox). Es CONTRATO, no dep.
   Declararlo apila el busybox de hammer sobre el de Alpine y ROMPE el build:
   binutils daba "cannot run C compiled programs" en las DOS máquinas. Verificado:
   sin busybox declarado, binutils construye (b3:f4507dcd…). Y la cadena se explica:
   binutils roto ⇒ zlib (que lo declara) tampoco construía.

Auditoría de la cosecha (diff real, no la línea completa del +): añadió sólo 5 deps
distintas — make ×73, busybox ×29, perl ×8, pkgconf ×5, binutils ×1. Sólo busybox
estaba mal; las otras 4 son deps reales medidas. busybox revertido de 30 recetas
(queda sólo en busybox.toml, pre-existente).

Es el mismo error que ya me habían señalado con otro disfraz: MEDIR BIEN Y ACCIONAR
MAL. harkaq midió correcto (el build toca /bin/busybox: es el shell); la acción
correcta no era declararlo sino reconocerlo como contrato.

+ COORDINACIÓN del §3 con Fable 5 (mismo diseño, tareas repartidas):
  - Su lección casper queda CONFIRMADA y REFORZADA: la clausura de build no sólo le
    FALTAN las clases del mundo (offline) — también le SOBRA casi todo (headers, gcc).
    Medido: htop (estático, 0 NEEDED) no toca NADA al correr ⇒ política = su binario.
  - Su "la clase viaja como campo de la ConcesionCapacidad, sin formato nuevo" se
    cumple LITERALMENTE: lo firmado es format::Permisos = u32 bitmask en 36 bytes
    canónicos (Ring 0) ⇒ las clases SON los bits. La cripto no se toca.
  - Diseño unificado: frontera (clases, u32, declaradas) + detalle (paths, Landlock,
    medidos). D3 rige en ambos.
  - Reparto: clases→Fable 5; medición/harness→Opus. CONTACTO: runtime-policy.sh ahora
    emite la CLASE detectada (/etc/resolv.conf→dns, /etc/ssl/certs→tls-certs, …), no
    sólo el path: la medición alimenta la tabla, la tabla decide el bit.
  - Consumidor esperando: plan-jaula-juegos F1 (Steam que no puede leer ~/.ssh).

+ juez.sh (§10): nombre único por corrida (con uno fijo pega en caché ⇒ falso
  "sin evidencia"). Fue el juez quien destapó todo esto en su primera corrida real.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:14:19 -04:00