Commit Graph
1444 Commits
Author SHA1 Message Date
Sergio 6fa955f297 estado: cosecha granja 2026-09-11T20:32:34Z — avance del árbol KDE 2026-09-11 20:32:34 +00:00
SergioandClaude Opus 5 61c33800c5 traducir: lector de OpenRC — el par que cierra el camino de gioser, y casi la mitad NO se puede leer
`openrc → arje` sin escribir ningún traductor de ese par: cuarto plugin, cuarto par. OpenRC es el
init de gioser, la máquina que esta mudanza termina borrando.

**El hecho que manda acá: un servicio de OpenRC es un PROGRAMA, no una declaración.** Un unit de
systemd se lee; un script de OpenRC puede hacer cualquier cosa antes de arrancar nada. Medido sobre
el `/etc/init.d` real: **60 de 121 definen su propia `start()`/`stop()`**. En ésos no hay `command=`
que valga — leer las variables y emitir tarjeta daría un servicio que arranca OTRA COSA. Regla al
revés que en systemd: `start()` propia ⇒ SIN-TRADUCIR y NO se emite tarjeta.

Los números del corpus real cierran solos: 121 − 1 (no es openrc-run) − 60 (start propia) − 1 (sin
`command=`) = 59, y el lector emitió exactamente 59, con 59 ids únicos.

Dos cosas que OpenRC obliga a ir a buscar fuera del script: `/etc/conf.d/<x>`, donde viven los
argumentos de verdad (a diferencia del `EnvironmentFile=` de systemd, éste SÍ está en la máquina: se
lee y se aplica), y `/etc/runlevels/`, que es lo único que dice si el servicio arranca solo.

**Tres defectos que sólo aparecieron corriendo contra las 121 de verdad**, no sobre un ejemplo mío:

- `name=` no es un identificador sino un rótulo humano: en gioser vale «Aura Backend», con espacio,
  y se iba al id de la tarjeta y al path del cgroup. El identificador es el nombre del fichero.
- Ids repetidos: `/etc/init.d` guarda copias `*.bak-FECHA` junto a los servicios vivos y son scripts
  válidos; dos con el mismo nombre dan el MISMO id determinista ⇒ semilla con dos cards homónimas.
  El escritor lo detecta y no emite la segunda.
- La tarjeta decía `"desde": "systemd"` viniendo de OpenRC: el campo que existe para saber de dónde
  salió algo era justo el que mentía. Estaba cableado.

Y el centro deja de filtrar la entrada por extensión: los servicios de OpenRC no tienen ninguna, y
filtrar en el centro es que lo que no entra se pierda EN SILENCIO. Filtra el lector, que sabe, y lo
anota — así un `.bak` en `/etc/init.d` se REPORTA en vez de desaparecer.

Controles: dos corridas dan el fichero byte a byte idéntico; los tres pares previos sin regresión
(`nginx → caddy` sigue dando `Valid configuration`, `systemd → arje` sigue emitiendo sus 3 tarjetas).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 20:30:20 +00:00
Sergio e1bd9198c6 estado: cosecha granja 2026-09-11T20:02:21Z — avance del árbol KDE 2026-09-11 20:02:21 +00:00
Sergio 0f8dff1c62 estado: cosecha granja 2026-09-11T19:32:11Z — avance del árbol KDE 2026-09-11 19:32:11 +00:00
Sergio a14b62436c atuq §6.7.bis: la barra lateral pregunta, y contesta un modelo de ESTA máquina
Inferencia REAL de punta a punta, en una jaula sin red: panel → fondo → host → `llama-server` (el
llama-cpp del corpus) → respuesta. El servidor lo levanta el host en la primera pregunta —no hay
servicio de IA en la imagen— y **se muere con el navegador**, al revés que el daemon del torrent
(§6.9): un torrent tiene trabajo que sobrevive; medio giga de modelo en RAM, no.

El puerto nativo vive en el FONDO y no en la página: si viviera en la página, cerrar la barra lateral
se llevaría el host y el modelo cargado con él.

`scripts/test-atuq-ia.py` mide tres cosas: que la respuesta salga del modelo de la imagen, que no
venga VACÍA, y que al cerrarse el navegador queden cero `llama-server`. Control negativo: sin modelo
aparece la causa y NO hay respuesta inventada.

⚠ Ese censo nació roto y del género que este repo colecciona: `pgrep -c` no existe en busybox y el
`|| echo 0` convertía el error en un cero — o sea en un verde. Con un motor vivo a propósito el
guardián pasaba igual. Ahora cuenta leyendo /proc y la rotura falla como debe. Y el PRIMER intento de
romperlo tampoco rompía nada: el motor de mentira moría al hacer bind porque el socket estaba en
/salida, compartido entre las dos corridas.

