Commit Graph
1417 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 6f12c231fa mudanza etapa 2: el PLAN — exportable, con el comando literal de cada paso
`planear.py` convierte un censo decidido en un plan ejecutable. Cada paso lleva su COMANDO LITERAL y
su verificación, así que el fichero se ejecuta a mano, línea por línea, sin la herramienta y sin este
repo. Eso es lo que el usuario pidió como «pasos exportables».

Las tres reglas, cada una pagada en el SDD 28:

1. **Nada sin decidir se ejecuta**: aborta con código 2 listando qué falta. El silencio no es
   consentimiento — sin decisión, ni mudar ni matar es correcto. Probado en los dos sentidos.
2. **Lo que muere se dice por su nombre y con su tamaño ANTES de borrar**, y el paso no borra nada:
   es una lista para leer antes de apagar el origen.
3. **Cada copia se verifica EN DESTINO**: el `rsync` que llenó el disco devolvió 0 y dejó 1367
   artefactos vacíos; sólo se vio contando del otro lado.

Y el preflight dimensiona con el tamaño de COPIA (hardlinks expandidos, 60 G → 85 G medidos), no con
`du`; si un tamaño resulta ilegible lo dice en vez de contarlo como 0.

**El añadido que cambia el valor: la invocación real.** Decir «este servicio no está declarado»
nombra el problema; lo que hace falta para resolverlo es cómo corre AHORA. El censo lo lee de
`/proc` (`cmdline` + `cwd`, nunca `environ`: el entorno trae tokens) y el plan lo emite:

    #   cmdline: /mnt/vvv/tawasuyu/target/debug/deps/puerta-f6e393ffbef3999e
    #   cwd    : /mnt/vvv/tawasuyu/shared/tejido
    #   puertos: 34221, 44961

Ese servicio de gioser es un binario de `target/debug/deps/` corriendo en producción, con dos
puertos, que ninguna declaración conoce. Apagada la máquina vieja, eso no se reconstruye de memoria.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 15:03:33 +00:00
SergioandClaude Opus 5 95c16097e4 SDD 29 + censar.py: la mudanza como PRODUCTO — censo, plan, aplicación. Sin IA.
Corrección de rumbo pedida por el usuario: el SDD 28 derivó hacia «poner ESTA caja a punto» —
instalar caddy, copiar claves, montar discos—, y eso es configurar un servidor, no construir algo.
**caddy no es un hueco de la imagen: es una instalación particular del usuario, y el programa tiene
que DESCUBRIRLA, no traerla.**

Y la consecuencia de método: todo lo que hice a mano en el SDD 28 ES la especificación de este
programa. Cada paso fue determinista; el único juicio fue «¿esto se muda o muere?», que es justo lo
que se le pregunta al usuario. No hace falta IA: hace falta que esté escrito.

**Etapa 1 implementada: `scripts/mudanza/censar.py`.** Sólo lee. Cruza lo DECLARADO contra lo VIVO y
reporta tres clases, cada una justificada por un error medido:

- `rc-status` decía `stopped` de cinco servicios que estaban VIVOS ⇒ la verdad es `ppid==1`, no el init.
- De 29 dominios, 10 vivos: 13 sin DNS, 2 en 502 y **4 que ya resuelven a otra máquina** — por eso se
  compara la IP del DNS contra las de la máquina, o `ya-mudado` se lee como `vivo`.
- `du -sh` no dice cuánto ocupa COPIAR con hardlinks (60 G vs 85 G): se reportan los dos.
- Los nombres no coinciden (`act-runner`/`act_runner`, `crond`/`cronie`, `dbus-daemon`/`dbus`): sin
  normalizar, el mismo servicio sale a la vez como `no-declarado` y `declarado-muerto`.
- Lo descartado se CUENTA: un `ppid==1` que no es servicio suele ser un huérfano, y eso es hallazgo.

Probado contra gioser, donde las respuestas ya se sabían a mano: encuentra MÁS (29 dominios contra
los 19 que probé; apareció `hifas.gioser.net`). Y afinarlo importó: la primera versión daba 28
`no-declarado` con ruido, la segunda da 18 y son reales (`matilda`, `pacha`, `puerta-…`, `adb`).
Un guardián con hallazgos falsos se ignora entero.

El SDD deja escritas las etapas 2 (plan exportable: el fichero ES la interfaz) y 3 (aplicación
idempotente que verifica EN DESTINO y deja el server corriendo), y mide qué ata la imagen a Hetzner:
menos de lo que parece — BIOS vs UEFI y los metadatos de red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 14:51:08 +00:00
Sergio 62f1c2b198 estado: cosecha granja 2026-09-11T14:32:06Z — avance del árbol KDE 2026-09-11 14:32:06 +00:00
SergioandClaude Opus 5 e5858d23e9 SDD 28 §6.13: upgrade apply no es un gestor de paquetes — cada generación ES un árbol
Lo destapó usarlo dos veces seguidas: tras aplicar `zstd-cli` y después `rsync`, el informe dice
`- 1 retirados` y `zstd` desaparece. Aplicar un árbol nuevo RETIRA los ficheros del anterior, porque
una generación es el estado completo y no un incremento. Es coherente con la ayuda («aplica un árbol
Stage1/PRODUCTO») y con que exista `rollback`: lo que se revierte es un sistema, no un paquete.

⇒ Usarlo para paquetes sueltos funciona una vez y se deshace a la siguiente. La forma correcta de
poner una caja al día es componer el árbol del perfil —lo que ya hace `servidor-image.sh`—, sellarlo
y aplicar ESO. Que además sería la «actualización en sitio probada» del SDD 19 §5.2 de verdad: una
imagen entera con rollback sobre una caja viva.

