Commit Graph
3377 Commits
Author SHA1 Message Date
Sergio 5a07271a97 estado: cosecha granja 2026-09-22T06:01:53Z — avance del árbol KDE 2026-09-22 06:01:53 +00:00
Sergio 976ee18878 estado: cosecha granja 2026-09-22T05:31:31Z — avance del árbol KDE 2026-09-22 05:31:31 +00:00
Sergio 85e9b7515a estado: cosecha granja 2026-09-22T05:01:41Z — avance del árbol KDE 2026-09-22 05:01:41 +00:00
Sergio dc37736c55 estado: cosecha granja 2026-09-22T04:31:38Z — avance del árbol KDE 2026-09-22 04:31:39 +00:00
Sergio a5ad955459 merge: trabajo concurrente del otro frente 2026-09-22 04:12:32 +00:00
SergioandClaude Opus 5 d293f39e04 qillqa construye el corpus y REPRODUCE BIT A BIT — donde brush hacía 0 de 3
La prueba que de verdad decide, la misma con que se midió brush el 21/09: copia
del lab, qillqa como /usr/bin/awk, y tres recetas autotools reales. Resultado:

  pkgconf    b3:cfb6a37e…  sellada · IDÉNTICA al hash vigente
  flex       b3:6b25e351…  sellada · IDÉNTICA
  oniguruma  b3:751a5270…  sellada · IDÉNTICA

Los tres hashes son los que ya estaban en build-state.json, construidos con el
awk de busybox. O sea que qillqa no sólo deja construir: produce los MISMOS
BYTES. brush pasaba 56/57 casos POSIX y no construía ninguna de las tres.

Latencia 1,04× la de busybox (brush: 4,1×) y binario de 0,64 MB, más chico que
gawk (0,83) y que el busybox entero con sus 277 applets (0,98).

⚠ Son tres recetas, no las 943. Lo que prueban es que el camino que tumbó a brush
—el config.status que genera el libtool, donde awk hace el trabajo pesado— aquí
pasa. No prueban que no haya un cuarto caso.

Declarado en `base`, con lo medido escrito al lado de la declaración. El censo
baja a 62 con clear/reset, que ahora los da ncurses-tools.

Y una corrección de método que me costó una reparación: la primera copia del lab
la hice con `cp -al` (hardlinks) y después un `chown -R` sobre ella — que cambió
el dueño del INODO COMPARTIDO y se llevó por delante 471 entradas del
/work/dev-fs/alpine REAL, el lab que usan los demás agentes. Reparado
(chown -h 65534 sobre las 471; el control de /bin confirmó el dueño esperado) y
rehecho con copia real en el disco grande. Un hardlink no es una copia: lo que se
comparte no son sólo los datos, son los METADATOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 04:12:26 +00:00
Sergio 1647310894 estado: cosecha granja 2026-09-22T04:01:44Z — avance del árbol KDE 2026-09-22 04:01:45 +00:00
Sergio 2c882bdd65 merge: trabajo concurrente del otro frente 2026-09-22 03:48:36 +00:00
SergioandClaude Opus 5 ae6f0b45d9 qillqa: el awk propio — 3531 líneas, sólo libc, y CERO divergencias contra dos oráculos
«awk soberano» (usuario, 2026-09-22), que era la tercera opción del ADR pendiente
y la única que no adoptaba la semántica de otro: gawk es C con extensiones GNU,
goawk es Go y rompe la premisa de cadena de la Etapa C, frawk es otro dialecto.

Banco diferencial contra DOS oráculos a la vez —el awk de busybox 1.36.1 (el
applet que hoy viaja) y gawk 5.3.2 --posix— con 109 casos: campos y separadores,
el modelo de valores, aritmética, expresiones regulares, las funciones de cadena,
printf, control de flujo, arrays, funciones de usuario, E/S y los rincones del
parser. Resultado: 104 iguales a los dos, 0 divergencias, y 5 casos donde los
ORÁCULOS discrepan entre sí — que es justo para lo que sirve tener dos.

De esos 5, cuatro los tenemos del lado correcto por POSIX: `2^3^2` es 512 (la
potencia asocia a la derecha; busybox da 64), `index(s,"")` es 1, el `%*d` de
printf existe, y `length(array)` funciona. El quinto era nuestro y se arregló:
una coincidencia VACÍA pegada al final de la anterior no sustituye, así que
`gsub(/a*/,"-")` sobre "xax" da 3 y `-x-x-`, no 4 y `-x--x-`.

El motor de expresiones regulares es propio y esa es la razón de fondo para no
adoptar un crate: POSIX es leftmost-LONGEST y la familia Perl —el crate `regex`
en su modo por defecto— es leftmost-first. Para /a|ab/ sobre "ab", POSIX devuelve
`ab` y Perl devuelve `a`. En un awk eso decide qué borra un sub(), dónde parte un
split() y cuánto valen RSTART y RLENGTH. Construcción de Thompson con conjunto de
hilos: sin retroceso, así que `(a*)*b` contra sesenta `a` no explota.

Las cinco cosas que el banco destapó, todas dando resultados plausibles y ninguna
reventando —quedan como pruebas de regresión en tests/divergencias_medidas.rs:

  · leer NF no forzaba el partido en campos ⇒ `{print NF}` daba 0 hasta que
    alguien tocara un campo. El partido es perezoso a propósito (es lo que hace
    barato asignar a $0) y el precio es acordarse de forzarlo.
  · `length` sin paréntesis se leía como una VARIABLE llamada length, que vale
    vacío. Es el único builtin al que POSIX permite omitirlos.
  · los arrays se pasan por referencia, y eso se decide mirando si la función usa
    el parámetro como array — `a["k"]=1` tiene el nombre en el LUGAR de la
    asignación, y sólo se miraba el valor.
  · substr con inicio ≤ 0 recorta a 1 SIN ajustar el largo, y los no enteros se
    truncan. La lectura estricta de POSIX daría otra cosa; ni busybox ni gawk la
    hacen, y lo que hay que ejecutar son los guiones escritos contra ellos.
  · la regla del vacío en gsub, arriba.

Verificado además contra este repo: de los 61 programas awk únicos que usan sus
guiones y recetas, qillqa acepta los 59 reales (los 2 restantes son f-strings de
Python que mi extractor tomó por awk). 29 pruebas propias en verde.

El nombre es quechua («escritura») y sigue el precedente de simi: lo registra el
comentario de cabecera y lo DECIDE un ADR. El comando instalado será `awk` — eso
es contrato y lo llaman los configure de medio corpus.

⚠ Lo medido es el build NATIVO del hub. Falta la receta, el estático musl con zig
cc, y la prueba que de verdad decide: un `configure` de autotools con qillqa como
/usr/bin/awk. Es la lección de brush — pasó 56/57 casos POSIX y no construía una
sola receta.