Queda UNA decisión, no trabajo: qué modelo se pinea (tamaño de imagen y licencia). Hasta entonces la
imagen no trae modelo y el panel lo dice con todas las letras.
2026-09-11 19:24:33 +00:00
SergioandClaude Opus 5 fefa92ec5a traducir: familia «servicio» — systemd → tarjeta de arje, y el pivote pasa a ser uno POR FAMILIA
Un unit de systemd no es un sitio web. Meterlo en `Sitio`/`Ruta` sería justo el «parecerse» que el
acta existe para evitar, así que cada familia tiene su modelo y el centro empareja SÓLO dentro de la
familia. Un par cruzado se rechaza con un error, no con un intento: `systemd → caddy` sale por
`sys.exit`, porque traducir entre familias daría un fichero que PARECE correcto.

Dos familias hoy: `web` (nginx, apache → caddy) y `servicio` (systemd → arje). 3 pares con 5 plugins.

**Dónde una elisión silenciosa no pierde una opción sino que CAMBIA el sistema.** El payload `Native`
de arje acepta `exec`, `argv` y `envp` y nada más (comprobado en la semilla real del producto): NO
hay campo de usuario. Un `User=git` traducido en silencio correría el servicio COMO ROOT — escalación
de privilegios en un fichero generado que nadie vuelve a leer. Sale SIN-TRADUCIR con su línea. Igual
`EnvironmentFile=` (el fichero no está en la máquina donde se traduce ⇒ envp incompleto, falla
tarde), `Type=forking` (arje supervisa al proceso que lanza ⇒ BUCLE DE REINICIO) y todo el
endurecimiento, que callado entrega un servicio MENOS confinado que el original.

Lo que sí tiene destino exacto: `Type=oneshot` → `"supervision": "OneShot"`, que ya existe en la
semilla del producto — buscarlo antes de declararlo intraducible evitó una elisión inventada.

**Lector y escritor no opinan del mismo campo.** `User=` lo CAPTURA el lector y lo JUZGA el escritor;
cuando los dos anotaban, el acta decía dos cosas distintas de la misma línea, y un acta que se
contradice se deja de leer. El lector registra en qué línea vio cada campo (`Servicio.lineas`) para
que el escritor señale la línea real en vez de un 0.

Controles: la tarjeta generada tiene el MISMO juego de claves que la tarjeta real de `sshd` del
producto (+`_mudanza` de procedencia); dos corridas dan el fichero byte a byte idéntico; y la familia
`web` sigue validando con el caddy del corpus (`Valid configuration` en los dos pares).

De paso, `ulid_determinista` sale a `ids.py`: lo necesitaban `declarar.py` y el escritor de arje, y
copiarlo habría dejado dos generadores de id que se pueden separar sin que nada falle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 19:15:21 +00:00
SergioandClaude Opus 5 822d0b8949 ADR 0017 y 0018: ciclo de vida del kernel y soberanía del arranque
Dos preguntas de diseño convertidas en decisiones con su evidencia.

0017 — el kernel es artefacto de contenido, así que el rollback ya es
gratis y atómico: lo que falta es A/B con vuelta atrás automática.
Medido sobre los 6 .config SELLADOS (no sobre las recetas): KEXEC=y en
los 6 (la primitiva está pagada, gratis), LIVEPATCH en NINGUNO —lo que
hay es HAVE_LIVEPATCH, que es capacidad de arch, no el feature— y
MODULES sólo en linux. Un livepatch ES un .ko ⇒ en los 5 kernels que
arrancan máquinas está cerrado por construcción.

0018 — la NVRAM es estado mutable compartido y Windows es otro agente
escribiendo sin coordinarse: el mismo patrón del índice de git (regla 2)
y del árbol de fuentes (ADR 0012) ⇒ reconciliación, no confianza.
Medido: efibootmgr aparece 0 veces en el repo y takana NUNCA crea una
entrada NVRAM — depende entera de la ruta EFI/BOOT/BOOTX64.EFI, que por
especificación es la de medios REMOVIBLES. Correcto para el USB, la peor
dirección posible para un disco compartido con Windows.

Se atan por KEXEC_FILE: no está en ninguno de los 6, y lockdown (que
trae Secure Boot) prohíbe el kexec_load viejo ⇒ hoy kexec y Secure Boot
no coexisten.