La caja quedó con `zstd` y `rsync` puestos a mano: funciona y no es el camino, igual que el `git`.

De paso, verificado que el arreglo de `recipes/rsync.toml` sirve: el respaldo ya no imprime el aviso
de compresión degradada — `compress list` pasó de `zlibx zlib none` a `zstd zlibx zlib none`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 14:03:02 +00:00
Sergio 90fade56f1 estado: cosecha granja 2026-09-11T14:02:19Z — avance del árbol KDE 2026-09-11 14:02:19 +00:00
Sergio 8ebcc99930 estado: cosecha granja 2026-09-11T13:32:05Z — avance del árbol KDE 2026-09-11 13:32:05 +00:00
SergioandClaude Opus 5 426093cd20 SDD 28 §6.13: takana upgrade probado de verdad — apply, rollback y un corte duro
Probado con un paquete que la caja necesitaba (`zstd-cli`): apply deja generación, la caja pasa a
desempacar su propio lab, `rollback` devuelve el estado anterior fichero a fichero (9 restaurados,
1 borrado), y tras el arreglo de durabilidad **sobrevive a un `hcloud server reset`, que es un corte
DURO y no un apagado limpio**.

Y cruza la frontera que `hydrate` no puede: store en el volumen, `/` en el disco local. Es el único
camino de actualización que funciona en una caja instalada.

El bug que destapó está en el commit anterior: `write_atomic` era atómico frente a otros procesos y
no frente a un corte, así que el primer upgrade se perdió al reiniciar y `recover` abortaba con la
huella misma del corte. Verificado en la máquina en los dos sentidos: binario viejo ⇒ estado en 0
bytes; binario nuevo ⇒ estado íntegro.

`recipes/takana.toml` re-pineado al commit del arreglo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 13:31:56 +00:00
SergioandClaude Opus 5 d3b42f892d zstd-cli: la distro comprime todo con zstd y no traía con qué descomprimirlo
`recipes/zstd.toml` construye **sólo `lib/`** —lo dice su propia cabecera, «build de lib/ (no CLI)»—
porque lo que necesitan sus 13 consumidores (mesa, libadwaita, appstream, rsync, los `zstd-sys`) es
`libzstd.a`. El binario nunca se construyó, y el agujero sólo se ve al instalar la distro en una
máquina de verdad: **un takana recién instalado no puede desempacar su propio laboratorio**, porque
el `tar` del rootfs es el de busybox (`tar: unrecognized option: zstd`) y `zstd` no existe. Hubo que
extraer por tubería desde otro hub — y un hub que se instala solo no puede depender de eso.

⚠ **Y mi arreglo anterior estaba mal**: declaré `zstd` en `perfil.base` dando por hecho que traía el
binario. Se destapó aplicándolo con `takana upgrade` en la caja: «✓ generación 1 aplicada, + 8
añadidos» y `zstd` seguía ausente, porque los 8 ficheros eran headers y `libzstd.a`. **El upgrade
hizo exactamente lo que debía; lo que estaba mal era lo que le pedí que aplicara.** Corregido a
`zstd-cli`.

Variante y no ampliación de la canónica: `zstd` es dep de 13 recetas y `mesa` está entre ellas;
re-hashearla arrastraría esa torre por un binario de 1 M que ninguna usa. Mismo criterio que las 23
variantes `*-shared`, con el eje en librería/herramienta en vez de estático/dinámico. Radio cero.

`HAVE_ZLIB/LZMA/LZ4=0` a propósito: sin eso el Makefile las detecta del sysroot del LAB y el binario
sale pidiendo sonames que el artefacto no publica. Verificado: estático, 0 intérpretes requeridos,
`Zstandard CLI v1.5.7`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 13:18:41 +00:00
Sergio be1187e2d1 estado: cosecha granja 2026-09-11T13:02:35Z — avance del árbol KDE 2026-09-11 13:02:36 +00:00
SergioandClaude Opus 5 19576e0039 SDD 28 §6.12: la caja dejó de ser desechable, y hydrate no puede cruzar el store
Dos consecuencias de que la caja ya sea un hub:

1. **`dd` de la imagen ya no es actualizar: es perder.** Sobrescribe el disco local, donde ahora
   viven el clon, las dos claves, las credenciales del respaldo, `/srv/repo` y caddy. El store se
   salva sólo por estar en el volumen. Y peor: la imagen etiqueta su partición local como
   `hammer-store`, así que tras un `dd` habría DOS filesystems con esa etiqueta y el `findfs` del
   arranque elegiría cualquiera. El `dd` es para PROVISIONAR; actualizar es `takana upgrade`, que
   existe con generaciones y rollback y está sin probar acá.

2. **`takana hydrate` no puede proyectar al root vivo**: hidrata con hardlinks y en una caja
   instalada el store es SIEMPRE otra partición que `/` (`Cross-device link`). No es culpa del
   volumen — en el layout original `/store` es sda4 y `/` es sda2. Hidratar al FHS vivo nunca fue
   posible en una caja instalada; funciona en el hub porque ahí store y destino comparten filesystem.

El `git` arreglado se instaló copiando el artefacto a mano: funciona, y no es el camino.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 13:01:42 +00:00
SergioandClaude Opus 5 9afe5afecc SDD 28: PUERTA 4 CERRADA — la granja late en la caja nueva
Con el volumen `takana-store` montado, la cosecha completa del worker corrió: **1369 → 1575
artefactos, 0 vacíos**, exactamente los 206 que faltaban. 12,4 G por la red para 39 G lógicos
(`speedup 3.14`, el ahorro de `-H`); `/store` queda en 73,7 G de 97,9.