Va también recipes/ncurses-tools.toml (clear, reset, tput, tset, infocmp, tic),
declarada en `base`: la canónica instala sólo lib y cabeceras, así que clear y
reset en una imagen eran el applet de busybox. Instala los binarios y NO la base
terminfo: resuelven con los --with-fallbacks compilados en la librería, que
cubren la consola del metal, QEMU y cualquier ssh moderno.

⚠ Y ncurses-tools selló DINÁMICO dos veces antes de salir bien: ncurses usa
`-static`/`-dynamic` como MARCADORES de tramo alrededor de las librerías —su
forma portable de -Wl,-Bstatic … -Wl,-Bdynamic—, no como decisión global. El
`-dynamic` llega al final de la línea de enlace y gana por orden. Se quita
sobreescribiendo LIBS_TIC y LIBS_TINFO en la línea de make. Tercer sabor del
agujero que documenta scripts/static-audit.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 03:48:25 +00:00
Sergio 0ff269a7d6 estado: cosecha granja 2026-09-22T03:31:36Z — avance del árbol KDE 2026-09-22 03:31:36 +00:00
SergioandClaude Opus 5 87d34b95fe red en metal: de udhcpc/ifconfig/route a dhcpcd/ip — y el adduser que NO se migra
Los dos guiones de WiFi que corren DENTRO de la imagen (metal-iso.sh y
cosmic/metal-desktop-image.sh) usaban cuatro applets que salen del defconfig:
udhcpc, ifconfig, route y el guión de hooks que udhcpc necesita. Los usos eran
básicos —subir la interfaz, pedir IP, poner ruta y resolv.conf— así que van a las
herramientas ya declaradas en `base`: `ip` de iproute2 y `dhcpcd`.

Y el cambio arregla algo de paso, no sólo sustituye: dhcpcd queda de daemon
RENOVANDO el lease. Una de las dos causas medidas el 2026-08-06 de que el enlace
se cayera solo a mitad de una depuración remota era precisamente que `udhcpc -q`
pedía la dirección una vez y nadie la renovaba. El bloque wifi-keep que lo
mitigaba sigue (la otra causa, el AP que olvida clientes ociosos, no se toca).

`adduser` de alta-usuario.sh NO se migra, y esa es la decisión, no una omisión:
`useradd` es de shadow y su modelo nativo es escribir /etc/shadow — el fichero
cuya creación dejó a la caja sin aceptar ni la clave de root, con los ocho
dominios caídos ~20 min (SDD 28 §6.51). El script tiene un guardián explícito
para cazar justo eso. Cambiar el applet por la herramienta «buena» reintroduce el
fallo que el guardián existe para atrapar ⇒ `adduser` queda indultado en el
defconfig junto a setuidgid, switch_root, halt, poweroff y reboot. El día que la
caja adopte shadow de verdad se revisa entero.

⚠ Sin probar en metal: no tengo la laptop acá. El cambio es de una línea por
guión y las dos herramientas están en los 7 perfiles, pero quien lo arranque en
hardware que mire `ip -4 addr show` antes de dar la red por buena.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 03:28:46 +00:00
Sergio 03ea9ef615 estado: cosecha granja 2026-09-22T03:01:45Z — avance del árbol KDE 2026-09-22 03:01:45 +00:00
Sergio 29c244612c sitio: la sección «las decisiones» — diez, con contra qué se tomaron
Faltaba lo que la página no decía y es lo que distingue a la distro: no QUÉ es,
sino QUÉ SE DECIDIÓ y contra qué. Diez fichas, cada una con tres lecturas de lo
mismo — frente a lo habitual, técnicamente, y en simple— para que sirva igual al
que quiere el detalle y al que quiere entender de qué se trata.

  01 fuentes clavadas por commit/sha256   06 sin systemd: arje y arranque por grafo
  02 reproducible bit a bit, y why-differs 07 romper a mano y volver (overlay+diario)
  03 el paquete es receta, no binario      08 Wayland-only y PipeWire
  04 se reconstruye a sí misma (mrustc→rustc) 09 lo ajeno enjaulado y FUERA del store
  05 musl estático con zig cc              10 hecha para que una IA la opere

La de los dos pisos no entra a la lista: es la sección de arriba y de ella cuelgan
las diez. Repetirla habría sido rellenar.

Ninguna ficha es opinión: todas salen de un ADR o de una decisión ya tomada en
`docs/` — 0006 fuentes pineadas, 0003 zig cc, 0007 arje, 0010 arranque por grafo,
0014 multi-origen con índice firmado, 0015 imágenes ajenas. Y las afirmaciones
técnicas se comprobaron en el código antes de publicarlas, no de memoria:
`--unshare-all` (que incluye la red) y `SOURCE_DATE_EPOCH=1` están en
`takana-build/src/sandbox.rs:415,453`.

⚠ El precio, dicho en el README del sitio: si una decisión cambia, esta página
miente hasta que alguien la toque. Se gana claridad en la landing y se paga en
mantenimiento.

Las fichas se armaron con un guion de un solo uso, que no entra al repo: la página
sigue siendo la única fuente y no hay generador que mantener. Renumerados los
sellos (el código pasa a 03, estado 04, paquetes 05, frontera 06) y la barra suma
su enlace. 142 pares es/en emparejados, etiquetas balanceadas, cero anclas rotas,
y lo servido coincide con el disco.
2026-09-22 03:00:28 +00:00
SergioandClaude Opus 5 c8fcd04945 paso 2 verificado: se hidrata y se mira, no se declara
El criterio de terminado del paso 2 decía «y la imagen los lleva», y eso no se
comprueba declarando. Hidratación real con el orden del ensamblado (busybox
primero, el userland encima, como USERLAND_COMPONENTS): ningún binario de los
declarados lo sigue ganando busybox — grep, sed, find, xargs, diff, cmp, gzip,
zcat, xz, bc, dc, vi, cpio, killall, pstree, fuser y los de coreutils salen todos
del paquete que corresponde.

Y la prueba que faltaba: hidratar la xz canónica ENCIMA de xz-tools —el escenario
que motivó quitarle lib/ e include/ a la variante— deja liblzma.a de la canónica,
usr/bin/xz de la variante, y xz --version sigue contestando. Conviven, que es lo
que la receta afirmaba y ahora está medido en vez de razonado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 02:56:43 +00:00
Sergio e5485d6505 merge: trabajo concurrente del otro frente 2026-09-22 02:54:07 +00:00
SergioandClaude Opus 5 6b33d1a435 el frente kmod no existe: los kernels de takana no tienen módulos
Fui a escribir el kmod en Rust que el plan pide —la pieza más grande que
quedaba— y lo primero fue buscar contra qué módulos probarlo. No hay ninguno:

  · 0 ficheros .ko en los artefactos sellados de los cuatro kernels (sólo
    boot/bzImage y el .config)
  · 0 recetas del corpus hacen modules_install
  · 0 drivers `=m` en los .config sellados (contra 1809 `=y` en linux-metal)
  · `# CONFIG_MODULES is not set` en los tres kernels que están en el store
  · nadie en el repo enciende CONFIG_MODULES