Abiertas a propósito: si el perfil servidor quiere livepatch (variante
con MODULES, nunca un flag en el kernel general), y si takana firma.
"No firmar" no es neutral: le traslada al usuario la clave de
recuperación de BitLocker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 19:08:08 +00:00
Sergio 2a836e5224 estado: cosecha granja 2026-09-11T19:05:04Z — avance del árbol KDE 2026-09-11 19:05:04 +00:00
Sergio 4b9f4acd3d estado: cosecha granja 2026-09-11T19:02:19Z — avance del árbol KDE 2026-09-11 19:02:20 +00:00
Sergio 78e0c126cb protoc: el catálogo tenía el PLUGIN y no el compilador — y abseil y protobuf tienen que compartir runtime de C++
`recipes/protoc-gen-go.toml` estaba SELLADO y era INERTE: un plugin de protoc no hace nada sin
`protoc`, y `protoc` no estaba en el catálogo. Cualquier servicio gRPC lo necesita en tiempo de
build — empezando por `qdrant`, que el censo del servidor de origen encontró corriendo como binario
suelto, de los que se pierden al apagar la máquina vieja. Entran dos recetas: `abseil-cpp` (que
protobuf exige) y `protobuf`.

Verificado corriéndolo, no por el código de salida: `protoc --version` contesta `libprotoc 36.1`, y
un `.proto` de prueba se compila a `.pb.h`/`.pb.cc` reales. Único NEEDED `libc.so`, que provee
`musl-shared`. Las dos REPRODUCEN bit a bit.

**Dos muros, y el segundo escondía al primero.**

1. **`zig c++` se cae al enlazar los tres plugins `protoc-gen-upb*`**: `Error running link command:
   Segmentation fault` — un crash del linker, no un error de símbolos. Se aisló FUERA de takana y
   fuera del sandbox, rehaciendo el link a mano sobre los mismos `.o` y las mismas `.a`: `zig c++`
   vuelve a segfaultear. No es presión de memoria (corrió solo) y no es el `-Wl,-rpath,::::::::::::::`
   que CMake emite — se probó sin él y cae igual. `protobuf_BUILD_LIBUPB=OFF` tampoco es escapatoria:
   queda `OFF` en el `CMakeCache` y los plugins se construyen igual. Con `g++` y el `ld` de GNU los
   cuatro binarios enlazan.

2. Al pasar **sólo** protobuf a `gcc`, con abseil todavía construida por `zig-cc`, el link murió con
   `undefined reference to std::__1::basic_string<…>::assign(char const*, unsigned long)`. **El `__1`
   es el namespace inline de libc++** —la libstdc++ que trae zig— y g++ usa la de GNU, con otro
   mangling para los mismos tipos. O sea: **abseil y protobuf tienen que compartir runtime de C++ o
   no enlazan**, y el error habla de `std::string` sin nombrar a abseil ni una vez. Es la familia de
   las dos glib estáticas en un proceso, pero en C++ y en tiempo de link.

⇒ Las dos recetas llevan `compiler = "gcc"` y está escrito en ambas que cambiar una sin la otra
vuelve a romper. `-static-libstdc++ -static-libgcc` para que `protoc` no salga pidiendo una soname
que sólo vive en el lab.

Y una decisión que NO es «la última versión»: abseil va pineada a `20250512.1` porque es exactamente
la que protobuf 36.1 declara en `cmake/dependencies.cmake`. Abseil no promete ABI estable entre
releases; el disparador para subirla es que protobuf suba la suya, no que abseil publique.
2026-09-11 18:59:10 +00:00
Sergio 6f94a346a3 estado: cosecha granja 2026-09-11T18:32:23Z — avance del árbol KDE 2026-09-11 18:32:23 +00:00
SergioandClaude Opus 5 3137d47f16 traducir: CENTRO de traducción con modelo pivote y plugins — N+M en vez de N×M
Traducir de a pares no escala: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y
cada formato nuevo agrega 2N. Con un modelo intermedio son N lectores + M escritores: un formato
nuevo cuesta uno o dos plugins y estrena todos los pares de golpe.

**Probado, no argumentado**: el lector de apache se agregó SIN TOCAR el escritor de Caddy, y con eso
`apache → caddy` funciona sin que exista ningún traductor de ese par. Las dos salidas las valida el
caddy del propio corpus: `Valid configuration` en las dos.

Los plugins se DESCUBREN (`formatos/` + `@lector` / `@escritor`): una lista mantenida a mano se
desincroniza y el formato nuevo «no existe» sin que nada falle. `--list` dice qué pares habilita.