Anotado: el enlace con el worker va a 11 MB/s y no a los 90 de gioser↔caja, porque el LXC vive en el
Proxmox de gioser, fuera de la red de Hetzner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 12:56:50 +00:00
SergioandClaude Opus 5 40002523b2 SDD 28 §6.11: la distro traía un git que no podía clonar — y lo destapó querer ser un hub
`git ls-remote` andaba y `git clone` moría con `invalid index-pack output`, así que parecía un fallo
de red. El informe COMPLETO —no la última línea— decía qué y dónde: sha1dc leyendo un `uint32_t`
desalineado, abortado por el runtime ubsan que `-fsanitize=undefined` en CFLAGS había enlazado.

Arreglado por los dos lados: el sanitizador vuelve a ser sólo de enlace, y
`-DSHA1DC_FORCE_ALIGNED_ACCESS` quita el UB en la fuente. `git` es raíz de `perfil.base`, así que
esto arregla la distro entera y no sólo al hub.

De paso queda anotado que la clave del gitea es `~/.ssh/tawasuyu`, no la de la granja.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 12:50:30 +00:00
SergioandClaude Opus 5 543676e1e9 SDD 28 §6.9: PUERTA 8 CERRADA — los fósiles, decididos uno por uno
Decisión del usuario (2026-09-11): **`terapeuta.ec` y `andino.ec` MUEREN**. Los 279 M de
`/var/www/terapeuta` se van con la caja y no se archivan. Los otros fósiles (aura, sigma, kosmofono,
gitea-redir, api.gioser, mail.sigma) también mueren: ni DNS ni ficheros ni dueño.

`summa`/`api.summa`/`dev.summa` no se mudan porque ya viven en otra máquina — sólo hay que sacar sus
bloques del Caddyfile al apagar gioser.

De las 19 entradas, **13 no se mudan**; la superficie real son 6 sitios y el que pesa es el gitea.

Que quede escrito ES la puerta: el día que se borre gioser, `terapeuta.ec` no se va a poder
recuperar, y la diferencia entre "lo decidimos" y "se nos pasó" es ese párrafo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 12:32:59 +00:00
Sergio 7529b4fde2 estado: cosecha granja 2026-09-11T10:31:58Z — avance del árbol KDE 2026-09-11 10:31:58 +00:00
Sergio e8adb57b77 estado: cosecha granja 2026-09-11T10:02:21Z — avance del árbol KDE 2026-09-11 10:02:21 +00:00
Sergio 71963464d2 estado: cosecha granja 2026-09-11T09:31:41Z — avance del árbol KDE 2026-09-11 09:31:41 +00:00
Sergio 2bea908767 estado: cosecha granja 2026-09-11T09:02:21Z — avance del árbol KDE 2026-09-11 09:02:21 +00:00
Sergio 25f2c7e75c estado: cosecha granja 2026-09-11T07:01:57Z — avance del árbol KDE 2026-09-11 07:01:57 +00:00
Sergio 47edbcd092 estado: cosecha granja 2026-09-11T06:01:47Z — avance del árbol KDE 2026-09-11 06:01:47 +00:00
Sergio 4d55ff8240 estado: cosecha granja 2026-09-11T05:31:52Z — avance del árbol KDE 2026-09-11 05:31:53 +00:00
Sergio 451cb198f6 estado: cosecha granja 2026-09-11T03:31:56Z — avance del árbol KDE 2026-09-11 03:31:56 +00:00
SergioandClaude Opus 5 bcd1cdefaf SDD 26 §6.7: el motor de inferencia local, y la corrección al propio documento
La tabla del §6 le ponía costo «bajo» a la IA local porque `rimay`/`iniy` parecían alcanzar. Se
miró antes de empezar y no alcanzan: los backends de `pluma-llm` son todos de nube, y
`rimay-verbo-fastembed` descarga onnxruntime (glibc) y el modelo de HuggingFace en el primer
arranque. Queda escrito, porque es lo que evita que alguien vuelva a estimarlo en «bajo».

Y de paso junta dos filas que el plan llevaba separadas: el §6.7 y la mitad semántica del §6.3 no
eran dos problemas — era que el corpus no tenía con qué correr un modelo. Un solo muro.

Queda documentado el hallazgo caro, con las dos líneas de cmake enfrentadas: SOURCE_DATE_EPOCH
—que exportamos para que los builds REPRODUZCAN— apagaba INS_ENB y con él las seis perillas de ISA
de ggml, sellando un motor de inferencia con CERO instrucciones vectoriales que corría igual.
Tabla 0/0/0 vs 42.356/1.298/86, medida sobre artefactos sellados.

Y las dos trampas del arnés que van a volver cuando se pinee el modelo de producción: `/health`
contesta 503 mientras carga, y un GGUF sin tipo de pooling hace que el endpoint compatible con
OpenAI conteste 400 — un error que parece del cliente.

⚠ Escrito también lo que esto NO es: el motor no es la función. Falta el modelo (fuente pineada,
como el perfil de PGO), los verbos del host y quién levanta el servidor. Y `llama-cpp` no se
declara en ningún perfil todavía: una imagen no crece 199 M por una función que aún no existe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqBhowvFe3aiieGCKgvxwa
2026-09-11 03:05:20 +00:00
SergioandClaude Opus 5 fb8bbba0b9 SDD 28 §6.10: el respaldo desde otro hub destapó CINCO bloqueos, uno de ellos destructivo
El más grave: el paso del repo va con `--delete`, y el `/opt/takana` de la caja llegó por
`rsync --exclude=.git`. Una corrida real desde ahí habría BORRADO `.git` del Storage Box — el
historial entero — en silencio, porque rsync haría exactamente lo que se le pidió.

