Commit Graph
92 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 6558b7c8de granja: la cosecha llenaba el disco — rsync sin -H y bajando .dmerge
La bajada del store murio con "No space left on device" con 95 G libres y un
store remoto de 25 G. Dos causas que se multiplican:

- `rsync -a` NO preserva hardlinks (eso es `-H`). El store es CAS y .dmerge
  hardlinkea los artefactos, asi que los 25 G que mide `du` en el origen -que
  deduplica por inodo- se escriben en destino como copias enteras, una por enlace.
- `.dmerge` es cache pura: hammer la reconstruye sola, no aporta un artefacto y
  es la parte mas pesada del arbol (132 G en el hub, 111,7 GiB exclusivos).

Ademas el fallo a mitad deja directorios creados sin ficheros, que es un
cache-hit envenenado: `mise` se cosecho asi, como nombre vacio. Se anade un
guardian que barre los vacios al terminar la bajada y avisa de re-correr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-09-01 15:53:35 +00:00
SergioandClaude Opus 5 33e02bdfbc poda de fuentes: 70 G recuperados y un vigia para que no vuelvan
El volumen del store estaba al 97% (8,8 G libres) con la granja escribiendo ahi, y el gordo
NO era el store: eran 79 G de `work/sources` — 106 arboles con el `target/` de cargo y los
`.o` dentro, porque el arbol de fuentes es tambien el directorio de compilacion. `pixi` pesaba
3,5 G y siete apps cosmic pasaban de 3 G cada una.

Nadie podaba eso: store-gc mira el store, la poda de CI mira la cache de CI, y este tercer
monton crecia sin dueno desde que el store se mudo al volumen.

Borrarlo no cuesta nada, y eso es lo que lo hace seguro: los dos caminos de fetch.rs
(fetch_git:73 y fetch_tarball:234) hacen `remove_dir_all` del arbol y lo re-extraen en CADA
build, sin una sola rama que reutilice. Un arbol viejo no acelera nada; la re-extraccion ocurre
igual. Lo unico que se pierde es el post-mortem del ultimo build, de ahi el suelo de 24 h.

El script toma `work/.farm-build.lock` porque el unico dano posible es borrar el arbol que un
bwrap esta compilando (ADR 0012 en su forma mas directa), y el mtime del directorio raiz no
distingue "viejo" de "build lento". Con espera acotada: si hay algo en vuelo salta el ciclo.

Suelo en HORAS y no en dias por la leccion de cache-ci-no-envejece: aquel cron podaba a +10
dias sobre datos de horas y liberaba cero. Los 106 arboles abarcaban 3 dias.

Sin umbral por espacio libre: podar solo cerca del borde convierte una poda barata y plana en
un pico justo cuando un build puede quedarse sin disco a mitad.