**El acta viaja DENTRO del modelo**, y es la decisión que hace que el pivote no mienta: si fuera un
efecto de cada traductor, lo que el LECTOR no entendió se perdería antes de llegar al escritor. El
escritor además puede agregarle — Caddy no tiene equivalente de `location ~`, así que en vez de
emitir un `handle` de prefijo que SE PARECE, lo manda al acta. Parecerse es peor que faltar.

Y un bug del lector de apache, encontrado probando: `ProxyPass` tiene dos formas y dentro de
`<Location>` lleva sólo la URL. Mirar sólo la de tres tokens dejaba sin traducir la forma más común y
el acta la reportaba como «no reconocida» — un falso negativo que manda a escribir a mano algo que el
lector sí sabe hacer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 18:27:32 +00:00
Sergio 030d728fa4 estado: cosecha granja 2026-09-11T18:03:01Z — avance del árbol KDE 2026-09-11 18:03:01 +00:00
SergioandClaude Opus 5 033913bb4a traducir.py: nginx → caddy, con acta — y validar con el binario real cazó un bug que CAMBIABA el sentido
Cambiar de nginx a caddy es reescribir la configuración. `traducir.py` hace la parte mecánica y dice
CON NÚMERO DE LÍNEA lo que no pudo traducir.

Doctrina heredada de `soltar`/`paskaq` (tawasuyu): su dominio es otro —datos presos en formatos
cautivos— pero sus principios son los que hacían falta. **Elisión honesta**: lo que no se pudo
traducir se reporta con su línea y su motivo, porque un traductor que descarta en silencio te deja un
servidor sin una redirección o sin una regla de auth y el sitio parece funcionar. **No adivinar en
silencio**: cada heurística queda como decisión explícita. **Procedencia**: cada bloque dice de qué
línea salió.

⚠ **Validar con el caddy real destapó dos bugs que leer la salida no mostraba:**

1. `ambiguous site definition` — en nginx dos `server` con el mismo nombre se distinguen por su
   `listen`; en Caddy, por el esquema de la dirección.
2. **El grave**: un `if (...) { return 403; }` salía como `respond 403` INCONDICIONAL — el sitio
   entero devolviendo 403. La directiva estaba dentro de un bloque declarado intraducible y se
   absorbía igual al de afuera. Es peor que la pérdida silenciosa: no pierde, CAMBIA el sentido.
   Ahora todo lo que vive en un bloque opaco sale `SIN-TRADUCIR` con su motivo.

Con los dos arreglados: `Valid configuration` según el caddy del propio corpus.

Y es honesto sobre su alcance: traduce el núcleo común y declara el resto. Uno que cubre el 70 % y
dice cuál es el 30 % restante es útil; uno que aparenta cubrir el 100 % es una trampa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 17:46:44 +00:00
Sergio c3d0c339bc estado: cosecha granja 2026-09-11T17:32:16Z — avance del árbol KDE 2026-09-11 17:32:16 +00:00
Sergio a90b4ef626 valkey: la familia «caché en memoria» estaba VACÍA — y el artefacto salió vacío sin que nada fallara
`scripts/mudanza/planear.py` agrupa el software del origen en familias funcionales y contesta, para
cada una, con qué la reemplaza takana. Cruzando esa tabla contra el catálogo, cuatro familias salen
SIN NADA sellado: caché en memoria, contenedores, correo y base vectorial. Ésta cierra la primera.

Valkey y no redis a propósito: redis dejó de ser software libre en 2024 (RSALv2/SSPLv1, que no pasan
la OSI); valkey es el fork de la Linux Foundation desde el último commit BSD, mismo protocolo y mismo
RDB/AOF. Y upstream instala los seis alias `redis-*`, así que un `redis-server` se reemplaza sin
tocar datos ni clientes. Para una distro que publica el catálogo con `license` poblado receta a
receta, meter SSPL sería meter algo que después hay que sacar.

**El hallazgo caro de esta receta no fue compilar: fue que el PRIMER artefacto se selló VACÍO.**
`make install PREFIX=/usr DESTDIR=/out` imprimió sus `INSTALL valkey-server` en verde y salió 0 — y
`/out` quedó sin un solo fichero. La causa está en la línea 65 de `src/Makefile`: valkey define
`INSTALL_BIN=$(PREFIX)/bin` **sin prefijar `$(DESTDIR)`**, o sea que IGNORA `DESTDIR` y copió los
binarios a `/usr/bin` dentro del sandbox, que se tira al terminar. Lo sellado fue un directorio con
`.hammer/recipe.toml` y nada más: un cache-hit permanente que habría contestado «valkey ya está»
para siempre. Es la regla 3 del repo en vivo — *un ausente falla ruidosamente; un vacío llega hasta
el final diciendo que todo fue bien*. Se vio mirando el árbol del artefacto, NO el código de salida.
El arreglo es `PREFIX=/out/usr`.