La regla que sale y que vale más allá de este script: **un `--delete` convierte «respaldar» en
«sincronizar», y sincronizar desde un origen incompleto no sube menos: BORRA.** Cualquier respaldo
con `--delete` necesita una guarda de completitud del ORIGEN, no sólo del destino.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 03:05:12 +00:00
Sergio 9bbd2e42c9 estado: cosecha granja 2026-09-11T03:02:03Z — avance del árbol KDE 2026-09-11 03:02:03 +00:00
SergioandClaude Opus 5 52144e040f SDD 28 §6.9-6.10: los 19 dominios sondeados uno por uno, y el respaldo corriendo desde la caja
**Puerta 8 (fósiles), sondeada por DNS Y por HTTP**: de las 19 entradas del Caddyfile, gioser sólo
sirve **6** de verdad (las dos landings, el gitea x2, sergio x2). Las otras 13 NO hay que mudarlas:
8 no tienen DNS, 2 dan 502, y **3 ya viven en otra máquina** (`summa`, `api.summa`, `dev.summa` →
154.197.1.2) aunque el bloque de Caddy siga en gioser. O sea que la superficie real a mudar son 6
sitios, y el que pesa es el gitea porque aloja el `origin` de takana.

⚠ `terapeuta.ec` es el único fósil CON DATOS (279 M en /var/www) y sin DNS: hay que decidir
explícitamente si se archiva o muere con la caja.

**Puerta 7**: la caja alcanza el Storage Box y refresca el manifiesto — 3140 artefactos, con 2
VACÍOS detectados y excluidos (`gnome-desktop` ×2). Para llegar ahí hubo que arreglar tres bloqueos
de la misma familia —*el instrumental asume que el hub es gioser*—: la raíz cableada, el binario de
desarrollo, y el rsync sin zstd que hacía que el respaldo no se ejecutara en absoluto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 03:00:54 +00:00
SergioandClaude Opus 5 6d90323fb7 SDD 28 §6.8: puerta 4 — el camino de la granja probado de punta a punta; falta disco, no ingeniería
Desde la caja, sin tocar el latido de gioser: llevada la clave de la granja y el `.fleet` (los dos
fuera de git a propósito), la caja alcanza al worker `PruebasIA`, `estado-granja.sh` corre allá y lee
bien el avance KDE, y —lo que cierra el lazo— se cosechó un artefacto REAL del worker
(`speedtest-go`, 12 M) con el transporte de la granja: llegó con su `.hammer/recipe.toml` y **su
binario CORRE en la caja**. Worker construye → hub cosecha → binario ejecuta.

Lo que falta es CAPACIDAD: 206 artefactos del worker que la caja no tiene, ~23 G a la media del
corpus, contra 3,7 G libres (94 % de ocupación). Forzarlo repetiría el llenado que dejó 1367
artefactos vacíos.

Las tres salidas quedan escritas: enganchar `harkaq-cosecha` (= el cutover, gioser deja de construir
en ese instante y hoy está moliendo KDE), un volumen nuevo para la caja (~€5/mes, no interrumpe
nada), o podar (deshace la mudanza). Es decisión del usuario, no de ingeniería.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 02:45:09 +00:00
Sergio 953706909d estado: cosecha granja 2026-09-11T02:32:11Z — avance del árbol KDE 2026-09-11 02:32:11 +00:00
SergioandClaude Opus 5 9a62f452d3 SDD 28 §6.7: PUERTA 3 EN VERDE — los dos grafos idénticos byte a byte
gioser: 7435d33d62577e8ce154e8c5e354e0465139cfbc632afe710076ea2223cea605
    caja:   7435d33d62577e8ce154e8c5e354e0465139cfbc632afe710076ea2223cea605

`base 61/61 · cli 84/84 · mirada 41/41 · servidor 96/96 · falta 0`, `wanted 3` (chrony, cronie,
logrotate: la deuda declarada), grafo CIERRA, topo-sort OK, en las dos máquinas. Es la prueba de que
un segundo hub está bien montado — no que "funcione".

Costó tres intentos y los dos fallos son la parte que vale:

1. **`unhashable 875`**, todas, con el lab bien y `takana hash` andando a mano. Los scripts asumen un
   árbol de DESARROLLO (`HAMMER = ROOT/target/release/takana`); en una caja instalada el binario es
   `/usr/bin/takana` y `target/` ni existe. No falla ruidosamente: sale el corpus entero como
   `unhashable`, que se lee como "este corpus no se puede hashear".
2. **Dos nodos divergentes** (`aichat`, `zola`): `sealed` en gioser, `never` en la caja. No era deriva
   ni pérdida — **tampoco están en disco en gioser**: son los 2 sellados avalados por los MANIFIESTOS
   (`work/farm-sellados.txt`, `work/respaldo-sellados.txt`). Y `work/` está gitignored, así que un hub
   recién sembrado no los tiene y degrada esos nodos en silencio.

Misma familia que `.fleet`: un fichero fuera de git del que depende una medición compartida.
**Sembrar un hub no es clonar el repo: es repo + store + lab + manifiestos.**

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 02:28:19 +00:00
SergioandClaude Opus 5 0fd87745c6 SDD 28 §6.6: el lab es parte de la IDENTIDAD, y copiar un store con rsync -a lo revienta
**Un hub no es repo+store: es repo+store+LAB.** Con python3 ya vivo, `takana hash` en la caja seguía
fallando por el apk db del lab: el toolchain entra en el ArtifactHash, así que una máquina sin lab no
puede ni preguntar cuál es el hash vigente de una receta — no puede computar el grafo. Sembrado el
lab, el hash testigo coincide byte a byte en las dos máquinas (`zlib` → `b3:dc363f26…`).