Sin CONFIG_MODULES el kernel ni siquiera implementa init_module/delete_module: un
insmod escrito en Rust recibiría ENOSYS. Los 6 applets de kmod no son escritura,
son 6 applets a retirar. Y con ellos otros 3: DEVTMPFS_MOUNT=y hace que el kernel
cree y monte /dev, no hay UEVENT_HELPER, y libudev-zero ya viaja en los 8
perfiles ⇒ mdev, makedevs y uevent tampoco tienen consumidor.

De los 23 applets que el plan llamaba «el trabajo real» queda: un ADR (awk), una
migración de card (getty → agetty, que ya está en los 7 perfiles), y decidir
nslookup (ningún candidato publica ese nombre; su único llamador es
scripts/drive-netup.py y migrarlo sale más barato que traer BIND).

Y la corrección que me toca: hace dos horas escribí en la salida del vigía, como
advertencia, que «el kernel llama a /sbin/modprobe por call_usermodehelper ⇒ esos
applets salen libres y no lo son». Es plausible, es la forma correcta de razonar,
y es falsa para este producto. La escribí sin medirla, en el mismo commit donde
presumía de haber arreglado dos falsos negativos. Corregida en el script.

NO se recorta ahora: editar RETIRADOS re-hashea busybox y las 24 recetas que lo
declaran build-dep, y 15 de esas 24 siguen en deuda por el recorte del 21/09. El
siguiente recorte junta lo ya decidido y se hace una vez: 6 de kmod + 3 de /dev +
6 alias internos de iproute2 (ipaddr iplink ipneigh iproute iprule iptunnel, que
no son comandos de nadie) = 15 applets, 277 → 262, sin escribir una línea.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 02:53:58 +00:00
Sergio 2c723c7493 estado: cosecha granja 2026-09-22T02:31:54Z — avance del árbol KDE 2026-09-22 02:31:54 +00:00
Sergio 55160d3f06 estado: cosecha granja 2026-09-22T02:02:12Z — avance del árbol KDE 2026-09-22 02:02:12 +00:00
Sergio 6af8547e94 estado: cosecha granja 2026-09-22T01:31:38Z — avance del árbol KDE 2026-09-22 01:31:38 +00:00
Sergio 0f051c51f2 estado: cosecha granja 2026-09-22T01:01:23Z — avance del árbol KDE 2026-09-22 01:01:23 +00:00
Sergio a2159048f9 estado: cosecha granja 2026-09-22T00:31:28Z — avance del árbol KDE 2026-09-22 00:31:28 +00:00
Sergio 720b72099c estado: cosecha granja 2026-09-22T00:01:47Z — avance del árbol KDE 2026-09-22 00:01:47 +00:00
Sergio 66db5484e9 estado: cosecha granja 2026-09-21T23:31:35Z — avance del árbol KDE 2026-09-21 23:31:35 +00:00
Sergio e08ee8ba0e estado: cosecha granja 2026-09-21T23:01:45Z — avance del árbol KDE 2026-09-21 23:01:45 +00:00
Sergio 9941ab3cea estado: cosecha granja 2026-09-21T22:31:22Z — avance del árbol KDE 2026-09-21 22:31:22 +00:00
Sergio a88d9e5b77 estado: cosecha granja 2026-09-21T22:01:37Z — avance del árbol KDE 2026-09-21 22:01:37 +00:00
SergioandClaude Opus 5 8a109400b5 vigía --uso: de los 64 applets que sólo da busybox, 12 tienen llamador
Es lo que el plan dejó sin medir tres veces («no se midió qué applets usa el
producto de verdad; eso abarataría el paso 1»). El modo lee del TSV los applets
con proveedor_medido = SOLO-BUSYBOX y los busca en el repo con las reglas
anti-ruido que el vigía ya tenía.

12 invocados: sh (28 sitios, lo cierra simi), ifconfig/route/udhcpc/netstat
(todos con ip/ss como primera opción y el applet como respaldo), setuidgid (3
cards: gitea, pacha-secretos, tejido), getty (la card de la consola), adduser,
nc, nslookup, poweroff. Y uno que es falso positivo del grep y conviene tener
escrito: los `insmod` de iso-image.sh/install-image.sh son de GRUB —`insmod
part_gpt` dentro de un grub.cfg—, no del busybox.

Los otros 52 no los llama nadie EN EL REPO, y la salida lo dice sin dejar que se
lea como una lista de borrado: un grep ve a quien invoca por nombre, y hay dos
consumidores que no aparecen nunca — el KERNEL (llama a /sbin/modprobe por
call_usermodehelper y al gestor de /dev por hotplug, así que modprobe/lsmod/
depmod/rmmod/modinfo/mdev/uevent/makedevs salen «libres» y no lo son) y el
USUARIO de la máquina, que espera clear/reset/vi/traceroute al entrar por ssh.

La primera versión del modo decía 10 y estaba mal en dos casos, los dos del lado
peligroso — dan permiso para retirar algo que se usa:

  · setuidgid se invoca como `exec /usr/bin/setuidgid pacha`, con RUTA ABSOLUTA,
    y el patrón de comando pide el nombre pegado al exec.
  · getty vive como `"argv": ["getty", …]`, elemento de un array JSON, y la regla
    que filtra cadenas por «parece una línea de comandos» —la que bajó el ruido
    de 91 hits a 0— descarta un nombre suelto.

Las reglas que quitan ruido son las que crean ceguera; lo que decide es hacia
dónde se equivoca cada una. Ahora busca tres formas: nombre desnudo, ruta
absoluta y primer elemento de argv. El vigía sigue verde en su modo original
(123 applets retirados, nadie los invoca) y se salta su propio fichero, que
nombra applets en la documentación y se denunciaba a sí mismo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:53:06 +00:00
SergioandClaude Opus 5 4357b29cb7 busybox 69 → 64: psmisc y bc, las dos piezas que el plan marcaba TRAER
psmisc 23.7 (killall pstree fuser prtstat pslog) y gavinhoward/bc 7.0.3 (bc Y dc
del mismo árbol). Ninguna estaba en el corpus, y lo de psmisc es lo que el plan
daba por cubierto sin estarlo: procps-ng NO trae killall ni pstree ni fuser —
procps-ng es ps/top/free/kill/pgrep/pkill/pidof/uptime/vmstat/w/watch.