El otro muro, medido con un control mínimo fuera de valkey: con `zig cc` el link muere con
`undefined symbol: __cpu_model` en los tres binarios. No es valkey — es que `__builtin_cpu_supports()`
(que valkey usa para elegir en runtime las rutas AVX2/AVX-512) emite esa referencia y el
`compiler_rt` de zig 0.16.0 no la trae:

    printf '#include <stdio.h>\nint main(void){__builtin_cpu_init();return printf("%d",__builtin_cpu_supports("avx2"));}\n' > t.c
    zig cc -target x86_64-linux-musl -O2 -o t t.c   ⇒  ld.lld: error: undefined symbol: __cpu_model

De ahí `compiler = "gcc"`, que es la palanca declarativa que el lab expone justo para esto. Arrancarle
el multiversioning a parches habría costado las rutas SIMD de `BITCOUNT`/`PFCOUNT` y habría que
rehacer el parche en cada versión.

Verificado: REPRODUCE bit a bit. Único NEEDED `libc.so`, que provee `musl-shared` — raíz de `base`
desde hoy ⇒ resuelve en cualquier imagen sin declarar nada en la receta.
2026-09-11 17:29:41 +00:00
Sergio 9a1139ce5f granja: las cuatro recetas nuevas REPRODUCEN bit a bit
`verificar-repro.sh` sobre popt, logrotate, chrony y cronie: 4 REPRODUCEN, 0 deriva,
0 no-determinismo. Importaba comprobarlo el mismo día y no dentro de tres meses: recién selladas,
el artefacto guardado y el lab son el MISMO, así que una diferencia acá sólo podía ser
no-determinismo — no deriva. Verificarlas más tarde mezcla las dos causas y obliga a la segunda
reconstrucción para desempatar.

Vale la pena decir cuál era el riesgo concreto, porque no era genérico: chrony mete en el binario un
`NTP_ERA_SPLIT` que por default calcula con `date` en tiempo de configure («hace 50 años»). Es
exactamente la familia del sello de tiempo de nftables y del BuildID de waterfox. No hizo falta
parchearlo: upstream ya respeta `SOURCE_DATE_EPOCH`, que el lab exporta — y esto lo confirma.
2026-09-11 17:17:03 +00:00
SergioandClaude Opus 5 68fdd9eb72 mudanza: el perfil — el software del servidor nuevo se DECLARA, no se instala a mano
`declarar.py --perfil-out` emite un `[perfil.<label>]` listo para `targets.toml`, generado desde el
censo: cada raíz está ahí porque un servicio que CORRÍA la necesita y takana tiene receta.

Por qué un perfil y no una lista de `install`: mudar servicio por servicio a una caja viva va contra
el diseño —el software de una máquina se declara y viene en la imagen, reproducible y firmado— y un
servidor armado a fuerza de instalaciones sueltas no se puede volver a construir. Que es justo el
problema que esta mudanza existe para no repetir: el `caddy` de gioser no tiene dueño ni receta y se
pierde con la máquina.

Y hay un impedimento medido, no estético: `takana install` REPRODUCE desde fuente y eso exige el lab
entero en el cliente (SDD 28 §5.4), que una caja de destino no tiene. El perfil lo resuelve por el
lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.

Del censo de gioser salen `caddy`, `gitea`, `python3` (agrupado: lo piden python3 y uvicorn).
Verificado que el instrumental lo acepta: `targets.py gioser-mudado` expande a 33 raíces.

⚠ Y dice lo que NO puede cubrir, uno por uno con su motivo: 34 servicios entre `suelto`,
`paquete-ajeno` y `desconocido`. Un perfil que se calla lo que le falta sale N/N describiendo un
servidor incompleto — la lección de `foot` en escritorio-sway.

Con esto la mudanza produce TRES documentos declarativos que reconstruyen el servidor desde cero:
el perfil (qué software), la semilla (qué corre y cómo) y el plan (qué datos, en qué orden). Ninguno
es un log de lo hecho: los tres son entradas que se vuelven a ejecutar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 17:16:16 +00:00
Sergio cc5aebd8c4 granja: las tres wanted del perfil servidor, construidas — y popt, que faltaba debajo
`targets.toml` declaraba `chrony`, `cronie` y `logrotate` como raíces del perfil `servidor` con el
motivo escrito al lado, y ninguna tenía receta: el grafo las contaba como `wanted` y ésa era toda la
deuda que le quedaba al perfil. Ahora sellan las cuatro (popt es la hoja que `logrotate` exige:
incluye `<popt.h>` en la primera pantalla y su configure aborta sin él).

    servidor  97/97 → 101/101 listo, falta 0.  `wanted` desaparece de los totales.