**Y la primera copia del store murió con el disco lleno, dejando 1367 de 1368 artefactos VACÍOS.**
La puerta 2 corrida EN EL DESTINO lo cazó en el acto — para eso está.

La causa, medida sobre el store de gioser:

    du -sh                  60 G   (hardlinks contados UNA vez)
    du -sh --count-links    85 G   (hardlinks EXPANDIDOS)
    .dmerge                 11 G   (la caché que hardlinkea al store)

`rsync` sin `-H` NO preserva hardlinks: los expande en copias enteras. O sea que «el store son 60 G»
—lo que dice `du` y lo que uno planifica— son 85 G al copiarlo, más lo que `.dmerge` expanda. La
partición de 68,7 G no tenía ninguna chance.

La copia correcta es `rsync -aH --exclude='.dmerge'`. Y la lección general: **`du -sh` sobre un árbol
con hardlinks no dice cuánto ocupa COPIARLO**; para dimensionar hay que medir con `--count-links`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 02:17:29 +00:00
SergioandClaude Opus 5 9a31857eaa targets: zstd en base — un takana recién instalado no podía desempacar su propio laboratorio
La distro comprime TODO con zstd: la imagen del lab (`lab-image.tar.zst`), el respaldo al Storage
Box, el `dd` remoto de una instalación. Y la imagen no traía el binario.

Medido sembrando el lab en la caja de producción: el `tar` del rootfs es el de busybox y contesta
`tar: unrecognized option: zstd`, así que hubo que extraer por tubería desde el hub
(`zstd -dc … | ssh caja 'tar -xf -'`). Un hub que se instala solo no puede depender de que otro hub
le descomprima las cosas.

La receta ya existía y está sellada (`b3:1ceb6215…`): sólo faltaba declararla.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 02:04:55 +00:00
Sergio 7bdb781352 estado: cosecha granja 2026-09-11T02:01:57Z — avance del árbol KDE 2026-09-11 02:01:57 +00:00
SergioandClaude Opus 5 7e41ffd0e6 SDD 28 §6.5: el muro derribado — de 23 binarios inertes a 1, y sin mover un ArtifactHash
Elegidas las salidas 1 y 2 del §6.4; hecha la 1, que resultó más barata de lo que parecía y desbloquea
la 2 en vez de competir con ella.

`musl-shared` publica `/usr/lib/libc.so` + `/lib/ld-musl-x86_64.so.1`. Como variante y no tocando la
canónica: `musl` es Stage 1 y su `of_tree` es el baseline del selfhost. Control: `takana hash
recipes/musl.toml` sigue en `b3:ce952f72…`. Y `musl` no es dep de nadie ⇒ cero re-hasheo del corpus.

En la caja: `sh: python3: not found` → `Python 3.12.10`. De 23 inertes quedó **1**, `sqlite3`, que
pedía `libz.so.1`; `zlib-shared` ya estaba sellado y sólo faltaba declararla en `base`.

La salida 2 (python/perl estáticos) sigue viva pero deja de ser urgente: con el cargador publicado el
sistema ya no cuelga del rootfs del lab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 02:00:51 +00:00
SergioandClaude Opus 5 7e86459738 musl-shared: el corpus publica su propio CARGADOR — 23 binarios dejan de ser inertes
La imagen del perfil servidor traía 23 binarios de 726 que no podían ejecutarse:

    $ python3 -c "print(1)"
    sh: python3: not found        <- y `command -v python3` decía /usr/bin/python3

No faltaban: eran ELF dinámicos pidiendo `/lib/ld-musl-x86_64.so.1`, y **ningún artefacto del corpus
publicaba ese fichero**. Funcionaban en el hub sólo porque el rootfs Alpine del LAB lo presta — la
misma fuga que documentan `zlib-shared` y `expat-shared`, un piso más abajo, y peor: el lab no entra
en `hash_inputs`, así que la dependencia era invisible para el store.

Los 23 son la suite binutils entera + perl, python3, sqlite3, flex y nft. Entre ellos, TODO el
instrumental del proyecto, que es Python.

**Variante y no `--enable-shared` en la canónica**: `musl` es componente de Stage 1 y su `of_tree` es
el baseline de `selfhost-verify`. Re-hashearlo obliga a rehacer ese baseline a propósito, y eso es su
propia unidad de trabajo. Con el patrón `*-shared` (ya hay 23 en el corpus) la canónica no se mueve —
control: `takana hash recipes/musl.toml` sigue dando `b3:ce952f72…`, el mismo sellado de Stage 1. Y
además `musl` no es dep de NADIE (medido: sólo de sí misma; el enlace estático lo resuelve el musl de
zig), así que esto suma sin mover un solo ArtifactHash del corpus.

**⚠ `compiler = "gcc"` y no `zig-cc`, medido.** Con zig cc el `libc.so` sale con 1586 símbolos
dinámicos contra los 1654 de Alpine, y los 68 que faltan son EXACTAMENTE los que musl implementa en
ensamblador x86_64 (`memset memcpy memmove memcmp strlen` y toda la familia matemática) más los
`__stack_chk_*`. No es que no se compilen —en el `libc.a` canónico están—: zig los resuelve con su
propio musl y los deja `FUNC LOCAL HIDDEN` de tamaño 0, fuera de la tabla dinámica. El síntoma es un
cargador que arranca, reloca y muere con `memset: symbol not found`, que se lee como "el binario está
roto" y no como "a la libc le faltan símbolos". Con gcc: **1651 símbolos**, `memset` y `ceil`
presentes, y `python3 3.12.10` CORRE con el cargador del corpus.