Probado en las cuatro ramas: dry-run vacio, dry-run con 15 candidatos, --aplicar sobre un arbol
sintetico (borra el viejo, deja los 15 recientes) y lock tomado (salta y sale 0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
2026-08-31 19:56:42 +00:00
SergioandClaude Opus 5 5fd34b07c1 cosecha-cron: cablear el audit de enlace estatico, con puerta diaria
Tercer cable de la misma familia que el vigia de fuentes y el contrato del
kernel, y por la misma razon exacta: el gate existia desde julio, nadie lo
re-corria, y hoy el «MIENTEN: 0» con el que se habia cerrado el frente resulto
ser 4 — todas selladas DESPUES del cierre. Un frente que se cierra en cero y no
se vuelve a medir no se queda en cero.

Lo que costaba ese silencio, medido hoy: `bash` —el shell de los perfiles base,
cli y dos escritorios— NO ARRANCABA fuera del sandbox, y las cuatro apps GTK4
del corpus (hammer-edit incluida) segfalteaban en gtk_init. Todo sellado y en
verde.

PUERTA DIARIA por sello (`work/.static-audit.sello`, gitignoreado): el barrido
lee cada ejecutable de los ~750 sellados con file+readelf y tarda ~8 min de CPU
en 4 vCPU compartidos con el worker. Cada 30 min seria mas de un cuarto de core
para siempre vigilando algo que solo cambia al re-sellar. Con la puerta, 8 min
al dia. Forzar: `rm work/.static-audit.sello`.

El sello es LIMITADOR DE RITMO, no marca de exito ⇒ se toca en cuanto el barrido
termina, salga como salga. Si solo se tocara al acertar, un fallo que consuma
los 8 min se repetiria cada 30 y el cable pasaria de vigilancia a sangria.

No toma work/.farm-build.lock a proposito (solo LEE el store; retenerlo 8 min
pararia la granja) y va con `nice -n 19`: la prioridad la tiene construir.

El veredicto lleva FECHA DE MEDICION en la primera linea, y no es decoracion:
con el frente en verde el texto del audit es constante, `git diff --cached
--quiet` no veria cambio, no se commitearia nada, y en tres meses el fichero
seria indistinguible de uno rancio. Con la fecha, cada barrido deja huella en el
git log: se ve que el vigia sigue VIVO, no solo que el ultimo veredicto fue
bueno. Misma trampa que el build-state-wlr congelado 17 dias.

Comprobado en las dos direcciones, que es donde estos cables fallan callados:
la puerta abre sin sello / a las 25 h / a los 3 dias y cierra a las 0 h y 23 h;
y con un audit que sale !=0 sin imprimir nada, NO pisa el veredicto anterior,
toca el sello igual y no deja temporales. Ya corrio en el cron real: la cosecha
de 17:01Z commiteo docs/state/static-audit.txt y logueo «sello de hoy, no toca».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
2026-08-31 17:09:43 +00:00
SergioandClaude Opus 5 588fedebcb granja: freno al crashloop del worker — 30 h de OOM y reinicio en bucle
`Restart=always` + `RestartSec=30` sin límite: si una receta no entra en la RAM de la
caja, el OOM killer la mata, systemd relanza a los 30 s, el loop recorre la cola por glob
alfabético y vuelve a la MISMA receta. En el LXC dev.gioser.net eso duró 30 horas con
`clang18`, y dejó la máquina con presión de I/O `full avg300=43` —todo bloqueado en disco
casi la mitad del tiempo— y sshd incapaz de completar el banner. Diagnosticarla desde
fuera era imposible: parecía red o disco. El único rastro eran dos `oom-kill` en el
journal, que sólo se ven desde dentro.

StartLimitBurst=3 / StartLimitIntervalSec=3600: a los 3 arranques en una hora systemd se
rinde y deja la unidad en `failed`, VISIBLE en `systemctl status`. OOMPolicy=stop: un OOM
no es un fallo transitorio, si no entra ahora no entra en 30 segundos.

Rendirse no pierde nada: el hub ya tolera workers ausentes —cosecha-cron dice "siembra
falló" y sigue con el siguiente— y un worker parado y diagnosticable vale más que uno que
se reinicia para siempre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-08-31 08:10:52 +00:00
SergioandClaude Opus 5 19b5d51dfe latido: el contrato del kernel se comprueba solo en cada cosecha
El gate de SDD 25 §4.ter existía y NADIE lo corría. Un kernel reconstruido sin MEMCG volvería
a pasar inadvertido — y ese fallo no se ve del lado del desarrollo, porque el kernel de la
máquina de trabajo sí trae MEMCG: sólo se ve comprobando el artefacto.

Mismo cable que el vigía de fuentes y por la misma razón que está escrita ahí arriba: lo que
nadie refresca envejece hacia el optimismo. Ahora cada ciclo deja el veredicto en
docs/state/kernel-contract.txt y el git log lo muestra como el resto del khipu.

El fichero se escribe por temporal y sólo se mueve si tiene contenido: contract sale != 0
cuando el contrato no se cumple —y ese rojo es justo lo que hay que guardar—, pero un fichero
vacío se leería como «nada que objetar».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
2026-08-30 11:03:41 +00:00
SergioandClaude Opus 5 dcf80a17a6 vps-setup: patch faltaba en la rama Fedora — tumbaba el stack GUI de GNOME
En Debian `patch` entra de regalo dentro de `build-essential`; al desglosar la rama dnf a
gcc/g++/make sueltos se cayó sin que nadie lo notara. El campo `patches = [...]` de una
receta lo aplica hammer DEL LADO DEL HOST, fuera del sandbox, así que no alcanza con que
el rootfs lo traiga —lo trae, `/usr/bin/patch` está en el alpine de los dos labs— y por
eso el diagnóstico "rootfs laptop ≠ worker" no aplicaba acá.

Medido en dev.gioser.net: `cairo-shared` moría con `spawn patch: No such file or
directory` y detrás caían `pango` y `gtk4`. El stack GUI entero de GNOME por un binario
de 129 KB ausente en el host.

Regla que queda escrita: toda herramienta que hammer invoque HOST-SIDE va en esta lista,
no en `[deps]` de la receta — declararla en la receta re-hashearía cairo y todo GNOME
debajo, para arreglar algo que no es de la receta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-08-29 19:00:18 +00:00
SergioandClaude Opus 5 b3cc18018c granja: incoming-cosmic e incoming-wlr faltaban en QUEUES del worker
Es el mismo fósil que el propio script documenta para GNOME, repetido con las colas
que nacieron después. Las 25 recetas en deuda de COSMIC no fallaban: nadie las
intentaba, porque su cola no estaba en la lista. Una cola ausente no da error, da
silencio — el mismo modo de fallo que "el corpus no está en las QUEUES del worker".

Se añade la regla en el comentario: cola nueva y línea QUEUES, en el mismo commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-08-29 18:33:35 +00:00
SergioandClaude Opus 5 0b1b4b0506 granja: el comentario de farm-up nombraba una golden que el código ya no usa
La cabecera decía 'def 408909310' y la línea 38 usa 417847948 desde el 2026-08-08.
La 408909310 es justo la que NO traía rust ni go — la que dejaba fuera al 76% del
catálogo — así que el comentario apuntaba a la imagen rota.

Un comentario que nombra un id concreto es una referencia, no prosa: si alguien lo
lee y re-apunta IMAGE a mano, revive el bug que el propio fichero documenta debajo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-08-28 22:56:19 +00:00
SergioandClaude Opus 5 853853d919 granja: vps-setup soporta Fedora/LXC además de Debian/Ubuntu
Un LXC prestado (dev.gioser.net, Fedora 44, 6 núcleos, 196 G) pasa a ser el worker
mientras esté disponible; el camino Debian queda intacto para seguir abriendo
workers efímeros en Hetzner.

Cambios:
- familia detectada por gestor de paquetes (apt | dnf5/dnf) con sus equivalencias
  (build-essential -> gcc/g++/make, libssl-dev -> openssl-devel, uidmap ->
  shadow-utils, xz-utils -> xz).
- el veredicto de userns deja de ser "escribí el sysctl" y pasa a ser "unshare -Umr
  funciona de verdad": en un contenedor el sysctl es read-only y no hace falta,
  porque el userns lo concede el host. Si no se puede crear, aborta: sin eso la caja
  no puede ser worker.
- swapon degrada a AVISO RUIDOSO en vez de abortar. En LXC devuelve Operation not
  permitted (medido) y no hay forma desde dentro; el swap lo da el host. Se dice
  explícito que sin swap el techo de RAM es duro y el OOM killer no avisa antes.
- documenta los dos pasos que una caja virgen necesita del hub y que la golden
  escondía: instalar rsync a mano, y que el HUB EMPUJE el lab ya verificado en vez
  de que el worker lo baje del Storage Box (el worker no debe tener secretos).

Validado provisionando dev.gioser.net de cero: hammer compila, el lab extrae y
verifica por sha256, y el toolchain queda "idéntico al lock (108 paquetes)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-08-28 21:12:39 +00:00
SergioandClaude Opus 5 45b95f78b9 ADR 0013: mirror de fuentes — la URL es transporte, el sha256 es la identidad
`rsync` (404 de samba.org) y `musl` (musl.libc.org no responde) no se pueden construir hoy, y no por
culpa nuestra. Es el estado estacionario: una distro que construye TODO desde fuente tiene tantos
puntos de fallo como fuentes, y son servidores de terceros que nadie nos prometió mantener.

LA MEDIDA, peor de lo que parecía. Sobre 1167 fuentes (561 tarball + 606 git, 79 hosts):
github.com sostiene 742 — el 64% del corpus depende de UN host. Doce hosts sostienen el 89%. Y 43
hosts sostienen exactamente UNA receta cada uno: ahí es donde muerde el bit-rot lento.

El vigía, en su primera corrida: 8 URLs muertas de 1167. Tres de ellas —busybox, freetype,
freetype-shared— están SELLADAS Y EN USO: son el shell y las fuentes del escritorio que se capturó
hoy. Se salvan sólo porque el tarball sigue en la caché local de esta máquina.

LA URL NUNCA FUE LA IDENTIDAD, y el código ya lo sabía: `hash_inputs` usa `tarball:{sha256}` /
`git:{commit}` y el `..` descarta la URL; la caché se nombra `{sha256}.tar` con un comentario que
dice literalmente que cambiar de mirror no la invalida. Añadir mirrors NO re-hashea NADA. Faltaba el
mecanismo, no el diseño.

Orden: caché local → mirror propio → upstream. El mirror va ANTES, no como rescate: el sha256 se
verifica igual, así que no hay diferencia de contenido posible, y un mirror que sólo se usa cuando
upstream falla es un mirror que nadie prueba — se descubre roto el día que hace falta.

LO QUE HAY QUE HACER BIEN. Un mirror que sirve calladamente lo que upstream perdió convierte un
fallo ruidoso en silencio. Por eso construir y vigilar van SEPARADOS: `hammer build` nunca avisa
(sería ruido en 561 recetas), y `fuentes-vigia.sh` pide cabeceras, escribe
docs/state/fuentes-vigia.json y lo corre el latido. Sin ese contrapeso las URLs se mueren una a una
y el corpus queda irreconstruible con todo en verde — el mismo modo de fallo que dejó el grafo de
wlr 17 días anunciando un 121/121 falso.

 PROHIBIDO cambiar el sha256 para "arreglar" una URL muerta. Es la tentación natural ante un 404 y
no arregla una descarga: cambia lo que la distro construye. Otro sha256 es otro contenido, y la
receta seguiría diciendo `rsync 3.4.4` mientras construye otra cosa.

VERIFICADO DE PUNTA A PUNTA. El primer intento —construir busybox con el mirror puesto— dijo BUILD
OK y NO PROBÓ NADA: cache-hit del artefacto, cero bytes descargados. La prueba válida usa una receta
efímera con un sha256 que sí está en el mirror y una URL que ni resuelve por DNS. Sin HAMMER_MIRROR:
`curl (6) Could not resolve host`. Con él: sella, con el contenido real.

Mirror poblado: 126 objetos en el Storage Box que ya se paga. `cargo test -p hammer-build`: 5/5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
2026-08-26 18:57:40 +00:00
SergioandClaude Opus 5 64e1ad2942 khipu: el grafo de sway llevaba 17 días congelado — el latido nunca corría --wlr
`build-state.py --wlr` existe desde que se abrió el frente, y su propio comentario dice que sin él
el frente es INVISIBLE para el khipu. La línea nunca se agregó a `cosecha-cron.sh`: el latido
regeneraba {base,kde,gnome,cosmic} y saltaba wlr.

Medido: build-state-wlr.json quedó fijo el 2026-08-09 anunciando `escritorio-sway 121/121`. Hoy,
regenerado, da 108/123. En el medio se re-hashearon freetype, make, python3 y libpng-pic, que
arrastraron a deuda a 14 dependientes (fuzzel, yambar, swaybg, swaylock, slurp, sed, strace, tzdata,
rsync, pciutils, procs, sd, skim, tokei). Nadie lo vio porque el número que se mira estaba perfecto.

Un grafo que nadie regenera no envejece en cualquier dirección: envejece hacia el OPTIMISMO. Sólo
puede sobreestimar lo sellado, porque el paso del tiempo únicamente invalida hashes, nunca los crea.

Va también `foot` a las raíces de escritorio-sway. El comentario de targets.toml afirmaba que entraba
por la clausura de `cli`; el grafo lo desmiente — `perfil.cli` no lo lista y ningún perfil lo
arrastraba. El escritorio daba 121/121 SIN EMULADOR DE TERMINAL. Se vio al hidratar la clausura para
armar la imagen, no antes: la métrica mide la clausura de las raíces DECLARADAS, y es estructuralmente
ciega a lo que falta en la declaración.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
2026-08-26 16:02:04 +00:00
SergioandClaude Opus 5 5cfebf0087 granja: el aviso de "sin volumen" ahora CORTA en vez de sembrar igual
La noche del 2026-08-22 la campana corrio sharded en dos workers. Un volumen de
Hetzner se adjunta a UN server, y este script crea N workers en un bucle
apuntando todos al MISMO VOL_NAME: hworker-1 tomo harkaq-cosecha y para
hworker-2 no quedaba nada que adjuntar. Construyo el shard 1/2 entero en su
disco raiz y el dead-man se lo llevo a las 03:27Z. 21 G, 397 artefactos
sellados; 584 de sus 675 no existen en ningun otro sitio (work/perdidos-hworker2.txt).

Lo caro es que la deteccion YA ESTABA y era correcta:

    echo "   ⚠ el store NO quedó en el volumen ⇒ lo que construya se PIERDE..."

Ese aviso se imprimio, con esas palabras, y el script siguio adelante: armo el
dead-man y sembro la cola igual. Un aviso que no detiene el pipeline no es un
guardian cuando no hay nadie leyendo el log. Es la regla 3 del CLAUDE.md con
otra cara: el ausente (el volumen) llego hasta el final diciendo que todo fue
bien.

Tres cambios, todos sobre el mismo fallo:

  - `hcloud volume attach` dejaba de tragarse el error. Estaba escondido dos
    veces: `>/dev/null 2>&1` el mensaje y `|| true` el codigo de salida.
  - Se pregunta ANTES quien tiene el volumen tomado, y el motivo lo nombra.
  - Los dos ⚠ finales pasan a ser `sin_volumen`, que NO siembra: borra el server
    recien nacido (vacio, para que no quede idle facturando como hworker-4) y
    sale 1. Mismo blindaje que el dead-man y farm-down: solo borra con label
    role=hammer-worker, verificado que gioser no matchea.

Escape explicito para el caso deliberado: SIN_VOLUMEN_OK=1.

Verificado en negativo, que es como se comprueba lo que un guardian IMPIDE: la
llamada corta con exit 1 sin alcanzar la linea siguiente, y con un server sin
label no borra nada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn
2026-08-23 17:13:09 +00:00
SergioandClaude Opus 5 262a4aeb61 granja: forzar la recompilacion — rsync -a preserva mtime y cargo no rebuildea
Sincronizar el codigo nuevo al worker y llamar a cargo NO basta: rsync -a
preserva el mtime del hub, asi que el fuente recien llegado puede quedar mas
VIEJO que el binario que el worker compilo hace un rato. cargo dice
«Finished in 0.09s», el worker se queda con el hammer anterior, calcula la
huella vieja y el paso 4 falla sin que se vea por que.

Paso el 2026-08-12 al empujar el lab con los -dev: el worker tenia el lab.rs
correcto Y el rootfs correcto, y aun asi divergia. Se arregla con un `touch`
a los fuentes antes de compilar.

El guardian del paso 4 hizo su trabajo: detecto la divergencia y NO dejo
construir. Sin el, el worker habria molido horas sellando en direcciones que
el hub nunca iria a buscar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:05:01 +00:00
SergioandClaude Opus 5 7279ea8780 granja: farm-up copiaba work/ ENTERO y podia dejar un worker sin dead-man
Dos fallos que se destaparon montando el primer worker con el lab anclado
(2026-08-11).

1. `--exclude /work` con barra inicial ancla SOLO la entrada `work`, no su
   contenido. Con `--include '/work/'` delante, rsync entraba al directorio y
   sus hijos no casaban con ninguna regla ⇒ se copiaba work/ ENTERO: 3,7 G,
   incluidos los mirrors de git A MEDIO COPIAR porque el propio rsync abortaba
   antes de terminarlos. El worker fallaba cada receta con `git fetch exit
   128`. Un work/ a medias es peor que ninguno: parece que esta. Se ancla con
   `/work/**`.

   Eso mismo causaba el abort: con --delete, rsync moria con «cannot delete
   non-empty directory» sobre los vendor/ de cargo, que quedan de SOLO
   LECTURA. Sin tocar work/, no hay nada que borrar ahi.

2. El `set -e` hacia que ese fallo abortara el script ANTES de armar el
   dead-man switch ⇒ server VIVO que no puede autodestruirse, que es
   exactamente lo que este script declara inadmisible. Ahora el rsync no es
   fatal: avisa y sigue hasta armarlo. El orden ideal es armar el dead-man
   ANTES de cualquier paso que pueda fallar; queda anotado en el codigo.

Verificado en seco contra un worker real: el patron nuevo no toca work/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:02:10 +00:00
SergioandClaude Opus 5 1c17f2f255 granja: empujar el lab anclado al worker y PROBAR que sella igual que el hub
farm-sync excluye /.dev-fs a proposito (el lab es del entorno, no del repo).
Eso valia cuando el toolchain no entraba en el hash. Desde 58d3161 si entra,
asi que un worker con el lab horneado de la golden (rustc 1.96) sella en
direcciones DISTINTAS a las del hub (1.97): no es que compile distinto, es
que lo guarda donde el hub nunca lo va a buscar.

Tampoco basta bootstrap-devfs.sh en el worker: su paso 0 solo trae la imagen
si NO hay rootfs, y la golden trae uno. Se reemplaza a la fuerza.

La imagen se EMPUJA por scp desde el hub en vez de bajarla del Storage Box,
para no poner la llave del box en el worker: el modelo hub-and-spoke dice que
el worker es compute puro sin secretos.

El paso 4 no es 'extraje la imagen', es comparar el hammer hash de una receta
testigo entre worker y hub. Si divergen FALLA: un worker que sella en otra
direccion quema dinero produciendo artefactos que nadie encuentra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:18:09 +00:00
sergioandClaude Opus 5 6a92bd3d19 granja: dos fallos que se repetían idénticos, que es la firma de que nadie reintenta
1. build-farm.sh — vulkan-loader fallaba con «io: File name too long (os
   error 36)» en TODOS los ciclos y sella sin una queja ejecutado en serie
   (comprobado a mano en el worker). La firma no estaba en la lista de
   colisiones del reintento serial, así que nunca se reintentaba: fallaba,
   contaba como deuda, y al ciclo siguiente fallaba igual.

   Un fallo que se repite IDÉNTICO no es intermitente: es uno que nadie
   está reintentando.

   Se añade la firma, pero el defecto de fondo era que faltarla fuese MUDO
   — la lista sólo puede crecer si alguien se entera de que se quedó corta.
   Ahora lo no reintentado se dice, con las primeras 120 letras del log.

2. farm-up.sh — no sembraba work/farm-sellados.txt. Un worker recién
   creado arrancaba con el manifiesto rancio horneado en la golden: 1172
   contra 1241 del hub, así que reconstruía lo que el hub ya tenía sellado
   (vulkan-loader entre ellos, y encima fallando).

   Es el mismo error de método que farm-sync.sh ya se documentó a sí mismo:
   «lo puse allí, di el bucle por cerrado, y la churn siguió porque hay DOS
   rutas». Había dos otra vez y sólo una estaba arreglada.

Tras el arreglo las cuatro colas dan 1/1 construyen (0 fallan) y el perfil
escritorio-kde cierra 162/162.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 22:28:30 -04:00
sergioandClaude Opus 5 ddc8561e07 buzón verificado en un worker real — y las dos veces que la prueba mintió
Cadena completa medida hoy: el worker depositó 63 artefactos (1,34 GB) en
8 s a 153 MB/s, y el hub los promovió con mv. Respaldo 1542 → 1605, buzón
en 0, guardián cuadrando. Contra los 440 kB/s del laptop, 348×.

Dos fallos de la prueba, que importan más que el resultado:

1. `ssh` cortaba con «Host key verification failed» porque la granja REUSA
   IPs y la host key cambia con razón. Como el stderr iba a /dev/null, el
   error salía como «no obtuve la pública del worker»: culpaba al worker
   cuando el problema estaba en el known_hosts del laptop. Ahora va por
   ssh_worker(), que purga la entrada vieja — correcto sólo acá, porque la
   identidad del worker es su label hcloud, no su llave.

2. verificar-buzon daba «aislado ✓» sin haber probado que escribe. El
   aviso de host key se comía el head -1, y el testigo se llamaba .btest,
   que `ls` no muestra. Un testigo invisible no prueba nada. Ahora exige
   las dos mitades y las dos están verdes: escribe en su buzón, y
   ../hammer/store no existe para él.

Y el progreso de rsync sólo con TTY: sin terminal reescribe con \r y deja
miles de copias de la misma línea. 8 segundos bastaron para un log
ilegible; en el latido sería cada media hora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:58:01 -04:00
sergioandClaude Opus 5 400f7171e0 respaldo: escribir sin poder borrar no es un permiso, es un alcance
Los permisos de subcuenta del Storage Box son BINARIOS: --readonly sí o no.
No hay append-only ni write-sin-delete (verificado en la API, no recordado).

Así que la contención se hace por alcance: el worker escribe en un BUZÓN
cuyo home es incoming/, y hammer/store sencillamente no existe para él. Lo
máximo que puede destruir es lo que él mismo depositó y aún no se promovió,
que sigue estando en el volumen. Un permiso puede estar mal puesto; un
directorio fuera de tu home, no.

La promoción es un mv DENTRO del mismo filesystem: un rename, instantáneo,
cero bytes por la red. Eso es lo que la hace viable con un uplink de
440 kB/s — el laptop manda órdenes, los datos van worker→box por dentro de
Hetzner (122 MB/s, 280×).

Medido de la shell del box, que NO es un bash:
  · no hay `for`   ⇒ los lotes se arman en el hub
  · `a; b` no encadena de fiar ⇒ una orden por conexión
  · `mv a b c dest/` sí acepta varios orígenes ⇒ cientos por conexión
  · `find` no existe y devuelve 0 EN SILENCIO ⇒ parece «no hay respaldo»

Y la trampa que motivó partir el trabajo en dos conjuntos: `mv A dest/` con
dest/A ya existente NO falla, mete A DENTRO y deja dest/A/A. Como el store
es CAS, un artefacto ya respaldado es idéntico ⇒ no se mueve, se borra del
buzón. Los dos caminos probados de punta a punta con un artefacto falso,
recuento verificado y sin anidar.

Defensa en profundidad aparte: plan de snapshots diario (03:17 UTC, retiene
7 de 10) y la carpeta ZFS visible para recuperar ficheros sueltos sin
rollback. La subcuenta no las alcanza: viven en la cuenta, con el token que
el worker no tiene.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:39:30 -04:00
sergioandClaude Opus 5 9388beba49 store en el volumen: «está sellado» dejó de ser «está en este disco»
El store de trabajo se mudó al volumen de la granja y el laptop quedó con
219 artefactos: los de fuente privada (que la granja no puede rehacer) más
lo que todavía no está respaldado. 979 artefactos verificados en el box se
borraron de acá, y con ellos el caché .dmerge de 92 G que sostenía sus
bloques: 128 G → 8,2 G, 115 G libres.

Eso rompía tres cosas que leían el disco local como si fuera la verdad:

- build-state.py habría reportado `never` sobre ~950 sellados y el latido
  lo habría COMMITEADO. Un grafo recién escrito miente con más autoridad
  que uno viejo. Ahora resuelve la presencia contra la unión del store y
  los manifiestos, y expone `sealed_remoto` para que «el store se mudó»
  no se lea nunca como «el corpus creció».
- farm-sync.sh armaba el manifiesto con `ls ./store`, así que le habría
  dicho al worker «el hub tiene 220» y el worker habría rehecho ~950. Es
  el bucle de churn que el propio fichero documenta, al revés. Ahora es la
  unión, con un guardián que aborta si el manifiesto encoge.
- cosecha-cron.sh bajaba el store entero cada 30 min: habría deshecho la
  mudanza sola, como la poda de 24 G que se deshacía en 2026-08-07. Ahora
  baja sólo la lista de nombres.

Los cuatro grafos regenerados dan idéntico a antes del recorte
(766/11/2), que es la prueba de que no se perdió nada.

Queda abierto: los artefactos nuevos viven SÓLO en el volumen hasta que
alguien corra una pasada de respaldo. El volumen tiene borrado protegido,
pero es una copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:22:36 -04:00
sergioandClaude Opus 5 2d6a3c774b respaldo→volumen: la -H de rsync no es opcional, y al llegar hay que podar los superados
Dos cosas medidas al traer los 167 GB.

1. `rsync -a` SIN `-H` no preserva los enlaces duros, y el store comparte ficheros entre artefactos
   justamente así. Llegó inflado: 76 G en el box → 159 G en el volumen, con **0 ficheros de nlink>1**
   al llegar — ésa es la prueba de que se perdieron, no una sospecha. (El 76 G del box es además ZFS
   comprimido, así que las dos cosas se sumaban y parecía peor.) El contenido es correcto —es CAS y
   los hashes casan—, pero ocupa de más y el volumen no sobra.

2. El respaldo conserva artefactos que el hub YA podó. Son SUPERADOS: existe otro con el hash
   vigente, y no pueden dar cache-hit nunca porque la receta que los nombraba cambió. Eran 544 de
   1536 · 31 G. Podados con work/store-gc-superados.txt como lista.
   El guardián es RECONTAR tras el rm: «borré 544» y «hay 544 menos» son afirmaciones distintas, y
   sólo la segunda es la que importa. Cuadró.

Volumen: 992 artefactos vigentes, 95 G libres de 246.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:02:48 -04:00
sergioandClaude Opus 5 17c4194f5d granja: subcuenta de SÓLO LECTURA del Storage Box — el respaldo entra al volumen por dentro de Hetzner
MEDIDO, no supuesto: el uplink de la oficina da 440 kB/s ⇒ los 127 G del store son 82 horas. Subir
el store desde el laptop no es lento, es imposible. Pero el Storage Box está en hel1 y el worker
está en hel1: por la red interna la misma copia va a 122 MB/s. Son 280×.
⇒ El laptop deja de ser el camino de los datos. Sólo manda las RECETAS (26 M).

POR QUÉ SUBCUENTA Y NO LA CUENTA PRINCIPAL. El worker es efímero y se borra solo; darle la
credencial principal sería darle permiso de borrado sobre el ÚNICO respaldo que existe. Un respaldo
al que puede escribir la máquina de la que hay que protegerse no es un respaldo. La subcuenta acota
el daño a cero por construcción: --readonly, --reachable-externally=false, home acotado a hammer/.

LA VERIFICACIÓN ES NEGATIVA. «Sólo lectura» es una afirmación sobre lo que el sistema IMPIDE, así
que leer no la prueba. Hay que intentar escribir y borrar y exigir que fallen:
  rm  → «Read-only file system» · scp → «dest open: Failure» · respaldo intacto.
El script trae ese paso como subcomando `verificar` y sale ≠0 si la subcuenta resulta escribir.

Dos gotchas que costaron:
· la API exige contraseña con mayúscula+minúscula+número+símbolo aunque después se use llave;
· la shell del Storage Box es RESTRINGIDA: acepta ls/mkdir/rm pero NO redirección, así que
  `echo k > authorized_keys` falla EN SILENCIO (crea el directorio, no el fichero). Va con scp.

La llave se genera EN EL WORKER (nunca viaja una privada desde el laptop) y muere con él.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:43:16 -04:00
sergioandClaude Opus 5 4c06eaef88 farm-up: los comentarios nuevos llevaban backticks DENTRO de la cadena que va por SSH
El bloque del volumen es una cadena entrecomillada que se manda al worker, así que un backtick ahí
no es tipografía: es SUSTITUCIÓN DE ÓRDENES. Mis comentarios de la commit anterior citaban
`volume attach` y `blkid` con backticks ⇒ la shell intentó EJECUTAR «volume attach» («volume:
command not found») y a partir de ahí el resto del bloque se mandó mutilado: mkfs.ext4 sin
dispositivo, mountpoint sin argumento.

Por eso los comentarios originales de ese mismo bloque escapan los backticks con \`. Yo escribí los
míos con el estilo del resto del fichero, que es correcto FUERA de la cadena y venenoso dentro.
Arreglado quitándolos: en un comentario que viaja por SSH, la comilla no vale lo que cuesta.

El volumen no sufrió: sigue con su filesystem del 17 de julio y montado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:35:08 -04:00
sergioandClaude Opus 5 68d8ae0aa9 dead-man: una transferencia rsync también es trabajo
Poblar el volumen con el store del hub son horas de rsync sin un solo `hammer build`. El dead-man
sólo miraba builds y latido ⇒ acumulaba ticks y borraba el worker A MITAD DE LA COPIA. El volumen
sobrevive (para eso está), pero la transferencia muere y hay que reanudarla a mano; sobre un enlace
lento eso puede no converger nunca.

`pgrep -x` y no `pgrep -f`: `-f` mira la línea de órdenes entera y se auto-matchea con el propio
dead-man si su ruta contiene la cadena. Ese error ya nos hizo informar cuatro veces procesos «vivos»
que estaban muertos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:29:07 -04:00
sergioandClaude Opus 5 36081cf1cf farm-up: DOS bombas de pérdida de datos — una ya explotó y se llevó ~900 artefactos del volumen
Investigando por qué el worker nuevo arrancó con 2 artefactos en vez de ~900, apareció esto.

BOMBA 1 — `rm -rf $REMOTE/store` sin comprobar si es punto de montaje.
La guarda era `[ -d ] && [ ! -L ]`: basta en un server creado desde la golden ORIGINAL, donde
/opt/hammer/store es un directorio normal. Pero al re-snapshotear un worker (ayer, para meterle
Rust y Go) la imagen se llevó también LA CONFIGURACIÓN DE MONTAJE ⇒ el server nuevo monta el
volumen en esa ruta AL ARRANCAR, antes de que corra el «rescate». Entonces el `rm -rf` borra A
TRAVÉS DEL MONTAJE y se lleva el store del volumen entero — justo lo que el volumen existe para
proteger.
Evidencia: el volumen pasó de ~900 artefactos a 2, pero NO se perdió el filesystem (lost+found
de julio, 17 montajes, .dmerge intacto). Se borró el CONTENIDO, no el disco. Ahora hay
`mountpoint -q`: si ya está montado no hay nada que rescatar ni que borrar.

BOMBA 2 — `mount … || { mkfs.ext4 -F …; }`. Formatear ante CUALQUIER fallo de montaje, incluida
la carrera normal entre `volume attach` y la aparición del dispositivo. Un hipo de segundos
destruía el volumen. Ahora sólo formatea si `blkid` dice que NO tiene filesystem; si lo tiene y
no montó, aborta y lo dice.

Las dos comparten la misma forma: un fallback destructivo que asume la causa benigna. `mv -n`
está bien elegido (no pisa), pero el `rm -rf` que lo sigue anulaba esa prudencia.

Y una consecuencia que hay que asumir: re-snapshotear un worker NO conserva el store — los
artefactos viven en el volumen, que no entra en la imagen. La golden aporta toolchain y sistema;
la caché la aporta el volumen. Hoy la imagen decía traer store y no lo trae.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 08:11:57 -04:00
sergioandClaude Opus 5 889720cac0 granja: golden NUEVA con Rust y Go — desbloquea el 76% del catálogo
Imagen 417847948 («hammer-golden-rust-go-2026-08-08») reemplaza a 408909310 como default de
farm-up.

EL PROBLEMA QUE CIERRA: hammer invoca `cargo vendor` y el toolchain Go en el HOST, no dentro
del sandbox — el vendoreo pasa ANTES de entrar a la caja. La golden vieja no los traía, así que
la granja no podía construir NINGUNA receta Rust ni Go: COSMIC entero, las 228 Rust y las 362
Go del corpus. El 76% del catálogo dependía de UNA SOLA máquina, el hub — justo lo que el
respaldo de ayer intentaba dejar de asumir.

VERIFICADO ANTES DE SNAPSHOTEAR, no después: `cosmic-bg` —que minutos antes moría con
`spawn cargo vendor: No such file`— sella en el worker. Un snapshot de una máquina a medio
arreglar es peor que ninguno.

MISMAS VERSIONES QUE EL HUB (cargo/rustc 1.96.0, go 1.26.4) a propósito: introducir un skew de
toolchain nuevo mientras se arregla otro es cómo se fabrican los fallos que nadie reproduce. Y
1.96 es además el techo MSRV del sandbox ya documentado.

NO RELAJA LA HERMETICIDAD: cargo/go son herramientas de FETCH, del mismo orden que `git` para
clonar. El build sigue ocurriendo dentro del sandbox con el árbol ya vendoreado. Es lo contrario
del caso «no engordar el rootfs del worker», que habla del rootfs DEL SANDBOX.

El PATH queda persistido en /etc/profile.d/hammer-toolchain.sh para que sobreviva a los logins
no interactivos de la granja. Y la imagen trae el store del worker al momento del snapshot (KDE
y GNOME reconstruidos hoy), así que los workers nuevos arrancan con más caché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:33:57 -04:00
sergioandClaude Opus 5 39b88ebd2a churn del store: eran CUATRO rutas de cosecha, no una — arreglé una y di el bucle por cerrado
Podé el store por tercera vez hoy: 363 artefactos superados, 24 G. Comparado con la poda de la
tarde: **362 en común**. Volvieron otra vez, pese al arreglo de esta tarde.

LA CAUSA de que el arreglo no sirviera: puse el `--exclude-from` del registro de podados sólo en
`farm-sync.sh`, y el latido NO usa ese script. `cosecha-cron.sh` tiene su PROPIO rsync del store
(línea 71), y corre cada 30 minutos por cron ⇒ una poda de 24 G quedaba deshecha en menos de una
hora. Los tres commits de «cosecha granja» de la madrugada son exactamente eso.

Y al ir a arreglarlo, en vez de parchear y seguir, busqué si había más: **son CUATRO** las rutas
que bajan el store del worker — `cosecha-cron.sh`, `farm-sync.sh`, `harvest-go.sh` y
`farm-down.sh`. Tenía protegida una de cuatro.

El error de método vale más que el bug: arreglar una de varias copias y declarar victoria es
PEOR que no arreglar, porque el síntoma se atenúa lo justo para dejar de mirar. Lo que lo
delató fue comparar los manifiestos de dos podas (`comm -12`), no leer código. Dos podas con el
mismo número no son coincidencia — ya está anotado como regla, y hoy la regla pagó dos veces.

Las cuatro llevan ahora el mismo `--exclude-from` y una nota que dice que son cuatro, para que
la quinta —si aparece— nazca protegida.

Disco: 112 G → 136 G tras la poda de ahora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 21:52:46 -04:00
sergioandClaude Opus 5 129aff9dd9 granja: el worker rehacía 87 de 90 recetas por ciclo — el sync del store es de UN SOLO SENTIDO
Fui a mirar los fallos de la granja y el número no cuadraba: de 20 «fallos» de incoming-gnome,
17 correspondían a recetas CON ARTEFACTO VIGENTE en el hub. Primero pensé que eran marcadores
`.fail` rancios, y me equivoqué: `build-farm.sh` hace `rm -rf "$FARM"` al empezar cada ciclo,
así que el status es siempre del ciclo actual. Después pensé que el worker corría recetas
viejas, y también me equivoqué: comparados los md5, hub y worker tienen copias IDÉNTICAS.

LA CAUSA REAL está en `farm-sync.sh`: sube el código EXCLUYENDO /store y baja el store del
worker. El store viaja worker→hub y nunca hub→worker. O sea que **el worker no se entera de
nada de lo que se sella en el hub**, y cada ciclo reintenta —y vuelve a fallar— trabajo ya
hecho. Medido: de las 90 recetas de incoming-gnome, **87 ya están selladas en el hub**. El
worker estaba quemando el 97% de esa cola en repetir lo hecho.

EL ARREGLO no es mandarle los artefactos (gigabytes por un enlace de 8 Mbps) sino la LISTA:
`work/farm-sellados.txt` son los nombres `<hash>-<paquete>` del store del hub, unos kilobytes.
`farm-sync.sh` la regenera en cada sync (para que no envejezca sola) y `build-farm.sh` salta
la receta cuyo hash vigente ya esté ahí. Ojo al detalle de rsync: la subida excluye /work, así
que el manifiesto necesita un `--include` ANTES del `--exclude` — rsync aplica la primera regla
que casa, y sin ese orden el fichero no viajaba y el arreglo no habría hecho nada en silencio.

Y LAS TRES DEUDAS REALES DE LA GRANJA SON UNA. gnome-session, gnome-settings-daemon y gdm
mueren todas en GTK3, que el frente GNOME aparcó a propósito. Verificado contra el meson.build
de cada tag: gnome-session 48.0 lo pide incondicional; gnome-settings-daemon 48.1 pide gtk+-3.0
Y gtk+-x11-3.0 (dos de las tres deudas aparcadas, no una); gdm 48.0 lo tiene condicionado a
`if have_xdmcp` pero da igual, porque muere construyendo gnome-session.

De paso, gnome-session pasaba `-Dsystemd=false -Dsystemd_journal=false`, dos opciones que NO
EXISTEN en 48.0 (sus opciones reales son seis: deprecation_flags, session_selector,
systemduserunitdir, docbook, man, x11). Meson aborta en la primera opción desconocida, así que
la receta ni llegaba a configurar y el fallo real quedaba tapado. Corregido, más -Dx11=false.
El diagnóstico de fondo YA ESTABA en la receta desde el 2026-07-27 y era más completo que el
mío (dice GTK3 **y** libsystemd); quité la nota duplicada que había añadido.

Y en el respaldo: la comprobación inicial de SSH era de un solo intento y tiró la corrida al
relanzarlo llegando a casa, con el wifi aún sin levantar. Es el peor momento para rendirse —
quien relanza un respaldo interrumpido acaba de cambiar de red. Ahora reintenta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:27:29 -04:00
sergioandClaude Opus 5 6263d24788 store-gc + farm-sync: cortar el BUCLE DE CHURN de 24 G que hacía inútil la poda
Podé el store por la mañana: 362 artefactos superados, 24 G. Lo volví a podar por la tarde:
362 artefactos, 24 G. Comparados los dos manifiestos, **solapamiento del 100%: son los
MISMOS 362**. No era casualidad ni recuento nuevo — es un bucle.

LA CAUSA no estaba en el gc sino en `farm/farm-sync.sh`, que bajaba el store del worker
ENTERO sin filtro. El worker nunca se poda, así que conservaba los superados y nos los
devolvía en cada cosecha. Podar → cosechar → vuelven → podar. El disco pagaba 24 G por
vuelta y el gc informaba éxito cada vez, lo que es peor que fallar: parecía que se avanzaba.

Es la misma familia de fallo que el gc que reportaba borrados falsos, sólo que un nivel más
arriba: entonces el borrado no ocurría, ahora ocurre y se deshace solo. En los dos casos el
mensaje de éxito era cierto y engañoso a la vez.

EL ARREGLO es darle memoria al sistema. `store-gc.sh` acumula lo podado en
`work/store-gc-superados.txt` y `farm-sync.sh` lo usa como `--exclude-from` al bajar el
store del worker. Es seguro porque el store es CAS: los nombres `<hash>-<paquete>` son
inmutables y un artefacto superado no vuelve a ser vigente salvo que una receta retroceda —
y si retrocediera, se reconstruye, que es barato.

Disco: 159 G → 182 G tras la poda de hoy, y esta vez debería quedarse.

De paso, en el respaldo: rsync 24 («ficheros del origen desaparecieron») NO es un fallo, es
justo lo que pasa al podar mientras se sube. El bucle de reintentos lo tomaba por error real
y habría parado el respaldo entero por algo inofensivo — y encima cuando el disco aprieta,
que es cuando menos conviene quedarse sin copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:11:31 -04:00
sergioandClaude Opus 5 14bed923ad granja: el worker no molía GNOME — faltaban dos colas en QUEUES
El default era fósil de cuando `incoming-gnome-onda1` era el frente entero:
listaba esa cola (6 recetas) pero NO `incoming-gnome` (86, donde viven gtk4,
mutter y gnome-shell) ni `incoming-gnome-onda2` (la isla dinámica, 22). El
worker reportaba moler GNOME y molía 6 de 114.

Se agregan las dos y se mueven las tres ANTES de incoming-kde: con 205 recetas
KDE por delante, un ciclo se consumía sin darles turno aunque estuvieran
listadas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:51:41 -04:00
sergioandClaude Opus 5 570fd3747e 🧮 cosmic: trazarle la yupana — el frente existía en el store y el ábaco no lo veía
Faltaba la mitad del método.  SÍ cruzaba incoming-cosmic (lo usé
antes de tocar libdisplay-info, pipewire y glib), pero la campaña no estaba en el
KHIPU: build-state.py sólo conocía --kde y --gnome, y targets.toml no declaraba
perfil.

Y eso no era cosmético. Antes de este commit,  decía
21 dependientes transitivos y dos imágenes; ahora dice 31 y TRES —
escritorio-cosmic entre ellas, con incoming-cosmic=10 en el reparto por cola. O
sea que el próximo que tocara libinput, libudev-zero o mesa habría MEDIDO DE
MENOS, que es exactamente el punto ciego que la metodología existe para cerrar.

Tres piezas:
- build-state.py --cosmic (y su build-state-cosmic.json).
- perfil.escritorio-cosmic en targets.toml, con las raíces que cosmic-session
  levanta MÁS los datos que ningún [deps] alcanza (iconos, xkb, fuentes, bash,
  dbus). Primera medición: 54/61 listo, faltan 7.
- el LATIDO lo regenera, por la misma razón por la que se le agregó GNOME en su
  momento: un frente que el cron no regenera envejece en silencio, y un grafo
  viejo miente con la misma cara que uno fresco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:44:18 -04:00
sergioandClaude Opus 5 5aec112629 gnome onda 3: cierra las 3 fronteras de gnome-desktop + el latido regenera el grafo GNOME
Las 3 fronteras que el propio comentario de gnome-desktop declaraba:

  iso-codes         receta nueva (datos + .pc). Ya no está en download.gnome.org (404);
                    Salsa es GitLab y su archive no es determinista (lección de cairo-shared),
                    así que la fuente es el .orig.tar.xz inmutable del pool de Debian.
                    Sin traducciones: i18n.gettext exige msgfmt completo y sólo hay
                    gettext-tiny — se vacían los 8 meson.build de dominio.
  libseccomp        receta nueva (autotools estático, gperf de build-dep real).
  xkeyboard-config  duplicada desde incoming-kde: fichero idéntico ⇒ MISMO ArtifactHash
                    (b3:bcf9b766) ⇒ ya está SELLADO. Frontera cerrada con cero rebuild.

Y un punto ciego del latido: cosecha-cron regeneraba build-state.json y el de KDE, pero
NUNCA el de GNOME. El grafo llevaba días mintiendo `never` sobre gobject-introspection y
toda la onda 2, que están selladas. Un grafo viejo miente con la misma cara que uno fresco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:14:59 -04:00
sergioandClaude Opus 4.8 da26b3a280 gnome onda-1: cola aislada incoming-gnome-onda1 para el worker
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>
2026-07-24 23:31:27 -04:00
sergioandClaude Opus 4.8 b98995b7b4 deadman: NO contar farm-worker-loop como trabajo (3er bug del dead-man)
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>
2026-07-23 14:14:16 -04:00
sergioandClaude Opus 4.8 2bd1944173 deadman: --puede-borrar sourcea /etc/hammer-deadman.env (o daba falso-negativo)
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>
2026-07-23 03:48:41 -04:00
sergioandClaude Opus 4.8 3de75a5229 granja: el dead-man del worker se mata SOLO (sin hub) — 3 bugs + gioser doble-blindado
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>
2026-07-23 03:23:42 -04:00
sergioandClaude Opus 4.8 be06314336 granja: el reaper dropea de .fleet los workers que ya no existen en hcloud
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>
2026-07-23 03:08:13 -04:00
sergioandClaude Opus 4.8 1203f86cda granja: reaper HUB-SIDE — la garantía "vps idle inadmisible" deja de depender del worker
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>
2026-07-23 03:05:12 -04:00
sergioandClaude Opus 4.8 6a0dc40a05 sandbox: el merge de deps cae en el store (mismo fs), no en la raíz — desbloquea kio
merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para
no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba
la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que
el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN
bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge
caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps
de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" →
kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado.

Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store
mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge
falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona.
En el laptop andaba por casualidad (store y su padre en el mismo fs).

`.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times.
cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo
expandiría a copias reales).

Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a
kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una
"sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de
timing sacó a la luz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:06:10 -04:00
sergioandClaude Opus 4.8 85b5a9bc46 yupana: instrumentar duración de build en el worker → camino crítico PESADO
El peso que le faltaba a las ondas: sin él, critical-path = nº de pasos; con él,
ETA real en segundos.