Lo que se midió, no se supuso:
  · logrotate  estático, CERO NEEDED, corre, y las rutas de gzip quedan en `/bin` — que es donde
    busybox las deja de verdad; el default de upstream en Linux es `/usr/bin/gzip`, que en esta
    distro NO EXISTE y habría fallado en runtime diciendo «no se pudo comprimir».
  · chrony     `+CMDMON +REFCLOCK +RTC +PRIVDROP +IPV6`; `cap_set_proc` está en el ELF, o sea que
    libcap entró de verdad y chronyd suelta privilegios. Sale `-NTS -SECHASH` a propósito: NTS
    necesita nettle o gnutls y ninguna está en el corpus.
  · cronie     los cuatro binarios estáticos sin NEEDED, con inotify dentro, y `/bin/vi` pineado.

**El hilo que recorre las tres recetas es el mismo, y es el que valía la pena escribir:** los tres
`configure` DECIDEN MIRANDO EL LAB. `logrotate` trae `--with-selinux/--with-acl` en `[default=check]`;
`chrony` prueba nettle, gnutls, libcap, seccomp y editline; `cronie` resuelve el editor de
`crontab -e` con `AC_PATH_PROG([vi])` y lo graba en el binario. El lab NO entra en `hash_inputs` ⇒
dos labs distintos sellarían bytes distintos en la MISMA dirección del store y nada lo notaría.
Cada palanca va fijada en la receta —que sí entra en el hash— para que sea una decisión y no un
accidente del entorno.

Y una anotada en vez de tapada: cronie no reemplaza al `crond` de busybox por reflejo (busybox ya lo
pone en `/sbin`); agrega `@reboot`, crontabs por usuario, `/etc/cron.d` y anacron. Cuál va en cada
imagen es decisión de perfil — lo que faltaba era que la opción existiera en el corpus.
2026-09-11 17:13:42 +00:00
SergioandClaude Opus 5 8ad9f0490d planear: alternativas por familia funcional, con una recomendación que no es gusto
Un servicio casi nunca es insustituible. `planear.py` agrupa por familia (servidor web, base de
datos, caché, forja git, contenedores, dns, correo, base vectorial), muestra qué equivalentes tiene
takana —marcando sellado / receta sin sellar / no está— y recomienda.

Pero no «el mejor» en abstracto: un gusto disfrazado de dato es peor que no recomendar. Los criterios
son hechos comprobables, en orden:

1. **El que ya corre, si takana lo construye** — porque cambiar de servidor web no es cambiar un
   binario: es REESCRIBIR la configuración entera, y eso casi siempre pesa más que cualquier ventaja
   teórica del otro.
2. Si el que corre no está en el catálogo pero un equivalente sí, se recomienda ése (takana puede
   construirlo, firmarlo y reproducirlo) diciendo lo que cuesta.
3. Si no hay ninguno, se dice. **No se sugiere «usá otro» cuando ese otro tampoco está.**

Probado contra el catálogo real: `caddy` → se queda (ya corre y está sellado); `nginx` → recomienda
caddy nombrando el costo; `redis` → «recomendado: NINGUNO», porque ni redis ni valkey ni memcached
están en el corpus.

⚠ Y el dato que la recomendación destapa, que vale más que la recomendación: de servidores web el
corpus sólo tiene **caddy y traefik**. No hay nginx ni apache.

Si se elige un equivalente queda en el censo (`alternativa = "…"`) y el plan lo dice en su paso, con
la advertencia arriba de todo: la configuración del origen no sirve tal cual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 17:07:05 +00:00
SergioandClaude Opus 5 6f5cc2629a mudanza: de dónde sale cada binario — y la herramienta corrigió un error mío
«Instalá el paquete» es lo que uno ya sabía. La respuesta útil sale de cruzar dos hechos: quién posee
el fichero en el ORIGEN (el censo se lo pregunta al gestor de paquetes de esa máquina, en un lote) y
si el corpus de takana tiene una receta con ese nombre.

  receta-takana  → instalarlo del repo firmado, NO copiar el binario
  paquete-ajeno  → escribir receta, o qorpa (ADR 0015)
  suelto         → nadie lo provee: llevarlo con su entorno o escribirle receta