bc es el de gavinhoward y no el de GNU por la cadena: C99 sin dependencias, bc y
dc en un árbol, y es el bc de sistema de Alpine y FreeBSD. El de GNU arrastra ed
y flex, y su dc va en otro paquete.

⚠ killall5 NO lo trae psmisc: es de sysvinit. Acá lo cubre arje-zero por FUNCIÓN
(es PID 1 y apaga él) ⇒ se retira con los indultos, no se reemplaza. Son 3 de los
4 que la tabla prometía, dicho así para que no vuelva a prometer lo que no hay.

Dos pisones de psmisc, los dos escritos en la receta para no repetirlos:

1. `-lncurses` no existe acá (la nuestra es widec ⇒ libncursesw.a). No sirve
   LIBS=-lncursesw —el AC_CHECK_LIB compila un programa de prueba CON -lncurses,
   no pregunta si el símbolo resuelve— ni la variable de caché con
   `make TERMCAP_LIB=…`, porque src_pstree_LDADD = @TERMCAP_LIB@ se sustituye en
   tiempo de configure y el Makefile queda con -lncurses LITERAL. Lo que funciona
   es un enlace libncurses.a → libncursesw.a en el scratch y un -L.

2. Y ese -L se comió el -static. El harness inyecta LDFLAGS=-static para las
   recetas link="static" (takana-build/src/lib.rs:285); reemplazar LDFLAGS lo
   pierde. Los seis binarios sellaron DINÁMICOS sin que nada fallara: killall
   respondía bien y sólo pstree moría en ejecución con `Error relocating
   /lib/libncursesw.so.6`, tomando la librería del anfitrión. Un `file` lo dice en
   un segundo; ninguna prueba del build lo iba a decir.

Los dos van a `base` en targets.toml, y el censo baja de 69 a 64.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:49:57 +00:00
Sergio 5fd8746445 atuq §7.terdecies: «no hay identidad» era también «no pude preguntar» — y en pacha-cli eso guardaba los dotfiles EN CLARO
Contesta el segundo pendiente del §7.duodecies: `pacha`/`pacha-secretos` en
`perfil.servidor`. La respuesta a la pregunta de origen es **NO**, y es la
CONTRARIA a la del escritorio.

Premisa medida, no supuesta: la Card de `pacha-secretos` es `scope=system` y
entra al `genesis`, o sea que arje arranca el daemon EN EL ARRANQUE. De ahí se
sigue que un sembrador en la imagen no llega a tiempo por construcción — en el
arranque no hay nadie logueado; `abrir_almacen()` decide una vez en `main()` y
no vuelve a mirar; y `agora-cli unlock` de una sesión ssh siembra en el llavero
de ESA sesión. Declararlo dejaría la función PARECIENDO cerrada, que es peor
que el hueco. Queda escrito al lado de las dos raíces, en `targets.toml`, para
que nadie copie la decisión del escritorio.

Y al ir a medirlo, el control POSITIVO salió rojo — con la seed en `/proc/keys`
el daemon decía que no había identidad — y eso destapó el defecto de verdad:
los CINCO lectores de la seed hacían `.ok().flatten()` (o un `_ =>`), que
convierte «el llavero no se pudo consultar» en «no hay identidad desbloqueada».
La causa del rojo la mide la sonda de syscalls: en el LXC `add_key` funciona y
`keyctl(KEYCTL_SEARCH)` da `ENOSYS` ⇒ se puede sembrar y no cosechar.

El peor de los cinco no era un mensaje feo: `pacha-cli` guardaba los dotfiles
SIN CIFRAR, en silencio, con la identidad del usuario desbloqueada.

Arreglado en tawasuyu (`4f2b5eac7`) con `pacha_llavero::SeedDeSesion` —tres
estados, tres textos— y `Reason` en `net.tawasuyu.Secretos1`. Pin de las dos
recetas `23a292863` → `cd9acd0d9` ⇒ `b3:e8038352` (4,6 M) y `b3:a9fb8c17`
(8,5 M), mirados por dentro y probados como artefacto: el daemon sellado ahora
dice «motivo=el llavero de sesión NO se pudo consultar (… os error 38) — esto
no es «no hay identidad»».

Tres cosas más que quedaron medidas por el camino:
- la guarda pegada al build **se disparó sola por primera vez**: el latido
  revirtió `pacha.toml` entre las dos construcciones de la misma tanda;
- el muro del `Cargo.lock`, quinta vez, y la arista que faltaba era la mía de
  esa mañana: se la llevó un «merge de git en «main» (import)». Bisecado con
  `git log -S`;
- el índice Y el árbol compartidos de tawasuyu tenían una versión de
  `pacha-boveda-llimphi` ANTERIOR al arreglo de `7917fbb96`. Lo delató el test
  de regresión, no el diff.
2026-09-21 21:41:43 +00:00
Sergio c020c2af19 merge: trabajo concurrente del otro frente 2026-09-21 21:34:58 +00:00
SergioandClaude Opus 5 466972e301 busybox: 79 → 69 applets sin otro dueño, y ninguno de los diez costó escribir userland
Cuatro artefactos sellados y el paso 2 del plan cerrado. Los diez applets que
dejan de depender de busybox salieron de arreglar recetas, no de escribir código:

  xz unxz xzcat lzma unlzma lzcat  → recipes/xz-tools.toml (nueva, b3:194c7d69…)
  vi                               → un symlink en recipes/vim.toml
  cpio                             → un symlink en recipes/libarchive.toml
  arch hostname                    → los trajo el feature de uutils (b3:614ff653…)

xz-tools es el TERCER caso de la familia de zstd-cli, y eso ya estaba escrito en
targets.toml: «la receta canónica construye sólo lib/ y su artefacto no tiene
usr/bin; declararla no habría arreglado nada». Igual que musl-shared, zlib-shared
y libffi-shared. Cuatro veces la misma forma — la receta publica la lib, la
imagen declara el paquete, y el binario no está. Va como variante porque ampliar
la canónica re-hashearía a sus 14 consumidores.

⚠ xz-tools selló DINÁMICO en la primera corrida pese al link = "static", y
funcionaba: comprimía y descomprimía sin una queja. Es el relink de libtool, que
libarchive.toml y bluez.toml ya documentan — hace falta LDFLAGS=-all-static en
compile Y en install. Lo delató el `file` del binario, no una prueba que fallara.

Paso 2 del plan: uutils, findutils, findutils-xargs, diffutils, gzip, grep y
xz-tools declarados en el perfil `base` — seis recetas que llevaban meses
`sealed` con `perfiles: []`. Hasta hoy, `find`, `xargs`, `diff`, `cmp`, `gzip`,
`zcat` y `grep` en una imagen de takana eran el applet de busybox, no porque
faltara escribirlos sino porque nadie los declaró. `grep` entra como PUENTE
declarado: ripgrep ya viaja pero publica `rg` y no es grep POSIX.