INSTRUMENTACIÓN (worker):
  scripts/farm/build-timed.sh envuelve `hammer build` midiendo la pared y la
  registra en $STORE/.times/<hash>-<host>.json — un SIDECAR del store, NUNCA
  dentro del artefacto. La duración es no determinista (varía por máquina/carga)
  ⇒ no puede tocar el ArtifactHash. Keyed por hash (CAS-safe: dos workers no se
  pisan) + host (varias muestras por receta). Viaja al hub con el rsync de store
  que ya hace la cosecha; ninguna herramienta del store lo confunde con artefacto.
  Sólo builds REALES (pared ≥ UMBRAL 3s) — los cache-hits no envenenan la mediana.
  campana-deuda.sh ahora construye vía build-timed.sh (transparente, mismo exit).

CONSUMO (hub): yupana._tiempos() carga name→mediana de segundos; keystones
computa el CAMINO CRÍTICO pesado = longest weighted path del subgrafo de deuda
(peso = segundos de build). La cadena más larga hay que construirla en SERIE
aunque haya ∞ workers ⇒ es la ETA con paralelismo infinito. Degrada con gracia:
sin datos, peso=1 y el camino crítico = nº de pasos (lo que ya daban las ondas).

Verificado: con muestras sintéticas da "ETA 19 min sobre 5 pasos, kio → kparts →
frameworkintegration → breeze → plasma-integration"; sin datos degrada a "5 pasos
(SIN datos)". store/.times está git-ignorado (metadata de máquina, no se commitea).