De paso: el comentario de `musl.toml` decía que el artefacto aportaba «el loader, para uso dinámico
futuro». Era falso —`--disable-shared` no construye ninguno— y es justo la etiqueta que hizo que
nadie buscara el agujero. Corregido (los comentarios no entran en `hash_inputs`; verificado).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 01:47:54 +00:00
Sergio ef6164861e estado: cosecha granja 2026-09-11T01:32:07Z — avance del árbol KDE 2026-09-11 01:32:07 +00:00
Sergio 7039afdccf estado: cosecha granja 2026-09-11T01:02:16Z — avance del árbol KDE 2026-09-11 01:02:16 +00:00
Sergio 38338913eb estado: cosecha granja 2026-09-10T23:31:52Z — avance del árbol KDE 2026-09-10 23:31:52 +00:00
Sergio 2f930322cc estado: nftables verificada — REPRODUCE (el sello de tiempo lo neutraliza el parche) 2026-09-10 23:19:08 +00:00
Sergio 7c4617aeb2 SDD 26: fila 10 del plan — el foco (6.5) con su mitad medida y lo que falta nombrado 2026-09-10 23:18:22 +00:00
Sergio 10c499777e atuq §6.5.bis: la extensión foco — muestra el foco del sistema y no tiene con qué apagarlo
La mitad del navegador del §6.5, deliberadamente asimétrica: sólo lee. Pregunta `focus.state` (nuevo
en puriy-costura, commit 38815b5f3) y pinta tres estados — `foco`, nada, y `?` cuando nadie escribió
el estado. Ese tercero es el que importa: si «no sé» se redondeara a «apagado», la insignia afirmaría
que no hay foco sin haberlo mirado.

No hay verbo para apagarlo, y es la propiedad y no un pendiente: si el navegador pudiera levantar el
foco, valdría lo que vale un bloqueador de extensión. En tawasuyu hay un test que lo fija; acá el
guardián mide el EFECTO — tras una sesión entera, el fichero de estado quedó igual.

`scripts/test-atuq-foco.py` corre los tres estados sobre el path de PRODUCCIÓN (`/etc/takana/focus`),
no la escotilla de pruebas: una escotilla mide el código, no el contrato con la imagen. Verde sobre
el artefacto vigente (atuq 5d1afc50, puriy-costura 0de6b4ca), y verificado rompiéndolo — con una
extensión parcheada que pinta «foco» siempre (en una COPIA del artefacto, vía ATUQ_DIR), falla
nombrando la insignia.
2026-09-10 23:17:32 +00:00
SergioandClaude Opus 5 be368df070 SDD 28 §6.3-6.4: puerta 2 en verde, el disco arreglado, y la puerta 3 choca con un muro estructural
**Puerta 2 **: 1362 artefactos reales, 0 vacíos. (El primer conteo dio «3 vacíos» y eran
`.mirror-tmp`, `.bootstrap-tmp` y `.divergen` — directorios de trabajo, no artefactos: el chequeo
tiene que acotar por el patrón `<hash>-<nombre>`, no listar el directorio.)

**El disco**: la imagen ocupaba 7 G de 76,3 y los otros 69 eran inalcanzables. Arreglado; `/store`
en la caja pasó de 487 M a **68,7 G**. Con eso el store de 55 G entra en disco local y **la mudanza
ya no depende de desenganchar el volumen de gioser** — que era la parte que paraba la granja.

**Puerta 3 🚧 BLOQUEADA, y no por configuración.** Al ir a computar los grafos:

    $ python3 -c "print(1)"
    sh: python3: not found        <- y `command -v python3` decía /usr/bin/python3

Está y no arranca: es un ELF DINÁMICO que pide `/lib/ld-musl-x86_64.so.1`, y **en la imagen no hay
ningún cargador dinámico**. Tampoco en el store: el artefacto `musl` publica sólo lo estático
(`libc.a`, `crt*.o`, headers) — no hay `libc.so` ni `ld-musl` en NINGÚN artefacto del corpus. Lo que
hace que esto funcione en gioser es el rootfs Alpine del LAB.

Contado sobre la imagen: **23 binarios inertes de 726**. La suite binutils entera, más perl, python3,
sqlite3, flex y nft.

Frena la mudanza porque TODO el instrumental del proyecto es Python (build-state, targets, yupana,
hydrate-profile, drenar, triaje): un servidor takana no puede correr ni una de sus herramientas.

Las tres salidas —publicar el cargador desde musl (toca Stage 1 y mueve el baseline del selfhost),
python/perl estáticos (frente propio), o aceptar que el instrumental vive en el hub (contradice
apagar gioser)— son decisión de ADR. Queda escrito, medido, y sin tocar nada de Stage 1 de rebote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 23:04:23 +00:00
Sergio 9ef875005d estado: cosecha granja 2026-09-10T23:01:57Z — avance del árbol KDE 2026-09-10 23:01:58 +00:00
Sergio 212b304b60 atuq §6.5: el foco por cgroup MEDIDO — y corregido el propio documento
La fila de la tabla se leía como «bloquear sitios». El cortafuegos NO sabe de sitios: su política de
egress tiene UNA dimensión, qué cgroup sale, y ninguna de destino (leído en UnidadRed, no supuesto).
El foco que estas piezas dan es «el navegador no sale», con todo lo local andando — promesa más
honesta, además: una lista de dominios se esquiva con un espejo; un cgroup sin egress no.

Medido con nuestro propio nft, en el LXC donde somos root:
  linea-base exit=0 · control exit=0 · foco exit=1