El censo gana modo --guardian, y vigila PÉRDIDAS, no un umbral: un umbral hay
que subirlo cada vez que se retira un applet y a la tercera nadie lo sube con
criterio; que un applet con proveedor medido deje de tenerlo es siempre una
regresión. Probado en los dos sentidos — rc=0 contra /store, rc=1 contra un store
mutilado, nombrando applet y proveedor perdido.

Los cuatro sellaron con el mismo hash en el store tirable y en /store: dos
work_root distintos, bytes idénticos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:34:46 +00:00
Sergio 077e5acc21 estado: cosecha granja 2026-09-21T21:32:11Z — avance del árbol KDE 2026-09-21 21:32:11 +00:00
Sergio e0e390c282 repo list/verify: hablan HTTP — un repo remoto con 173 paquetes se leía como vacío
`repo list --repo <url>` tomaba un `PathBuf`, así que trataba la URL como una ruta
relativa que no existe y respondía **«repo vacío» con salida 0**. No es que no
soportara el caso: es que afirmaba lo contrario de lo que pasaba, y con rc=0, que
es la forma de fallo que un script no puede detectar. Salió al probar el repo del
dominio nuevo.

`install --repo` ya sabía hablar HTTP con `RepoSource` (ADR 0014: lista de
orígenes separada por comas, probados en orden). `list` y `verify` ahora usan el
MISMO resolvedor, así que la misma cadena de espejos vale en los tres verbos.
`sign` se queda local a propósito: firmar es escribir.

Y `verify` remoto no es un extra — es el caso de uso del ADR 0014. Verificar el
release de un espejo ANTES de instalarle nada era imposible sin construir.

De paso, tres estados que se daban por iguales y ahora se distinguen (CLAUDE.md
§3: un ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo
fue bien):

  dir que NO EXISTE        → error, rc=1, y sugiere que quizá era una URL
  dir sin index.json       → «repo por crear» (estado válido: lo crea `pack --repo`)
  índice con 0 paquetes    → «índice publicado y SIN paquetes»
  origen que no sirve      → error del fetch, con los orígenes probados

Y el índice se atribuye al origen que REALMENTE lo sirvió, no a la lista entera:
con `<espejo-caído>,<bueno>` la salida dice el bueno. Es la misma regla que el
propio `fetch_desde_algun_origen` aplica a los `.swm` —«decir bajado de la lista
entera cuando sólo uno respondió es una media verdad»— que `read_index` tiraba.

Medido contra el repo vivo: `list` 173 paquetes [release firmado por release],
`verify` trusted. 93+93+6+1+3+4 tests del crate en verde.
2026-09-21 21:28:49 +00:00
Sergio 008692f7b9 sitio: el dominio ya tiene SSL, y takana.gioser.net pasa a redirigir acá
El DNS de `takana-os.net` y `www` quedó apuntado, así que Caddy sacó el
certificado de Let's Encrypt (vence el 2026-12-20) y el sitio responde 200. Un
detalle que costó dos minutos y no estaba escrito: **el vhost se había puesto
ANTES del DNS**, y Caddy no reintenta al instante — se queda en su backoff y el
nombre sigue mudo aunque el registro ya esté. `arjectl restart caddy` fuerza el
reintento; anotado en el README del sitio.

Dos nombres más, y los dos SÓLO redirigen:

  takana.gioser.net  → 301 a takana-os.net   (la página vieja, la de hermanas.py)
  git.takana-os.net  → 301 a git.gioser.net  ( le falta el registro A)

`git.takana-os.net` no sirve gitea y no es por pereza: gitea tiene UN `ROOT_URL`
con el que arma cada enlace absoluto y cada URL de clon que muestra la UI.
Colgarlo de un segundo nombre daría un sitio que se contradice —entrás por uno y
te ofrece clonar del otro— y moverlo de verdad rompe los clones y el CI que ya
apuntan al viejo. `git clone` sigue el 301 sin chistar.

⚠ `publicar-webs.sh` comparaba `takana.gioser.net` contra el fichero en disco, y
eso ya no aplica: ahora es un 301. Se separó el control en dos —`comprobar` por
sha contra el disco, `redirige` por destino— y `takana-os.net` entra en el
primero. Lo que NO entra es el `pull`: su raíz es el árbol de trabajo que los
agentes editan, y tirar de ahí mueve HEAD bajo los pies de otro (CLAUDE.md §2 ter).

⚠ Y la primera versión de `redirige()` preguntaba por `getent hosts` quién
resuelve: **en esta caja `getent` NO EXISTE** —musl/busybox no lo traen— así que
daba 127 para todo y declaraba «no resuelve todavía» hasta de los nombres que
estaban andando. Un control que nunca mira nada se ve igual que uno en verde; es
la lección del reaper otra vez. Ahora se lo pregunta a curl, por su código 6.
Corrido entero: 4 verdes por contenido, 1 por destino, 1 avisando del A que falta.

La página suma los dos verbos que ya se pueden correr contra el repo remoto sin
construir nada (`repo list` y `repo verify`), que es el commit de al lado.
2026-09-21 21:28:33 +00:00
Sergio edc4864771 servidor: el pin de pacha/pacha-secretos sube al hijo — otro merge se llevó la arista del lock
`cargo vendor --locked` sobre `4f2b5eac7` muere con «cannot update the lock
file». Esta vez la arista que faltaba era la MÍA de esta mañana: `rpassword`
en `agora-cli`, que `338d4a9b9 "merge de git en «main» (import)"` se llevó al
resolver el lock con el lado viejo. Bisecado con `git log -S`, no adivinado.

Quinta vez que este monorepo pone el mismo muro, y ya está claro que no lo
levanta quien toca el lock sino cualquiera que toque un `Cargo.toml` — o un
merge que lo resuelva mal.

Cerrado en el worker, donde el registro está completo: +1 línea, y el blob es
byte a byte el que ya se había publicado.

hash: pacha-secretos b3:7599de72 → b3:e8038352 · pacha b3:916788ae → b3:a9fb8c17
2026-09-21 21:27:46 +00:00
Sergio 63c3b31600 servidor: pacha y pacha-secretos suben de pin — «el llavero falló» se contaba como «no hay identidad»
Los dos daemons de `perfil.servidor` leen la seed de identidad del llavero de
sesión, y los dos tragaban el error: `.ok().flatten()` (o un `_ =>`) convierte
«no se pudo preguntar» en «no hay identidad desbloqueada». La primera es un
error de sistema; la segunda se le echa a quien está delante.

Medido en el LXC del worker, con el binario sellado que va en la imagen:
`add_key` funciona y `keyctl(KEYCTL_SEARCH)` da `ENOSYS` (seccomp de
contenedor) ⇒ ahí se puede SEMBRAR y no COSECHAR, y con la seed presente en
`/proc/keys` `pacha-secretos` igual arrancaba diciendo «SIN identidad agora
desbloqueada». Es el mismo `Err` de cualquier sistema sin llavero de kernel.