LÍMITE honesto (en la cabecera de build-timed.sh): `hammer build` arrastra deps ⇒
la pared incluye deps no selladas. En orden topológico (drenar) las deps ya están
selladas y la pared mide sobre todo ESTA receta — cota superior buena para pesar.
La precisión exacta pediría instrumentar el sellado dentro de hammer-build (Rust).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:25:09 -04:00
sergioandClaude Opus 4.8 25e3878331 farm: JOBS=1 en el worker-loop mientras el ADR 0012 esté sin decidir
`build-farm.sh` usa `xargs -P$JOBS`; con >1, dos recetas que comparten dep disparan la carrera del
árbol de fuentes del ADR 0012 y el árbol queda roto para siempre. Se vio a escala: al invalidar
libdrm, 118 de las 205 recetas KDE pasaron de cache-hit a rebuild real y ~93 murieron con
"/src/.zwrap/cc is not a full path to an existing compiler tool" — el wrapper de zig que la receta
deja en el ÁRBOL DE FUENTE, barrido por el fetch concurrente de otra receta sobre el mismo qtbase.
El reintento serial de build-farm tampoco las recupera: el árbol ya quedó inconsistente.

La caché estaba ocultando la carrera, no evitándola. Serializar es la única mitigación correcta
hasta que el ADR 0012 elija salida.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:15:38 -04:00
sergioandClaude Opus 4.8 b08e582c89 catálogo objetivo P4: la cola de la granja pasa a ser derivada del grafo
drenar.py parte la deuda en ONDAS topológicas (una receta sólo espera por deps
que también estén en deuda; las selladas ya están en el store). Onda 1 =
construible ya; dentro de cada onda, orden por `unblocks` desc.