Tres medidas y no una: la línea base porque un harness roto se lee igual que un foco que funciona, y
el control porque un reglaset que niega de más tampoco se distingue. `--broken-rules` abre egress al
cgroup en foco y el guardián falla nombrándolo (sale 1). Verificado en los dos sentidos.

Restricciones reales que salieron de medir: crear un cgroup pide root (EPERM incluso en un userns con
CAP_ALL), y `nft -c` no es sintaxis — resuelve el path del cgroup contra la máquina viva.

⚠ Y lo que rompí en el camino, documentado: `/sys/fs/cgroup` está montado SHARED, así que desmontar
la copia de un --rbind se propaga al montaje real. Dejé al worker sin cgroup2 dos veces, en silencio.
Remontado y jerarquía intacta. Ahora todo va en un mount namespace propio (--propagation private),
sin `umount -l` antes de un rm -rf, y con /proc/mounts consultado antes de borrar.

Tres falsos positivos más que cazó el harness, todos con cara de éxito: un gmp viejo tomado por
`ls store/*-gmp` (sin .so ⇒ nada conectaba, y el lab del hub lo tapaba), /sbin fuera del PATH del
chroot (sin `ip`, loopback caído), y un COMENTARIO que se ejecutó — backticks dentro de un heredoc
sin comillas corrieron `ip`/`ifconfig` en el host y pegaron su salida en el guión generado.
2026-09-10 22:55:10 +00:00
Sergio 6704ebe033 estado: cosecha granja 2026-09-10T22:32:06Z — avance del árbol KDE 2026-09-10 22:32:06 +00:00
Sergio e063fc16e9 estado: cosecha granja 2026-09-10T22:01:45Z — avance del árbol KDE 2026-09-10 22:01:45 +00:00
SergioandClaude Opus 5 f4efd8b3e3 SDD 28 §5: la caja sirve su propio repo firmado — y consumirlo choca con la decisión del SDD 27 §4
Cerrado: clave de release estable, 88/88 del perfil publicados con hash anclado, servidos por la
caja, y el control de firma en los dos sentidos (confianza buena ⇒ instala; confianza vacía ⇒
aborta).

El lazo destapó dos cosas que sólo se ven cerrándolo:

- **`strip_debug` no viajaba en el `.swm`** y ES entrada de hash ⇒ 40 recetas del corpus (18 de las
  88 de este perfil) no se podían instalar. Nadie lo sabía porque nunca se había hecho el viaje
  completo receta → paquete → `install --require-signed`.
- **El loopback nunca se levantaba.** `lo` DOWN, y el síntoma era un timeout de 30 s con caddy
  escuchando: un cuelgue que parece del servidor, no un "connection refused".

Y deja el límite escrito, que es lo que más vale: **la caja sirve, verifica y descarga, pero NO
puede reproducir**. `install` construye desde fuente y eso exige el LAB ENTERO en el cliente — no es
que falte un compilador: el lab entra en el ArtifactHash, así que sin él no se puede ni calcular el
hash a comparar. Es el SDD 27 §4 visto desde el otro lado: para cualquiera que no sea un hub de
build, reproducir no es una opción, y el default tiene que ser hidratar artefactos firmados.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 21:44:46 +00:00
Sergio 5beb3aa461 estado: cosecha granja 2026-09-10T21:31:53Z — avance del árbol KDE 2026-09-10 21:31:53 +00:00
Sergio b98c46b530 atuq §6.9: el guardián del torrent dejaba un daemon suelto y decía ✓
Encontrado con `ps` sobre la máquina, no por un test: al cerrar el sandbox quedaban
vivos el daemon y su `bwrap` interno, sosteniendo overlays ya borrados. Es correcto que
el daemon se quede —su torrent de prueba no tiene enjambre, así que nunca termina y
nunca está «sin nada»—, lo que faltaba era despedirlo y MEDIR que se fue.

- el guión de adentro le pide `puriy-costura-torrent stop` (el verbo del producto);
- la huella del socket se anota adentro y ANTES del stop: el daemon lo borra al irse;
- el guardián censa daemons antes/después con `ps -C` (nunca `pkill -f`) y falla
  nombrando el PID; el `finally` barre lo propio para que un test rojo no deje basura;
- tope de 180 s al sandbox, como seguro contra un cuelgue.

Comprobado en los dos sentidos con una copia rota fuera del repo: sin el `stop` el
guardián daba ✓ igual, y con la aserción nueva sale ✗ nombrando el PID. La hipótesis
de que se COLGARÍA era falsa: el `bwrap` externo vuelve; el que espera es el interno.

Verde: positivo, control negativo y rotura a propósito.
2026-09-10 21:29:28 +00:00
Sergio 78f1c6a771 atuq: re-pin del daemon con el arreglo del inmortal, y la lección en el SDD
El §6.9 apunta ahora a tawasuyu `3bd1440d` (`b3:27c7b73a`), que arregla un daemon que
quedaba VIVO PARA SIEMPRE cuando su sesión de torrent no arrancaba: el bucle hacía
`let Ok(g) = gestor() else { continue }` y el `continue` saltaba la evaluación de
«¿sobro?».

Y lo que queda escrito en el §6.9 es CÓMO se encontró, porque es lo reutilizable: no lo
encontró ningún test sino mirar `ps` después de las pruebas —un daemon con 23 minutos y
`idle_seconds = 300`—. Los tests cubrían la decisión como función PURA, y esa función
estaba bien: el que no llegaba a llamarla era el bucle. **Una decisión correcta que
nadie toma se ve igual que una que no existe.**