Y en `pacha-cli` era peor que un mensaje equivocado: con el llavero roto los
dotfiles se guardaban SIN CIFRAR y sin decir una palabra.

Arreglado en tawasuyu (`4f2b5eac7`) con `pacha_llavero::SeedDeSesion` —los tres
estados, con `motivo()` distinto para cada uno— y test en los dos sentidos.
`pacha-secretos` gana además `reason` en `net.tawasuyu.Secretos1`, al lado de
`persistent`, que existía justamente porque «desde afuera es indistinguible».

hash: pacha-secretos b3:1d2946d0 → b3:7599de72 · pacha b3:ea0c664f → b3:916788ae
(las dos suben juntas al mismo commit para pagar UN solo vendoreo de 2,4 G)
2026-09-21 21:24:36 +00:00
Sergio 3573fb0057 merge: cosecha de la granja 21:02Z 2026-09-21 21:02:47 +00:00
SergioandClaude Opus 5 64005efcbd busybox: los 23 applets sin dueño eran 79 — el reparto era juicio, y medido no da
El plan de botar busybox decía de sí mismo que el reparto applet→dueño era
«juicio, no medida». Medido contra los 1407 artefactos del store: de los 277
applets vivos, 167 los provee el dueño que el TSV nombra, 31 los provee OTRO
paquete, y 79 no los provee NADIE salvo busybox — 56 más de los 23 previstos.

El hallazgo transversal es que una receta de LIBRERÍA no es un paquete de
HERRAMIENTAS, y build-state no distingue: `xz` figura en SEIS perfiles con el
`usr/bin` VACÍO (su receta pasa --disable-xz --disable-xzdec --disable-scripts,
o sea liblzma y nada más), y `ncurses` instala sólo install.libs/install.includes
⇒ ni clear, ni reset, ni tput. Las dos viajan, las dos dicen `sealed`, y el único
binario que hay adentro de la imagen es el applet de busybox.

Y al revés: `switch_root` y `sulogin` los trae util-linux, `partprobe` lo trae
parted, y `getty` tiene sustituto — `agetty` de util-linux, en los 7 perfiles ⇒
deja de ser ESCRIBIR y pasa a ser migrar el exec de la card. Seis de los 18
huecos de iproute2 (ipaddr iplink ipneigh iproute iprule iptunnel) no son
comandos de nadie: son nombres internos de busybox y se borran del defconfig.

uutils traía 78 applets, no los 90 que el plan le atribuía: el multicall
contestaba `coreutils: unknown program 'chmod'`. Construía con las default
features (feat_common_core) y el resto vive detrás de feat_os_unix_musl, que
cubre exactamente los 25 que faltaban. Construido y verificado contra un store
tirable: 79 → 106 applets, 27 nuevos y CERO perdidos, chmod/uname/id ejecutan, y
`stdbuf` correctamente ausente (por eso la variante _musl y no feat_os_unix:
upstream lo excluye en musl porque necesita un cdylib).

Importaba más de lo que parece: USERLAND_COMPONENTS hidrata uutils DESPUÉS de
busybox para ensombrecerlo, y lo que no existe no ensombrece nada — chmod, chown,
id, uname y stat los seguía dando busybox con el userland Rust instalado encima.

⚠ Dicho en la receta y en el documento: who/users/uptime/pinky compilan contra
los stubs de utmpx de musl y contestan vacío. No es regresión, tampoco es
sustitución.

El censo queda como script (scripts/busybox-censo-proveedores.py) y como dos
columnas del TSV, no como una tabla escrita a mano que vuelva a envejecer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:02:07 +00:00
Sergio 8f0391dbc0 estado: cosecha granja 2026-09-21T21:02:05Z — avance del árbol KDE 2026-09-21 21:02:05 +00:00
Sergio c7f122697c sitio: el README de operación no se sirve — el matcher del vhost lo 404ea
Vive junto a la página porque es su documentación, pero quedaba servido en
/README.md: rutas de la caja y el procedimiento del vhost, que no son para el
visitante. El bloque del README se sincroniza con el que quedó en la caja para
que no derive.
2026-09-21 20:56:24 +00:00
Sergio 86cf362627 takana-os.net: el sitio propio, servido del repo y con las dos direcciones
El dominio se registró hoy. La página entra al repo takana y NO al de gioser-web —
la que hay en `takana.gioser.net` la genera `hermanas.py` desde otro repo y todavía
apunta a `git.gioser.net/sergio/hammer`, un nombre que dejó de existir hace doce
días: una página sobre el proyecto que vive fuera del proyecto envejece sin que
nadie se entere.

Lo que la página publica, que es lo que se pidió: **dos direcciones**, no una.

  código    https://git.gioser.net/sergio/takana   (clone por HTTPS)
  paquetes  https://takana-os.net/repo/            (el MISMO /srv/repo firmado)

El `/repo/` cuelga por `handle_path` del mismo árbol que sirve `repo.gioser.net`,
así que es la dirección en el dominio propio sin DNS ni certificado nuevos.
Comprobado de punta a punta antes de escribirlo en la página, no supuesto:

  takana install age --repo https://repo.gioser.net --trust ./trust
  ⇒ release: trusted (by release) · apply OK

⚠ De paso se vio que `takana repo list --repo <url>` NO habla HTTP —sólo mira
directorios locales— y ante una URL responde «repo vacío» con salida 0 en vez de
decir que no sabe. Queda anotado en el README del sitio; el arreglo es otra unidad.

Las cifras de la tabla salen de `docs/state/build-state.json` y del store de la
caja, contadas hoy: 941 recetas, 926 selladas, 15 en deuda, 1407 artefactos, y las
clausuras de los cuatro escritorios (KDE 418/420, GNOME 318/319, COSMIC 282/283,
sway 269/270). No se estiman.

Sin terceros: las tipografías de la marca viajan en `fuentes/` (154 KB, subconjuntos
latin y latin-ext, las dos SIL OFL con su licencia al lado). Un sitio sobre no
depender de binarios ajenos que pidiera las fuentes a Google en cada visita sería
una contradicción barata.

Contraste medido, no a ojo: el brasa `#DF6B2A` de la marca sobre alpaca da 2,9:1 y
no se lee, así que el acento del tema claro es un brasa oscurecido `#9E3F16`.
Ninguna combinación de la página baja de 4,5:1.