Da lo que una lista plana no da: el orden correcto, la PROFUNDIDAD real de la
cadena (pasos secuenciales = el reloj de pared) y qué paraleliza sin pisarse.

   base                0 en deuda   imagen completa
   cli                 0 en deuda   imagen completa
   escritorio-kde     77 en deuda   12 ondas   onda 1 = 1 receta: qtbase
   escritorio-mirada   4 en deuda    2 ondas   onda 1 = 3

HALLAZGO: el escritorio KDE está serializado detrás de UNA receta. qtbase
destraba 117 nodos y es lo único de la onda 1 — hasta que no esté sellada no hay
nada que paralelizar, por más workers que se enciendan. Cambia la pregunta de
"¿cuántas faltan?" (77, poco informativo) a "¿cuál es el camino crítico?".

Dos correcciones salidas de mirar la salida:
· el grafo se elige por la `cola` declarada en targets.toml, NO por cuál tiene
  más nodos: incoming-kde SOMBREA recetas canónicas, así que medir
  escritorio-mirada contra el grafo KDE medía una imagen que nadie construye
  (3 en deuda en vez de 4). El mismo atajo estaba en seed-graph.py --frontera;
  corregido en los dos.
· regla de la fuente única implementada en deps_de(): manda la receta si existe,
  la semilla sólo si el nodo es `wanted`, marcando procedencia en la salida.

Enchufado al latido: cosecha-cron regenera docs/state/drenaje.json junto al
grafo y lo commitea ⇒ cola derivada y versionada, el git diff entre ciclos
muestra qué salió de la deuda. NO lanza builds: eso cuesta € y sigue siendo
decisión explícita (farm-up + campana-deuda.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:14:37 -04:00
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 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 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