Los dos guardianes vuelven a pasar sobre los artefactos vigentes (`atuq b3:d1a444ad`,
`puriy-costura b3:cad5c855`, `puriy-costura-torrent b3:27c7b73a`): el positivo con
`TOMADO prueba-atuq.bin` y el socket que nadie arrancó, y el control negativo nombrando
lo que falta.
2026-09-10 21:07:30 +00:00
Sergio 3ff7585aa2 estado: cosecha granja 2026-09-10T21:02:45Z — avance del árbol KDE 2026-09-10 21:02:45 +00:00
Sergio c214438f00 atuq: el torrent lo toma un daemon propio, perezoso, que sobrevive al navegador
El 6.9 del SDD 26, con la arquitectura que pidió el operador: daemon PROPIO,
CONFIGURABLE y PEREZOSO. Vivaldi ya trae torrent y termina en una carpeta; acá lo que
baja entra al CAS —un objeto BLAKE3 con la misma identidad que una descarga del
navegador (§6.2) o un `.swm`—, y ésa es la razón por la que esto vale.

    página de la extensión → fondo → connectNative → puriy-costura
      → /usr/bin/puriy-costura-torrent add   (que LEVANTA el daemon si no está)
      → daemon: librqbit, y al completarse, el CAS

POR QUÉ NO ES UN VERBO MÁS DEL HOST — dos razones, y la primera decide:
1. una descarga tiene que sobrevivir al navegador, y Gecko mata al host cuando se
   cierra el puerto (medido hoy);
2. librqbit + tokio + rustls son 226 crates: dentro del host, ese binario —952 K,
   compartido por CINCO extensiones— pasaría a ~20 MB y cada iteración de cualquier
   función del navegador a un cuarto de hora.

⚠ Que esa pila COMPILA para musl con zig-cc se midió ANTES de decidir, con una receta
desechable: 226 crates y sella. Era una incógnita real —ninguna receta del corpus
había construido tokio+rustls desde tawasuyu— así que la decisión no fue «no se
puede», fue «no ahí». El artefacto del daemon pesa 7,7 M.

PEREZOSO EN LOS DOS SENTIDOS: no hay servicio en la imagen (el binario está y no corre
hasta que hay un torrent; lo levanta su cliente con `setsid`, sin lo cual moriría con
el navegador) y se va solo tras `inactividad_seg` sin nada activo — sembrar cuenta como
actividad.

CONFIGURABLE: TOML opcional; sin fichero anda, y uno ROTO es error. El CAS por defecto
es el del host, con un test que falla si esas raíces se separan.

LO QUE EL GUARDIÁN NO HACE, Y POR QUÉ (está en su cabecera y en el §6.9):
· no usa un `magnet:` sino un `.torrent` servido por HTTP —dar de alta un magnet
  BLOQUEA esperando metadata que sin peers no llega: se mediría un timeout y no la
  cadena—. El `.torrent` se arma en el propio guardián, en bencode a mano, porque un
  binario de prueba en el repo es una dependencia que nadie revisa;
· no pasa por el despacho de protocolos de Gecko: un clic en `magnet:` abre el diálogo
  de «¿con qué lo abro?», que en headless no contesta nadie, y eso es una elección del
  usuario y no código nuestro. Se abre la página de la extensión —la misma que el
  handler abriría— descubriendo su URL base del `dump` de una primera corrida, porque
  el UUID lo asigna Gecko por perfil y adivinarlo sería inventar.

El control negativo saca el cliente del daemon de la imagen y exige que el host lo diga
NOMBRANDO lo que falta, en vez de fingir que lo tomó.

⚠ Y sigue sin haber test automático de un transfer REAL entre peers: haría falta un
sembrador y una espera que volverían la suite una que nadie corre. Dicho en el test del
daemon y en el §6.9, para que nadie lo lea como «probado de punta a punta».

La página de la extensión NO valida la forma del origen: la valida el host, que es
quien decide qué le pasa al daemon. Tener esa regla escrita dos veces es tenerla
mintiendo el día que una cambie.

Del §6 quedan el foco (6.5) y la IA local (6.7), ésta bloqueada por algo medido: el
corpus no tiene ninguna receta de LLM ni de embeddings.

MEDIDO sobre `atuq b3:d1a444ad`, `puriy-costura b3:cad5c855` y
`puriy-costura-torrent b3:76afb7a8` (7,8 M):

    MAGNET http://…/prueba.torrent
    TOMADO prueba-atuq.bin · nuevo · en /salida/hogar/Downloads · id=0
    socket del daemon: run/puriy-costura-torrent.sock      ← nadie lo arrancó

⚠ Y DOS COSAS QUE EL GUARDIÁN DESTAPÓ, las dos por comprobar el NOMBRE y no sólo que
la respuesta llegara:

1. **la extensión leía claves que el daemon no manda** (`name`/`files`/`bytes` contra
   `nombre`/`carpeta`), y el síntoma era «TOMADO — 0 fichero(s), 0 bytes»: parecía un
   torrent vacío y era un vocabulario inventado del lado del lector — la misma trampa
   que había evitado en el host y no acá;
2. mirándolo apareció que **el socket y la config del daemon estaban en castellano**, y
   la regla 7 de tawasuyu nombra explícitamente las claves de configuración entre lo
   que se tipea. Corregido allá antes de que llegara a ninguna imagen: un protocolo y
   un fichero de config son contrato, y renombrarlos después rompe lo que alguien ya
   escribió.

tawasuyu: 4f36f057 (el crate) · 3f69aab2 (lock) · 3b56db26 (el verbo del host) ·
066b3761 (el log del daemon, que faltaba y hacía invisible su único fallo de arranque)
· ca9947b9 (las claves en inglés).
2026-09-10 20:45:53 +00:00