Medido sobre gioser (37 servicios vivos): 4 receta-takana · 4 paquete-ajeno · **11 SUELTOS** · 7 sin
binario legible. Los sueltos viven en `/usr/local/bin` o en un home (`qdrant`, `matilda`,
`pacha-secretos`, `act_runner`, `shuma-gateway`) y son los que se pierden al apagar el origen.
Sorpresa medida: `/usr/bin/caddy` tampoco tiene dueño — está puesto a mano.

**Y corrigió un error mío**: en el SDD 28 escribí que `gitea` no tenía receta y que «es la que más
peso tiene». Las dos cosas falsas — `recipes/gitea.toml` existe y está sellada. Lo afirmé de memoria;
el programa fue a mirar. Corregido allá con la nota.

**Y un fallo del clasificador, que vale como regla**: calculaba la raíz del repo con un `dirname` de
menos, no encontraba ninguna receta y contestaba `suelto` A TODO, incluido `caddy`. Un clasificador
que contesta siempre lo mismo no clasifica. Ahora falla ruidosamente si no encuentra el catálogo, en
vez de dar una respuesta falsa con forma de respuesta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 17:02:52 +00:00
SergioandClaude Opus 5 808e4a4660 install-image: UNA imagen que arranca por BIOS **y** por UEFI
«Instalable en Hetzner o en cualquier otro servicio» se rompía en el arranque: Hetzner Cloud arranca
BIOS y otros proveedores sólo ofrecen UEFI. Tener dos imágenes hermanas obliga a elegir por proveedor
y a que diverjan — y ya divergían: la hermana EFI documenta etiquetas `takana-*` que su propio código
no escribe (corregido en este commit; son `hammer-*` y están congeladas a propósito por el ADR 0016,
porque viven en sistemas YA INSTALADOS).

Layout nuevo: `p1` BIOS-boot · **`p2` ESP FAT32** · `p3` `/` · `p4` estado · `p5` store (última, la
que crece). El `core.img` de i386-pc y el `BOOTX64.EFI` de x86_64-efi apuntan **los dos** a
`(hd0,gpt3)/boot/grub`: **un solo `grub.cfg`, una sola línea de comando, un solo sitio donde
editarla.** La ESP se puebla con mtools, sin root y sin loop, como el resto del script.

No se usa EFI-stub directo acá (sí `install-image-efi.sh`, ADR 0010): el stub por la ruta fallback
recibe LoadOptions VACÍO y necesita la cmdline HORNEADA en el kernel; la de `linux-generic` sólo trae
la consola, sin `root=`. Hornearla ataría la línea de comando al ArtifactHash del kernel.

Verificado con LA MISMA imagen en los dos firmwares, hasta entrar por SSH:

    UEFI (OVMF)   → /sys/firmware/efi presente · PID1 arje-zero · raíz sda3 · store sda5
    BIOS (SeaBIOS)→ /sys/firmware/efi ausente  · PID1 arje-zero · raíz sda3 · store sda5

Si falta `grub-mkimage` con x86_64-efi o mtools, la ESP se saltea con un aviso que dice qué se pierde
—no en silencio—: la imagen sigue arrancando por BIOS, pero eso la ata a esos proveedores.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 16:47:54 +00:00
Sergio 62e6a14814 estado: cosecha granja 2026-09-11T16:32:14Z — avance del árbol KDE 2026-09-11 16:32:14 +00:00
SergioandClaude Opus 5 96dc488842 mudanza: recomendaciones CON MOTIVO, y la lista completa a la vista para elegir
Correr en el origen no significa que aplique en el destino. `planear.py --revisar` muestra todo
agrupado con su recomendación **y su razón**, y `--decide` deja aceptarlas en bloque o revisarlas una
por una.

Cuatro familias que no aplican en una caja remota, cada una con su motivo:

  · hardware local            → bluetoothd, ModemManager, upowerd, adb
  · escritorio o pantalla     → waypipe
  · la red del destino        → NetworkManager, dhcpcd: **pelearían** con el init de allá
  · lo provee el init destino → udevd, dbus-daemon, elogind, polkitd, agetty

11 de 37 servicios de gioser caen ahí. El motivo no es cortesía: una recomendación sin razón no se
puede discutir, así que o se acepta a ciegas o se ignora entera.