El vhost ya está puesto en la caja (respaldo del Caddyfile + `caddy validate` +
`arjectl restart caddy` + testigo `git.gioser.net` respondiendo antes de darlo por
bueno). Falta lo único que no puedo hacer yo: los dos registros A a 2.29.29.217.
2026-09-21 20:55:27 +00:00
Sergio bf6037c832 ⚠ un overlay abierto se traga TODA la máquina: el /etc/shells y el chsh no estaban donde parecía
Mientras se escribían el /etc/shells y el `chsh -s /bin/zsh sergio` había un overlay del FHS
abierto —de un `takana install` sin --prefix sin commitear— y las DOS escrituras cayeron dentro,
aunque no tenían nada que ver con ese paquete ni pasaron por takana (un `cat >` y el chsh de
shadow). Medido con `mount --bind /` no recursivo: en el /etc REAL no había shells y sergio seguía
en /bin/bash, mientras la pantalla mostraba las dos cosas hechas. Un discard o un reinicio lo
borraba y nadie se habría enterado hasta el siguiente login.

Resuelto con commit (5 ficheros promocionados: passwd, passwd-, shells, zsh, zsh-5.9). El
desmontaje perezoso del §5.10 se ganó el sueldo ahí mismo: saltó en /bin y en /usr/bin.

La lección, que es más grande que el caso: `install` sin --prefix NO es un experimento acotado a
ese paquete — cambia la semántica de escritura de la máquina entera hasta que alguien commitea o
descarta, y el overlay no avisa por ningún lado. Como mínimo `takana status` debería salir al
entrar, y el `overlay listo` del install no debería leerse como «terminé». Anotado en SDD 28 §5.11.
2026-09-21 20:43:22 +00:00
Sergio b6e20829e4 chsh: faltaba /etc/shells — y debajo, que la distro no puede expresar un binario setuid
Pedido tras instalar zsh: «no me funciona chsh». Dos capas, y sólo la primera es un arreglo.

1. /etc/shells NO EXISTÍA en la caja, y shadow rechaza cualquier shell que no esté listado (para un
   usuario normal es rechazo duro, no aviso). Escrito con lo que existe, comprobado -x uno por uno,
   en vez de copiar la lista de otra distro: un shell listado que no está deja a quien lo elija sin
   poder entrar, y eso no se ve hasta el siguiente login. Con eso `chsh -s /bin/zsh sergio` va, y
   `su - sergio` entra con zsh 5.9.

2. ⚠ Pero sergio sigue sin poder cambiárselo él mismo: `su sergio -c "chsh …"` ⇒ «Cannot change ID
   to root». Los cinco binarios de shadow que necesitan setuid (chsh, passwd, su, chfn, newgrp)
   salen del store r-xr-xr-x, porque la receta lleva --disable-account-tools-setuid heredado del
   APKBUILD de Alpine (allá los hace setuid el empaquetado, no el configure). Y debajo hay otra
   capa: of_tree sólo hashea el bit de EJECUCIÓN (mode & 0o111), así que el modelo de artefactos
   hoy no puede ni expresar ni garantizar un setuid — aunque la receta lo pusiera, el hash no lo
   cubriría y nadie notaría si se pierde o aparece.

Quitar la bandera no alcanza y no es un arreglo de receta: es una decisión de seguridad. Anotada
en la propia receta y en SDD 28 §5.11, sin decidir.

El comentario va en la cabecera de la receta y NO mueve su hash: comprobado antes y después,
b3:843e9098… las dos veces (los comentarios fuera de [build.phases] no entran en hash_inputs).
2026-09-21 20:37:41 +00:00
Sergio d154ce02a2 simi: REPRODUCE bit a bit — y dos trampas operativas que costaron dos corridas
scripts/verificar-repro.sh simi en el worker: «✓ simi REPRODUCE», 0
no-determinismos. Queda anotado en docs/state/repro-verificado.tsv con su hash,
que es la clave del libro: si la receta o alguna dep cambia, la entrada deja de
casar y hay que volver a verificar.

Las dos trampas, anotadas en el SDD porque las dos fallan en silencio:

1. --store <tirable> NO sirve para verificar reproducibilidad. Un store vacío da
   «Error: io: No such file or directory» a secas —sin decir qué falta— porque las
   deps del build no están ahí. El método correcto ya estaba escrito: apartar el
   artefacto dentro del MISMO store y reconstruir. Dos corridas perdidas por
   inventar un instrumento en vez de leer el que existía.

2. La cosecha BORRA una receta sembrada a mano: siembra hub→worker con
   rsync --delete, y el hub del que siembra es gioser, no el clon donde se
   trabaja. El worker perdió recipes/simi.toml entre el build y la verificación
   (el artefacto sellado sobrevivió, la receta no) y el verificador informó
   «REPRODUCEN: 0 · sin artefacto: 0» — o sea nada, porque su primer [ -f ] falla
   y salta en silencio.

Comprobado de paso: editar los comentarios de la receta NO cambia el hash
(hash_inputs es una lista explícita de campos). Y el artefacto vive sólo en el
store del worker: sella con PROMOTE=0 y la cosecha baja el manifiesto, no el
store. Promoverlo es otro paso.
2026-09-21 20:35:53 +00:00
Sergio c2586b131c estado: cosecha granja 2026-09-21T20:31:34Z — avance del árbol KDE 2026-09-21 20:31:34 +00:00
Sergio 4621381050 simi: SELLADA en el worker — estática de verdad, y la latencia del artefacto es 1,04x el ash
b3:46ff6fcf67bd23dece4d130c92855908520200f88ca85981317f81fcc56bb834

Construida con flock -o en dev.gioser.net (el LXC prestado, coste 0). El
artefacto trae dos ficheros: usr/bin/simi y su .hammer/recipe.toml.

Medido sobre el ARTEFACTO, no sobre el build del hub:
- ELF x86-64 estático, stripped, 498.736 bytes
- scripts/static-audit.sh simi: «estáticos de verdad: 1 · MIENTEN: 0», o sea que
  el link=static no es una declaración sino un hecho (ese guardián existe porque
  libtool lo ignora en silencio en otras recetas)
- banco diferencial, 159 casos contra el ash de busybox: 0 divergencias
  (bash: 8, brush: 35)
- latencia 468 us contra 449 us del ash: 1,04x. brush: 1569 us, 3,5x

Y la lección que vale más que el número: el build NATIVO del hub (glibc,
dinámico) daba 865 us, 1,4x el ash. El estático musl baja a 1,04x. Medir el
binario que VIAJA, no el que se compila a mano — la diferencia fue del 35 % y
habría quedado escrita como si fuera del shell.

Sigue sin estar en ningún perfil de targets.toml a propósito: declararla es
meterla en ATTEST_PATHS y eso es frente C, su propia unidad de trabajo.
2026-09-21 20:30:49 +00:00
Sergio 136594828c simi: la receta pineada y el veredicto en el SDD — sin construir todavía, y dicho así
recipes/simi.toml sigue el patrón de hammerd/netup: crate del workspace a un
commit fijado, zig cc, musl estático, strip_debug. SIN [build.phases]: el
BuildSys::Cargo instala el binario, y el symlink `sh` NO lo pone la receta a
propósito — quién gana como /bin/sh lo decide USERLAND_COMPONENTS en
takana-bootstrap, que es frente C y su propia unidad de trabajo.

