10 Commits
Author SHA1 Message Date
Sergio d79299d351 granja: cuatro sondas quedaron ciegas tras el renombre — el informe decía «idle» compilando
Todas buscaban el proceso `release/hammer`, y desde el ADR 0016 el worker invoca
`./target/release/takana`. Ninguna daba error: simplemente no encontraban nada.

Medido hoy, no deducido: `estado-granja.sh` imprimía «moliendo: (nada — idle o entre
colas)» mientras en el worker corría
`./target/release/takana --store ./store build recipes/centrifugo.toml`. Leyendo ese
informe se concluye que la granja está seca. Tras el arreglo, el mismo informe dice
«moliendo: cilium-cli.toml» y `pgrep` allá confirma exactamente ese proceso.

Las cuatro:
  · estado-granja.sh      «moliendo» — mentía sobre si hay trabajo
  · farm-worker-loop.sh×2 detección de build en vuelo
  · deadman.sh            `hay_trabajo` — y acá el precio es un worker BORRADO a mitad
                          de un build. En el LXC no llegó a morder (sólo borra cajas
                          hcloud y su timer no está activo allá), pero la próxima caja
                          de pago sí lo habría pagado.

Se aceptan LOS DOS nombres: cargo sigue compilando `hammer` como alias, y una sonda que
sólo mira el nombre nuevo se rompería con un worker que arrastre binario viejo — que es
justo lo que pasa acá, porque la siembra excluye `/target`.

Es el mismo renombre que dejó colgado el enlace de `go` (commit anterior). Un renombre
no rompe sólo lo que compila: rompe las cadenas que alguien escribió a mano.
2026-09-14 14:12:31 +00:00
Sergio aacee245d6 takana: el worker pasa a /opt/takana y takana-farm.service
Renombrado el directorio del worker y la unit, con las tres units del repo
(farm, deadman service y timer). Todas las rutas /opt/hammer del hub pasan a
/opt/takana: eran defaults REMOTE= y HAMMER_DIR=, mas los systemctl/journalctl
que nombraban la unit sin el sufijo .service, que el primer sed no casaba.

Orden, para que no quedara partido: el hub siembra por rsync a una ruta fija,
asi que se saco el cron ANTES de mover. Si se movia el worker con el hub
apuntando a la ruta vieja, la siguiente cosecha recreaba /opt/hammer y quedaban
dos arboles.

Verificado de punta a punta:
- el worker cerro un ciclo de build REAL desde /opt/takana (dunst sellado, 1/1)
- una cosecha completa del hub contra la ruta nueva: siembra, manifiesto, los
  nueve grafos, static-audit (691 estaticos, MIENTEN 0) y estado pusheado
- la unit vieja quedo deshabilitada y borrada; /opt tiene una sola entrada

NO se tocan dos referencias a /opt/hammer que siguen siendo CORRECTAS:
docs/23-plan-rehasheo.md describe rutas EMBEBIDAS en artefactos ya construidos
—que literalmente dicen /opt/hammer/work en sus secciones .debug— y el HANDOFF
de la noche de KDE es registro. Cambiarlas haria que los documentos mientan.
2026-09-09 20:20:16 +00:00
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00: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 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 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