**Y la recomendación DERIVADA, que es la más fuerte**: si el binario vive en un árbol que no se muda,
el servicio no podría arrancar allá. Se calcula cruzando el `cmdline` leído de `/proc` contra las
decisiones de datos. Probado en los dos sentidos: con `/mnt/vvv` muriendo, `puerta-f6e393ff` sale
«no mudar — su binario vive en /mnt/vvv»; con `/mnt/vvv` mudándose, sale «mudar».

Por eso la revisión decide los DATOS PRIMERO: de ellos se deriva la recomendación de los servicios, y
al revés no se puede calcular. Y los datos salen SIN recomendación a propósito — qué datos valen no
se deduce de la máquina; `terapeuta.ec` eran 279 M sin DNS y sólo el usuario podía decidirlo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 16:27:58 +00:00
Sergio c4641bade8 estado: cosecha granja 2026-09-11T16:03:12Z — avance del árbol KDE 2026-09-11 16:03:13 +00:00
SergioandClaude Opus 5 d682b9b240 mudanza: el APLICADOR — idempotente, reanudable, y que no marca como hecho lo que no ejecutó
`aplicar.py` ejecuta un plan. La regla que lo define: **un paso que no se ejecutó no se marca como
hecho**. Un plan tiene pasos ejecutables y pasos MANUALES cuyo `cmd` son comentarios («instalá el
paquete», «cambiá el registro A»); un aplicador que ejecuta un bloque de comentarios obtiene exit 0 y
lo marca «ok» — la peor mentira posible, porque deja el servicio caído con el informe en verde.

Acá quedan `pendiente-humano`, la corrida sale con ≠0, y `--hecho N` los confirma — negándose si el
paso sí tenía comandos («corrélo, no lo marques»).

Se le cree a la VERIFICACIÓN, no al exit code: el rsync que llenó el disco devolvió 0 y dejó 1367
artefactos vacíos. Si el comando sale bien y la verificación falla, queda `sospechoso`. Y las
verificaciones van TIPADAS (`cmd`/`humano`): «la columna Available debe ser > X» no es un comando.

Estado reanudable en `<plan>.estado.json`, escrito con temporal + fsync + rename — la lección que
costó un upgrade entero en el SDD 28.

**Y un fallo propio, encontrado en la primera corrida real**: la verificación del paso de datos
imprimía el número de directorios vacíos y devolvía 0 igual. Dio «✓ verificado: 2» donde ese 2 eran
dos vacíos en destino. Un guardián que siempre pasa no es un guardián. Ahora COMPARA los ficheros de
los dos lados y falla si difieren. Probado con rotura a propósito y con el control que debe pasar:
copia no hecha ⇒ origen=5 destino=0 ⇒ FALLA; copia hecha ⇒ 5 y 5 ⇒ PASA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 15:20:15 +00:00
SergioandClaude Opus 5 1a89592b72 mudanza etapa 3: declarar.py — de «corre y nadie sabe cómo» a una Semilla que arranca
Copiar un binario no lo levanta al arrancar. Esto toma la invocación que el censo leyó de `/proc` y
emite `seed.card.json`. Medido sobre gioser: **37 de 37 servicios vivos declarados, cero fallos**, y
la tarjeta de caddy sale con su invocación real — exactamente lo que faltaba para que no muriera en
el próximo reinicio.

Por qué no alcanza con `arje-absorb`: absorb lee la DECLARACIÓN del init ajeno, que es justo la que
miente (`rc-status` daba `stopped` para cinco servicios vivos), y los `no-declarado` no aparecen en
ninguna declaración por definición. Absorber la declaración reproduce el agujero. Se complementan.

Tres reglas:
1. Un servicio sin `cmdline` legible NO se emite y se dice: una tarjeta que no arranca es peor que
   una ausente — la ausente falla ruidosamente, la rota deja el servicio caído en silencio.
2. El `cwd` se preserva ENVOLVIENDO, porque el payload `Native` acepta `exec`/`argv`/`envp` y no
   `cwd` (comprobado sobre la semilla real). 9 de 37 servicios de gioser dependen de su directorio.
3. IDs deterministas ⇒ misma entrada, misma semilla BYTE A BYTE (verificado con sha256). Un fichero
   generado que cambia en cada corrida no se puede revisar con `diff`.

**Y un bug del censo que costaba el 70 % del dato**: `cmdline` y `cwd` se leían en un solo comando, y
como `readlink /proc/<pid>/cwd` exige permiso de ptrace, en cualquier proceso de root el comando
entero salía ≠0 y se descartaba TAMBIÉN el `cmdline`. 26 de 37 servicios quedaban sin invocación.
Sondas separadas. **Una sonda que falla es un dato; no puede arrastrar a las que funcionaron.**

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 15:13:53 +00:00
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