El SDD queda con el veredicto (simi 0 divergencias, bash 8, brush 35), el alcance
por consumidor, los siete bugs del camino y las seis cosas que faltan. Con dos
avisos explícitos: lo medido es el build NATIVO del hub (glibc, dinámico) y el
estático musl no se construyó, así que simi no entra en ningún perfil de
targets.toml; y la shell interactiva de getty no está cubierta.
2026-09-21 20:24:24 +00:00
Sergio 6809ef4be2 simi: el /bin/sh propio del producto — 4448 líneas, sólo libc, y CERO divergencias contra el ash
«simi» es boca/lengua en quechua. Es el sh que el frente C necesitaba: busybox
está en la raíz de confianza del arranque (STAGE1_COMPONENTS, ATTEST_PATHS) y en
el exec de cada card, y el candidato ajeno que se midió —brush 0.4.0— pesa 6,9 MB,
arranca 4x más lento que el ash y trae dos divergencias medidas (#1394, #1396).

Alcance MEDIDO, no supuesto: las 23 cards con shell (8 builtins, 13
construcciones), los 21 /init empotrados de las imágenes (+ if, funciones, case,
aquí-documentos) y el POSIX de takana-live-install.sh.

Resultado del banco diferencial, 159 casos contra el ash de busybox 1.36.1
(10 fixtures de regresión, 107 de tortura de comillas, 23 fragmentos de cards,
19 /init empotrados):

    simi   DIVERGE=0        bash  DIVERGE=8        brush  DIVERGE=35

Incluye el bucle REAL de config.status que genera cualquier configure de
autotools — el que tumbó a brush. 499 KB release, 1,4x la latencia del ash
(brush: 4,1x). 47 pruebas propias en verde.

Seis bugs que el banco destapó, y ninguno se veía sin un control al lado:

1. El lexer fundía lo entrecomillado con lo desnudo, así que x="a b" se ejecutaba
   como ORDEN y el error decía «no encontrado» — mandando a buscar al lugar
   equivocado. Ahora el comillado es por carácter.
2. Usaba la barra invertida como marca interna y la quitaba al final, así que una
   barra PRODUCIDA por una sustitución se perdía. La quita de comillas es sobre
   las comillas de la palabra, nunca sobre el resultado de una expansión: hace
   falta una máscara de protección, no un escape dentro del texto.
3. set -e no se suspendía dentro del cuerpo de una función llamada desde una
   condición. Ahora es un contador dinámico, que entra solo donde corresponde.
4. Los delimitadores de la sustitución de orden y de ${ } no respetaban la barra
   invertida: un paréntesis escapado cerraba la sustitución.
5. Las clases entre corchetes no entendían la barra invertida — y la del
   config.status de libtool es literalmente una clase con cuatro escapes.
6. Dentro de comillas dobles el cuerpo de un backtick sigue entre comillas, así
   que la barra también se elimina ante la comilla doble.

Y uno que el banco NO vio y sí vio la prueba de regresión: el trap EXIT HEREDADO
se disparaba en el subshell. POSIX §2.12 manda resetearlo — es la mitad
complementaria del #1396 de brush, y al revés. Los dos oráculos hacen falta; el
caso quedó agregado al fixture 08.
2026-09-21 20:22:40 +00:00
Sergio 26c153c149 overlay: el commit no podía desmontar /bin en una máquina viva — y el código prometía el perezoso sin hacerlo
La primera instalación real de un paquete en la caja (`takana install zsh`, sin --prefix) dejó el
sistema A MEDIAS: `commit` desmontó 5 de 7 targets y murió con `target is busy` en /bin, con dos
overlays montados y el estado sin promocionar. La causa no es un fd abierto: los procesos tienen su
EJECUTABLE mapeado desde /bin y /usr/bin —empezando por PID 1, /usr/bin/arje-zero— y un ejecutable
mapeado pin-ea el montaje. En un FHS vivo eso no se puede evitar esperando.

El comentario de `do_umount_lenient` prometía el desmontaje perezoso «desde la Fase 2» y el código
NUNCA pasaba `-l`. Ahora, ante `busy`, reintenta perezoso: desengancha el montaje ya mismo (las
rutas vuelven a resolver al directorio real, que es lo que el promote necesita) y libera cuando el
último proceso que lo miraba se muere; los vivos ven el mismo contenido porque el commit copia el
upper al lower justo después.

Probado en la caja con un paquete nuevo: install tree ⇒ overlay; commit ⇒ WARN del perezoso sobre
/usr/bin y «commit OK — 1 archivo(s) promocionados»; 0 overlays, /usr/bin/tree real, tree v2.3.2.

El SDD 28 §5.10 anota las otras cuatro cosas que ese `sudo takana install zsh` destapó: el repo por
defecto que no existía, el lab que no se encontraba desde /root, el binario del sistema incapaz de
leer los .tkn nuevos, y —la peor— que la instalación queda PARTIDA porque el overlay cubre siete
directorios y /usr/share no es uno: 1296 de los 1331 ficheros fueron directos al FHS real mientras
2 binarios esperaban el commit, y el mensaje decía `apply OK` igual.
2026-09-21 20:20:54 +00:00
Sergio a63af7b531 el aviso, enchufado en la caja — y «al día» con cero instalados era una mentira cómoda
Cron horario de root → /usr/local/bin/avisar-actualizaciones, un envoltorio que sólo fija las rutas
de la caja y llama al guion del repo. La lógica no se copia: comprobado en vivo que arreglar el
guion en el clon cambia la salida del cron sin volver a desplegar nada.

⚠ La primera corrida real destapó un defecto del aviso: con la DB de instalados vacía decía «al
día». Es verdad y no dice nada — peor, se lee como «comprobado y correcto» cuando lo cierto es que
no había NADA que comprobar. Un aviso que suena igual cuando vigila que cuando no vigila nada es
cómo se deja de mirar un aviso. Ahora distingue el caso y lo nombra.

Control en la caja con una DB de juguete (un zsh a otro hash): grita «1 con novedad · hash
distinto» y en la segunda corrida calla.

Y dos piezas que faltaban allá: /var/lib/hammer/trust/release.ed25519.pub (el almacén de confianza
por defecto de la CLI, que no existía ⇒ ahora install/outdated verifican sin pasar --trust) y
/usr/local/bin/takana con el arreglo del §5.5. Ojo: el PATH de la caja no incluye /usr/local/bin,
así que un `takana` a secas sigue siendo el del store; el cron usa rutas absolutas.
2026-09-21 20:03:34 +00:00