El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta. Los USUARIOS se quedaron
donde estaban las Cards: `/etc/passwd` de la imagen es `takana_bootstrap::PRODUCT_PASSWD`, un literal
con `root` y `sshd`, y añadir un tercero era editar Rust y recompilar takana.
No es simetría por elegancia, es un servicio que no arranca: `gitea` se niega a correr como root, su
Card hace `setuidgid gitea`, y sin la cuenta arranca, muere y reintenta para siempre con un log que
dice `unknown user` — no «a la imagen le falta una cuenta».
[[user]]
name = "gitea"
uid = 916
home = "/var/lib/gitea"
**El uid se declara, no se asigna**, por la misma razón que el ULID de la Card: un «primero libre a
partir de 1000» hace que dos imágenes del mismo perfil salgan con dueños distintos y el rootfs deje
de reproducir SIN QUE NADA FALLE — los ficheros se ven iguales y `ls -l` dice otro número. Fuera de
`hash_inputs`, medido: el hash de gitea no se movió (`b3:391a613e…` antes y después).
`takana users <recetas…> [--merge <rootfs>]` es el gemelo de `service-cards`, y fusiona **por clave,
no por línea entera** — componer dos veces no duplica, y `grep -q` de la línea completa no serviría
porque la misma cuenta con otro GECOS se leería como nueva. Tres decisiones con su control:
· Una cuenta ya presente con OTRA línea es CONFLICTO y no se pisa: sale ≠0. Pisarla es cambiarle el
uid a ficheros que ya son de alguien, y eso se descubre dentro de la VM.
· Se planea todo y sólo entonces se escribe. Fichero a fichero, un conflicto en `passwd` dejaba el
`group` ya escrito: media cuenta es peor que ninguna, porque parece que está. Comprobado con el
caso exacto — `group` sin la cuenta, `passwd` con otro uid — y los DOS ficheros quedan intactos.
· Un `passwd` ausente es un error, no un fichero a crear: crearlo dejaría una imagen SIN `root`.
La validación rechaza lo que rompe tarde: `root`/`sshd`/`nobody` y sus uids, uid fuera de
100..=65533, `home` relativo y un `:` en cualquier campo — que partiría la línea y fallaría lejos.
Y `--user-paths` NO es `--service-paths` con otro nombre: aquél lista los servicios HABILITADOS,
éste los paquetes INSTALADOS que declaran cuentas. `postgres` instalado y sin levantar necesita su
usuario igual, porque los ficheros de la imagen ya son suyos.
`scripts/servidor-image.sh` lo aplica entre hidratar el perfil y sellar la imagen, y aborta si hay
conflicto: un rootfs con el uid equivocado produce ficheros de un dueño que no existe.
Verde: 6 tests del módulo, core 234, cli 88, bootstrap 42, `targets.py --selftest` 7/7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
`--como-usuario` (atuq pelado, con SU perfil por defecto en la ext4 del disco) reprodujo el síntoma:
300 s de observación, seis capturas, ninguna con ventana. Pero el MOZ_LOG dice otra cosa 30 s después
de la última captura: `mapped 1`, «marked as visible & has buffer», `WaylandBufferSHM` de 1280x696
commiteado y `mIsFullyOccluded 0` — lo mismo que hace cuando pinta.
Segunda corrida, 900 s y captura cada 60 s:
+66s +137s fondo liso
+204s 699458 px magenta — la página, pintada
+268s…+526s 703766 px, estable
La primera pintura cae entre +137 s y +204 s, contra ~+134 s con perfil nuevo en tmpfs; y en la
corrida anterior no había llegado a los +291 s con la máquina más cargada. Bajo TCG «cinco minutos y
dos capturas» no distingue «no pinta» de «todavía no pintó», así que el §6.10.ter queda corregido en
su propio párrafo: de sus cuatro afirmaciones, la cuarta medía la paciencia.
⚠ Trampa nueva, para el que venga a medir la imagen: `cosmic-idle` ATENÚA la pantalla. Entre +526 s y
+590 s sin una sola entrada, los 703766 px de (255,0,255) pasan a 703779 px de (117,0,117) — la misma
ventana al 46 %. Un diff de píxeles o un contador por color exacto lo lee como «desapareció».
kio kcolorscheme kdeclarative krunner kwayland layer-shell-qt kquickimageeditor
kcharselect kruler kdf kfind.
REPRODUCEN: 11 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
`kio` entra acá y es de las gordas del árbol KDE: reproduce bit a bit.
La receta ya declara su servicio (SDD 30) y `perfil.servidor` lo habilita. El ArtifactHash NO se
mueve (`b3:391a613e…` antes y después): `[[service]]` está fuera de `hash_inputs`.
⚠ Primero, una corrección del commit anterior: declaraba `gitea` en `perfil.servidor` con SEIS
LÍNEAS DE COMENTARIO y sin la línea `"gitea",`. El perfil cargó igual, el grafo no dijo nada y el
paquete simplemente no estaba. Lo cazó `targets.py --services` al resolver el label: «lo declara
gitea, que NO pertenece al perfil». Un comentario que explica una entrada que no existe se lee como
la entrada.
La card no encarna el binario directo: **gitea se niega a correr como root** (`[F] Gitea is not
supposed to be run as root`), así que va por `setuidgid`. Y comprueba dos cosas del SITIO antes de
arrancar, saliendo 78 con un mensaje que las nombra: `/etc/gitea/app.ini` y el usuario `gitea`. Un
gitea sin config no falla — arranca y ofrece el asistente de «crear administrador» a quien pase.
Probado en el worker con el argv exacto y `env -i`, que es lo que arje hace de verdad. Tres fallos
que con una shell normal no se ven NUNCA:
· `exec setuidgid …` a secas ⇒ `sh: exec: line 0: setuidgid: not found`. El `sh` de busybox de la
imagen no trae `FEATURE_SH_STANDALONE`: no despacha sus applets, los busca en `PATH`, y PID 1 no
garantiza ninguno. Todo con ruta absoluta (`/bin/grep`, `/usr/bin/setuidgid`).
· sin `PATH` en el `envp` ⇒ `git not found: executable file not found in $PATH`. **gitea lanza
`git` como subproceso**, y el mensaje se lee como «falta git» con git instalado y raíz de `base`.
Un envp vacío no es «limpio»: es sin PATH.
· sin `cd` ⇒ `fatal: error reading '/root/.git'`. El cwd se hereda y gitea corre `git config` en él.
`Service` no tiene campo `cwd`, así que va en el argv, a la vista.
Con eso: escucha, `GET /` responde **200** y el proceso corre como `gitea`. Las dos guardas
verificadas por separado (78 y su mensaje cada una). El worker quedó limpio y censado: sin usuario,
sin /etc/gitea, sin /var/lib/gitea, sin symlinks y sin procesos.
Y `/etc/gitea` entra en las rutas de la mudanza: el `app.ini` guarda los SECRETOS generados (gitea
los escribe en el propio fichero, que por eso tiene que ser suyo, `gitea:gitea 0660`).
⚠ Lo que queda abierto y está anotado en la receta: **el usuario `gitea` no existe en el producto**.
`/etc/passwd` de la imagen es la constante `takana_bootstrap::PRODUCT_PASSWD` y sólo trae `root` y
`sshd`. El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta; los USUARIOS
siguen donde estaban las Cards. Hasta que se declaren, el usuario llega con los datos del sitio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Hub, KF6 tanda 4: REPRODUCEN 9 · DERIVA 1 · NO-DETERMINISMO 2 · no construyeron 0
Worker, Rust: REPRODUCEN 13 · DERIVA 2 · NO-DETERMINISMO 0
El libro pasa de 104 a 207 entradas en la jornada.
── Los dos que divergen, y por qué importan ──────────────────────────────────────────
`kirigami` — está en `escritorio-kde` y tiene **37 dependientes**. Dos sitios:
· libKirigamiTemplates.so: los símbolos del AOT de QML llevan un contador por fichero
que cambia de corrida a corrida (`…LinkButton_qml3$_28/_38/_58` en A contra
`…Badge_qml3$_88/_98/_108` en B) ⇒ el orden en que se compilan los .qml no está fijado.
· kdevappwizard/templates/kirigami6.tar.bz2: difiere desde el byte 0xa, o sea desde el
primer bloque comprimido — un tar armado sin orden estable.
`syntax-highlighting` — también en `escritorio-kde`. libKF6SyntaxHighlighting.so difiere
en 8.800.310 bytes desde el offset 41 y hasta **el tamaño cambia** (10.345.888 contra
10.345.840). Las cadenas que difieren son datos comprimidos ⇒ es el recurso generado con
las definiciones de sintaxis, otra vez orden de entrada.
Los tres de hoy (con `gocryptfs`) son la MISMA clase: **recurso generado cuyo orden de
entrada nadie fija**. No es el compilador ni el lab: es que la receta deja que el orden lo
decida un `readdir` o el planificador.
── Lo que NO se hace acá, y por qué ──────────────────────────────────────────────────
Arreglarlo toca la receta ⇒ re-hash ⇒ reconstruir kirigami y sus 37 dependientes. Eso es
una decisión de campaña con su coste, no algo que se mete de paso en una verificación.
Los cuatro ejemplares quedan en `store/.divergen/` para poder mirarlos.
── Control que sostiene el hallazgo ──────────────────────────────────────────────────
`kirigami-addons` y `kquickcharts` TAMBIÉN llevan QML y reproducen. O sea que no es «QML
es no-determinista»: es algo propio de esas dos recetas.
La mudanza emite tarjetas de arje y el producto instalado también; quién es dueño del formato no
estaba escrito, así que por omisión son dos repos. Medido hoy:
el tipo canónico existe y está en tawasuyu `shared/card/card-core::Card`
…y VALIDA su versión CARD_SCHEMA_VERSION = 1, comparada al deserializar
emisores dentro de takana 2, en dos lenguajes (bootstrap Rust + arje.py)
deps de card-core en crates/*/Cargo.toml 0
`"schema_version": 1` escrito a mano 5 sitios
arje-absorb usa card_core::Card y ya trae systemd/openrc/runit/
dinit/sysvinit; formatos/ reimplementa DOS en Python
El día que el esquema pase a 2, arje rechaza las tarjetas de takana — y no falla en un test: falla
en el arranque de una caja recién instalada. Hoy coinciden por suerte, no por construcción. Es el
mismo fallo que ya se pagó un piso más abajo, cuando había dos emisores DENTRO de takana y cada uno
tenía un campo distinto mal en la raíz.
Decisión: tawasuyu DEFINE, takana GENERA. Un solo emisor acá y que serialice con el tipo, no con
`format!` — criterio de aceptación negativo: cero literales `"schema_version"` en el árbol. Los
lectores de init ajeno se RETIRAN (absorb ya los tiene), no se mejoran, y mientras convivan hay una
prueba que compara las dos salidas y falla si divergen. Lo que takana aporta —el lector `proc`, que
lee lo VIVO porque `rc-status` miente— va hacia absorb, no en paralelo.
Y la herramienta NO se muda a tawasuyu: la mudanza habla de recetas, perfiles, store y ArtifactHash.
Lo que cruza la frontera es el tipo, no la herramienta.
Dos precondiciones que no se saltan: `card` es uno de los 26 repos sin copia fuera de gioser (hacer
que el bootstrap dependa en compilación de un repo que sólo vive en la máquina que se borra es
convertir un problema conocido en un bloqueo de arranque); y `STAGE1_SEED_CARD` ENTRA EN EL HASH del
bootstrap, así que reserializar con card-core puede mover el baseline del selfhost — se mide con
`takana hash` antes, y si se mueve es una decisión propia. La mudanza no tiene ese problema y puede
adoptar el tipo primero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
`fuera_de_git()` era una lista cableada de 10 rutas con `~` adentro, y el censo corre como root:
`~` se expandía a `/root`. La huella quedó en `work/mudanza/censo-gioser.toml`, donde `~/.ssh` y
`/root/.ssh` salen IDÉNTICOS. Lo que nunca se censó por su nombre:
/home/sergio/.config/takana ⚠ la clave PRIVADA de release (el .pub está EN el repo)
/home/sergio/.config/gh el token con el que se crean los espejos de los 26 repos
/home/sergio/.ssh/config el bloque `git.gioser.net` `Port 2345`, sin el cual no se clona
/home/sergio/.gitconfig los `insteadOf`, que reescriben remotos y esconden que uno es local
/home/sergio/.claude 6,8 G la memoria y los transcripts del proyecto
Ninguna de esas falla el día que se borra la máquina: la clave falla la próxima vez que alguien
firma, y para entonces no hay de dónde sacarla. Hoy sólo viajaban dentro del bloque `/home` (22 G,
`destino = ""`), o sea sin que nadie las hubiera mirado.
Cuatro cambios:
· **La lista sale a `rutas-fuera-de-git.txt`**, un fichero de datos con el motivo de cada ruta. Una
lista cableada se queda vieja sin que nada falle: la anterior preguntaba por `~/.config/hammer`
—muerto desde el renombre, existe VACÍO— y no preguntaba por `~/.config/takana`.
· **`homes()` lee `/etc/passwd`** y expande `~/…` por cada home real (root incluido, cuentas de
servicio con `nologin` fuera). En gioser: 6 homes, 19 entradas contra las 9 de antes.
· **Tres estados, no dos.** Un `ls` sin permiso se lee igual que un directorio ausente: `ausente` se
descarta, `sin_permiso` se REPORTA («2 rutas EXISTEN y no pude leerlas») para que nadie decida
sobre una lista incompleta creyéndola completa. Probado corriendo el censo como no-root.
· **Una sola llamada** en vez de una por ruta: N rutas × M homes por SSH era el censo tardando más
que el trabajo que describe. Y cada entrada lleva dueño, tamaño y por qué importa.
Y el paso que faltaba en `planear.py`: `fuera_de_git` no lo consumía NADIE, así que el censo lo
listaba y el plan no proponía copiarlo — se veía y no se actuaba. Ahora es el paso 0 bis, pegado al
del código, con revisión humana para decidir y, por cada ruta `muda`, su `rsync -aHAX --numeric-ids`
(permisos: una llave con el modo cambiado no falla al copiarse, falla al usarse) verificado POR
CONTENIDO: el digest de los digests, que no revela nada y caza lo que contar ficheros no caza — una
llave truncada cuenta como un fichero igual que la entera. Probado en los dos sentidos: idéntico ⇒
0, un byte distinto ⇒ 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
La sospecha salía del propio serial: el arranque del §6.10.ter veía DOS dispositivos DRM
(`drm: card0 card1 renderD128`) y uno nuevo ve uno solo. Sin `-vga none`, QEMU agrega una VGA
estándar además del virtio-gpu, OVMF pinta su GOP ahí y el kernel levanta simpledrm encima: un
compositor con dos tarjetas puede componer en la que el `screendump` no muestra, y eso se ve igual
que «la ventana no aparece». Por eso el guion lleva `VGA=1`, para pedir ese caso por su nombre.
Medido con las dos configuraciones y el mismo lanzamiento:
-vga none → card0 → 703766 px magenta
VGA=1 → card0 card1 → 703766 px magenta
El mismo número al píxel. La ventana aparece —panel, dock y el navegador con su barra lateral—,
`nsWindow::Create() Toplevel` está en el MOZ_LOG, la superficie se mapea y `mIsFullyOccluded 0`, con
`WaylandBufferSHM` de 1280x696, el mismo camino de buffers que bajo sway.
⇒ la segunda pantalla no es la causa. Queda una sola diferencia con aquella corrida: cómo se lanzó
el navegador. Acá va con perfil nuevo y MOZ_ENABLE_WAYLAND puesto; allá fue un `atuq` pelado con su
perfil por defecto tecleado en el serial. Para eso está `--como-usuario`, que es la corrida que sigue.
⚠ Dos trampas del arnés, medidas y escritas en el guion: el terminal hace ECO de lo que se le
escribe, así que una marca de fin literal se lee en el eco y las órdenes se pisan; y `cmd & ; echo`
es error de sintaxis en ash ⇒ el navegador no se lanzó y la corrida siguió dando capturas de un
escritorio vacío, que se leen igual que el fallo que se investiga.
Deja escrito lo de hoy donde ya vivía el diagnóstico de esta clase de fallo (§9):
el aprovisionamiento de `go` en el worker SE HIZO el 2026-08-28 y el renombre lo
deshizo el 2026-09-09 sin que nada fallara. Con el método de detección (varias
muriendo en segundos = sistémico; leer el error de UNA antes de contar deuda), la
comprobación en los dos sentidos, y las cuatro sondas de `pgrep` que el mismo
renombre dejó ciegas.
Incluye el no-determinismo de `gocryptfs` con la hipótesis REFUTADA anotada como
tal: no es el `cgo` (sq y usql también lo usan y reproducen) ni la ruta aleatoria
del temporal (las cadenas son idénticas y difieren 1.874.136 bytes, desde .rodata).
Queda abierto y sin causa; no está en ningún perfil, así que no bloquea imágenes.
Hub, KDE Frameworks tanda 3: kglobalaccel kpackage kconfigwidgets kiconthemes
ktextwidgets kxmlgui kparts kcmutils knotifications kdeclarative ksvg kwallet.
REPRODUCEN: 12 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
Worker (ya con `go` arreglado), familia Go:
REPRODUCEN: 15 · DERIVA: 1 · NO-DETERMINISMO: 1 · no construyeron: 0
· `checkmake` — la que antes «no construía» — REPRODUCE. Cierra el arreglo del enlace
de `go`: no estaba rota, faltaba el binario en el PATH del host.
· `cilium-cli` DERIVA: el artefacto guardado era viejo; las dos reconstrucciones
coinciden entre sí ⇒ se movió el mundo, no la receta. No es un fallo.
· `gocryptfs` NO-DETERMINISMO, y es el primero de verdad del frente Go.
La causa de gocryptfs, con los dos ejemplares guardados en `store/.divergen/`:
usr/bin/gocryptfs ELF, difieren [.text .rodata .data .debug_*]
· .debug_str sólo en A: …/.gotmp/go-build3432240771/b141
· .debug_str sólo en B: …/.gotmp/go-build2186295412/b141
El directorio temporal `go-build<aleatorio>` queda horneado en el DWARF. Hipótesis a
comprobar, no conclusión: sólo pasa con `cgo = true`, porque cgo genera fuentes C en ese
temporal y su ruta real entra en la info de depuración; con CGO off no hay fuentes
generadas y las 15 restantes reproducen. En el corpus hay exactamente cuatro recetas Go
con cgo: gitea, gocryptfs, sq y usql — ninguna verificada todavía. Se miden antes de
afirmar nada.
La mudanza de gioser pasa por levantar su gitea del otro lado — 28 repos, 26 sin copia fuera. El
corpus ya tenía `gitea` sellado (1.26.4, `b3:35bb4f04…`) y NO sirve, medido contra el artefacto:
$ gitea --config app.ini migrate
Error: sqlite3 requires: -tags sqlite,sqlite_unlock_notify
this Gitea binary was not built with SQLite3 support
Construía, reproducía y no podía abrir la sqlite en la que gioser guarda TODO (`DB_TYPE = sqlite3`).
Y `gitea --version` decía `version development`. Es subcomando-sin-driver un piso más abajo.
Tres cambios, y el tercero es el que no se ve venir:
· Pin 1.27.0 (gioser corre 1.27.0). No es cosmético: gitea migra el esquema hacia adelante y se
planta si la DB trae migraciones más nuevas que el binario, así que el pin va por delante del
origen, nunca por detrás.
· Tags `sqlite,sqlite_unlock_notify` (⇒ `cgo = true`, el driver de mattn es C) y `bindata`, que
embebe `public/` y `templates/`: sin él la receta publica sólo el ejecutable y el servidor
arrancaría sin UI.
· La fuente pasa de `repo`+`commit` al TARBALL DE RELEASE. El clon no alcanza: la UI se compila
con Node y las deps Go no están vendoreadas en git. El `gitea-src-1.27.0.tar.gz` trae las dos
cosas hechas — medido: `public/assets/` 764, `vendor/` 11246, `templates/` 663 — así que
construye con el lab tal cual, sin Node y sin red. La URL es locator (ADR 0013); ancla el sha256.
Nuevo hash: `b3:391a613e…`. Declarado en `perfil.servidor` como paquete y NO en `servicios`:
arrancarlo necesita el [[service]] del SDD 30 con su app.ini, y declarar un arranque que no existe
sería la mentira que ese campo existe para no tener.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Tanda 2 de la familia KDE Frameworks (hub): kbookmarks kcolorscheme kcompletion
kdnssd kholidays kunitconversion kauth kpty solid sonnet prison kservice.
REPRODUCEN: 12 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
Y aparte: el worker tenía su propio libro (`work/repro-worker.tsv`, fuera de la siembra)
con 13 verificaciones del 2026-09-05 que nunca volvieron al hub. Cobertura medida y
perdida. Importadas 10.
Las otras 3 NO se importan, y el motivo es el que justifica que la clave del libro sea
(receta, ArtifactHash) y no la receta sola: su hash cambió desde entonces, así que la
medición ya no habla del artefacto de hoy.
· atuq, bzip2 — reproducían, pero a un hash superado
· aichat — decía NO-DETERMINISMO a `c40a15bd…`; el hash vigente es `7a515b0a…`
y a ÉSE el propio worker le midió «reproduce». Importar por nombre
habría arrastrado un no-determinismo que ya no existe.
Todas buscaban el proceso `release/hammer`, y desde el ADR 0016 el worker invoca
`./target/release/takana`. Ninguna daba error: simplemente no encontraban nada.
Medido hoy, no deducido: `estado-granja.sh` imprimía «moliendo: (nada — idle o entre
colas)» mientras en el worker corría
`./target/release/takana --store ./store build recipes/centrifugo.toml`. Leyendo ese
informe se concluye que la granja está seca. Tras el arreglo, el mismo informe dice
«moliendo: cilium-cli.toml» y `pgrep` allá confirma exactamente ese proceso.
Las cuatro:
· estado-granja.sh «moliendo» — mentía sobre si hay trabajo
· farm-worker-loop.sh×2 detección de build en vuelo
· deadman.sh `hay_trabajo` — y acá el precio es un worker BORRADO a mitad
de un build. En el LXC no llegó a morder (sólo borra cajas
hcloud y su timer no está activo allá), pero la próxima caja
de pago sí lo habría pagado.
Se aceptan LOS DOS nombres: cargo sigue compilando `hammer` como alias, y una sonda que
sólo mira el nombre nuevo se rompería con un worker que arrastre binario viejo — que es
justo lo que pasa acá, porque la siembra excluye `/target`.
Es el mismo renombre que dejó colgado el enlace de `go` (commit anterior). Un renombre
no rompe sólo lo que compila: rompe las cadenas que alguien escribió a mano.
El §6.10.ter dejó dos variables cambiando a la vez (compositor y arranque real) y ninguna medición
que las separe. `scripts/cosmic/atuq-en-cosmic.sh` saca la primera de encima en ~2 min por vuelta,
sin QEMU y sin imagen: sway headless de andamio, cosmic-comp nesteado encima por winit, atuq adentro,
y `grim` capturando del lado de sway. El `--control` corre EL MISMO binario de atuq —el del cierre
hidratado para la imagen— directo sobre sway.
Las dos capturas coinciden al píxel en la región de la página (586331 px magenta, mismo bbox), y no
es que el navegador se escape a sway: antes de lanzarlo, la ventana de cosmic-comp ocupa 1276x637 en
ese mismo rectángulo, y el MOZ_LOG del widget ve `mode output size 1276 x 637` con cosmic contra
`1280 x 720` en el control. Las dos corridas enteras difieren en 1130 píxeles, todos en las barras
de título.
⇒ cosmic-comp no es el que impide la ventana. Queda como variable el arranque de verdad: kms/DRM
sobre virtio-gpu en vez de winit, el PID1 de arje, el seat.
De paso queda anotado el camino de buffers cuando SÍ funciona —`WaylandBufferSHM`, memoria
compartida y no dmabuf—, que es la línea de base contra la que comparar la traza de la imagen.
Y `scripts/cosmic/atuq-en-imagen.py` para medir allá: arranca la imagen, maneja el serial, lanza el
navegador con el mismo MOZ_LOG y pide capturas por QMP. La corrida del §6.10.ter fue a mano y no
dejó un solo log que se pueda releer.
El enlace `~/.cargo/bin/go` del worker apuntaba a `/opt/hammer/.dev-fs/tools/go/bin/go`
— la ruta de ANTES del renombre a takana (ADR 0016, 2026-09-09). `/opt/hammer` ya no
existe, así que el enlace estaba COLGADO desde entonces.
Nada falló. `go` lo invoca takana DEL LADO DEL HOST (`go mod vendor` durante el fetch,
con red), fuera del sandbox, así que el rootfs no lo cubre — es el mismo modo de fallo
que `patch` en agosto, y `vps-setup.sh` ya lo tiene escrito como regla: «toda herramienta
que takana invoque HOST-SIDE va en esta lista». `go` no estaba.
Cómo se descubrió: por casualidad, verificando reproducibilidad. 16 de 25 recetas Go
murieron en SEGUNDOS. Un fallo sistémico se lee como 16 recetas rotas si nadie abre el
error — el verificador manda la salida del build a /dev/null. El error era
`spawn go mod vendor: No such file or directory (¿está `go` en el PATH del host?)`, que
lo decía todo.
Arreglado en el worker (enlaces repuestos a /opt/takana) y COMPROBADO con un build real:
`gron` no construía, ahora construye y reproduce bit a bit (why-differs: 5 entradas
idénticas, 0 divergen).
Acá va la parte que evita la repetición: el script daba «go enlazado ✓» sin comprobar
nada — `ln -sfn` crea un enlace colgado tan campante. Ahora repone un enlace colgado
preexistente (avisando) y comprueba que RESUELVE ejecutando `go version` A TRAVÉS DE ÉL;
si no ejecuta, sale 1 con el síntoma escrito. Un enlace que existe no es un `go` que corre.
Probado en los tres sentidos, con control que TIENE que pasar:
· enlace sano ⇒ ok, rc=0
· enlace colgado a /opt/hammer ⇒ avisa, lo repone, rc=0
· destino inexistente ⇒ falla ruidoso, rc=1
Barrido por FAMILIA (KDE Frameworks tier 1, todos CMake), que es lo que hace
legible el censo: si el fallo fuera del toolchain se concentraría acá y saltaría
a la vista. karchive kcodecs kconfig kdbusaddons kguiaddons ki18n kidletime
kitemmodels kplotting kwidgetsaddons kwindowsystem kcrash.
REPRODUCEN: 12 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
Los 12 además CONSTRUYEN hoy, que es el otro hallazgo del verificador: `sealed`
sólo dice que alguien los construyó alguna vez con algún lab, y el lab rueda.
Estaba sólo en `recipes/incoming-wlr/`, o sea inalcanzable desde las otras
cuatro colas. `cliphist` vive en el CORPUS y la pide en `[deps] run`, así que
corpus, gnome, kde y cosmic la reportaban como `orphan_deps: ["wl-clipboard"]`.
No era cosmético: `escritorio-cosmic` declara `cliphist` de raíz, y la imagen
se habría armado con el demonio de historial y SIN `wl-copy`/`wl-paste` — el
modo de fallo silencioso que la propia receta de cliphist advierte (el binario
corre y no hace nada). La clausura no lo podía ver: decía `falta=0` en los
cinco perfiles, porque una dep que no resuelve no se cuenta, desaparece.
Solución = la que el catálogo ya usó con `mpv` y `atuq`: subirla al corpus, de
donde las colas la alcanzan (sibling-first y después el catálogo padre).
Medido antes de mover: ninguna de sus 9 deps tenía variante hermana en
`incoming-wlr`, así que el ArtifactHash no podía cambiar — y no cambió
(`b3:fc2ab7b7…` a los dos lados, comprobado con `takana hash`). Cero re-sellos.
Tras regenerar los cinco grafos: `orphan_deps: []` en los cinco, y la clausura
de `escritorio-cosmic` pasa de 273 a 274, sellada.
Los demonios de sistema pasan de lanzarse con `&` desde un script de 500 líneas
a ser Cards del `genesis`. El bloqueo que este frente daba por corpus se levantó
solo: otro agente selló evolution-data-server y gnome-shell mientras esto se
escribía, y escritorio-gnome quedó 309/309.
SE CORRIÓ UN CONTROL PRIMERO, y es lo único que hace interpretable el resultado:
colord control: `MURIÓ al arrancar` cards: `ya vive (pid 175)`
ColorManager control: `NO apareció en 40s` cards: `OK`
login1/Accounts/UPower OK en los dos
compositor wayland-0 y shell vivo en los dos
El fallo del control DESAPARECIÓ, y no lo buscaba: colord moría arrancado por el
script y vive arrancado por arje, con el bus ya listo porque la espera está
dentro de su argv. Sin el control, «ColorManager OK» sería un dato suelto en vez
de una diferencia. Los PIDs lo confirman: polkit=119, colord=175, upowerd=178,
accounts=180 — de antes de que el lanzador de sesión existiera.
QUÉ NO SE COMPROBÓ: no hay screendump; QEMU salió por timeout y el control
tampoco lo tuvo. La comparación es serial contra serial y lo que se afirma es
sobre los DEMONIOS, no sobre el pintado.
Las piezas donde corresponde: `takana service-cards` (UNA sola implementación de
receta→Card; el formato es contrato con card_core::Card), `targets.py
--service-paths` (une qué-es con si-arranca), y un inyector en FICHERO APARTE
porque anidar dos heredocs de python falló en vivo — el terminador del interno
cerró el externo y media cosa corrió como shell.
La espera del bus va DENTRO del argv de las 5 recetas de sistema: sin ella un
daemon arranca antes de que dbus escuche y queda en modo idle sin registrar su
nombre — un fallo que no se ve, porque el proceso vive y el bus no lo tiene. Los
5 hashes intactos. Y el guardia de gnome-start es por «¿está corriendo?», no por
una perilla: así es correcto venga de donde venga el proceso y la misma copia
sirve donde no se inyectaron cards.
Segunda tanda del censo con `verificar-repro.sh`, esta vez sobre `poppler-glib` y las dos recetas
CMake de la cola de GNOME: **2 de 3 no construyen**. Con esto el censo va en **7 rotas de 18
barridas**, todas por el mismo crash del `lld` de zig con el `--dependency-file` de CMake ≥3.27.
⚠ **`libical` cae enlazando `libical.so` — una librería COMPARTIDA, no un ejecutable.** Eso termina
de enterrar el predictor barato que intenté ayer («falla la que instala binarios», 12 aciertos de
14): el crash puede estar en CUALQUIER link del build. Ni ejecutables instalados, ni sólo
ejecutables: cualquier link.
`evolution-data-server` muere en `camel-lock-helper` y `camel-gpg-photo-saver`, y necesita su propia
perilla además de la de su dep — arreglar `libical` no la arregla, porque el fallo está en SU link.
Las dos estaban selladas y rotas a la vez. El artefacto tapaba que la cola de GNOME ya no se puede
reconstruir entera.
Los tests no eran el punto: el punto era que un servicio DECLARADO en una receta
termine supervisado por PID 1. `product-boot-test.sh` sobre el product-rootfs de
la ruta real da `ok card sshd en la seed` y `PRODUCT_SSH_OK uid=0`, sin ningún
eslabón escrito a mano:
recipes/openssh.toml [[service]] → sidecar dentro del artefacto sellado →
service_cards() → genesis de /ente/seed.card.json → arje-zero encarna sshd →
la sesión SSH responde.
Y HABÍA QUE DESCARTAR QUE LO MOVIERA ESTE CAMBIO: el product-rootfs salió con
hash distinto (5011955a… → 8639894332…). No fue la seed — las dos son
BYTE-IDÉNTICAS (diff del JSON, cero diferencias). Cambiaron `netup`, que es otro
artefacto que cuando se selló el viejo, y por arrastre `ente/attest.json`, que
registra su BLAKE3. O sea: la constante y la receta producen la misma seed,
comprobado sobre el árbol real y no sólo en un test.
Y queda escrito por qué el ESCRITORIO no se puede arrancar supervisado todavía,
que es corpus y no diseño: gnome-shell en deuda (bloqueado sólo por
evolution-data-server, que tiene blocked_by vacío) y sway sin sellar. Sin
compositor no hay sesión. NO se cablearon los scripts de imagen a ciegas: inyectar
cards en un genesis que no se puede bootear es escribir código que nadie puede
contradecir, y este mismo doc ya tiene el ejemplo de qué pasa entonces (el SDD 06
afirmando meses un lector que no existía).
Primera prueba de `atuq` dentro de una imagen de disco arrancada, y el resultado no es el de la
jaula. La cadena previa funcionó, y eso también es medición: el perfil `escritorio-cosmic` hidratado
con el cierre de hoy (275 nodos, 8,4 G) trae `/usr/bin/atuq`, `/usr/bin/llama-server` y el modelo —
declarar en el perfil SÍ pone las cosas en la imagen—, la imagen EFI de 12 G arranca en QEMU y COSMIC
pinta panel y dock en ~2 min.
Y `atuq` arranca —sus extensiones inician, la del foco sondea cada minuto, WebRender inicializa— sin
que la ventana aparezca nunca. Descartado, para que nadie lo repita: no es el sandbox de Gecko
(relanzado con los cinco MOZ_DISABLE_* puestos, idéntico), no es que el proceso muera (sigue
ejecutando el JS de las extensiones), y no es la IA (pasa con about:blank).
La pista: bajo sway headless el MISMO artefacto pinta perfecto y hay capturas del panel contestando.
La diferencia son el compositor (cosmic-comp+llvmpipe contra sway+pixman) y el arranque real contra
bwrap. Es su propia unidad de trabajo, no un parche apurado.
⚠ La lección del método: quince guardianes en verde, `vigia-imagen.py` en ✓, y el navegador igual no
se puede usar en la imagen. «Sella», «hidrata» y «los tests pasan» son tres cosas distintas de
«arranca y se ve».
De paso, `metal-iso.sh` acepta ahora STORE y AUG por entorno, porque en esta máquina no funcionaba:
arma el rootfs con `cp -al` desde el store y el store es un BIND-MOUNT del volumen — `linkat` rechaza
cruzar mounts aunque sea el mismo disco, así que hay que nombrar los dos lados dentro del mismo
mount. Es la cuarta vez que aparece el mismo EXDEV hoy.
`scripts/atuq-captura-ia.py`: el arnés de sway headless de los guardianes + `grim` + un PNG. No es un
guardián (los veredictos siguen saliendo del `dump`); es la evidencia que ningún log puede dar: que
el panel se pinta y que la respuesta está ahí.
Encontró tres cosas, ninguna visible en un log:
1. **la barra lateral se abría SOLA** en el primer arranque, ocupando un tercio de la ventana. Gecko
lo hace al instalar una extensión con `sidebar_action`, y para un navegador que la trae de fábrica
eso es imponerle un panel a todo el mundo. `"open_at_install": false`, con captura del después;
2. el diálogo «Close Firefox» en la segunda corrida — el arnés mataba el navegador sin despedirse y
quedaba el `.parentlock`. Arreglado en el arnés;
3. dos barras de notificación VACÍAS en el arranque. El atajo era reportarlas como fuga de marca, y
era falso: instrumentando una copia del artefacto para volcar el DOM salieron
`sandbox-content-disabled` (lo apaga el propio arnés) y `startup-restore-session-suggestion` (por
el cierre abrupto anterior). El texto está en el DOM y no en los píxeles — comprobado además
quitando nuestro CSS, con el mismo resultado ⇒ es el render por software de la jaula.
⇒ Una captura muestra síntomas; el DOM dice de quién son. Sin ese segundo paso, dos de los tres
habrían entrado al SDD como bugs nuestros.
El log del navegador viaja junto a la foto, siempre: una captura muestra que algo se ve raro y no por
qué. Y `test-atuq-ia.py` + `test-atuq-archivo-semantico.py` siguen verdes tras el cambio.
mesa-llvmpipe b3:7662f5ec25c42cd679da1679c8c6ed7c53cd49d57fa781493ab7ff433d51b9b0
En vez de lanzar 153 rebuilds a ciegas, la campaña se prueba en la única de las tres mesa que tiene
**radio 0 y cero imágenes** (`yupana radio mesa-llvmpipe`: 0 dependientes, «(ninguna declarada)»).
Cambiarla no tira un solo sellado ajeno, así que es el laboratorio exacto.
LA PREGUNTA, que hasta hoy nadie tenía contestada: con `-Dglvnd=true`, ¿mesa produce de verdad el
VENDOR que al despacho le falta? RESPUESTA MEDIDA SOBRE EL ARTEFACTO:
usr/lib/libEGL_mesa.so.0 ← el vendor (hoy no existe en ningún lado)
usr/share/glvnd/egl_vendor.d/50_mesa.json ← su registro, que es lo que glvnd busca
usr/lib/libgbm.so.1 · libglapi.so.0 · dri/{swrast,kms_swrast}_dri.so
…y **deja de instalar libEGL.so.1 y libGLESv2.so.2** (los dos `ls` fallan).
⇒ La colisión de sonames del rootfs de KDE —dos libEGL.so.1 distintos en la misma ruta— DESAPARECE
POR CONSTRUCCIÓN con la campaña: mesa ya no reclama esas rutas y el dueño pasa a ser glvnd, que es
exactamente lo que su partición quiere. La campaña deja de ser una apuesta: se sabe qué produce.
Lo que queda por decidir es CUÁNDO pagar, no SI funciona.
EL PRECIO, medido antes de empezar y sin cambiar: `yupana radio mesa` = 148 directos, 154
transitivos, **153 sellados que caen a deuda**, las CINCO imágenes, 123 de ellos en incoming-kde —
la cola que otro agente está moliendo ahora mismo. Por eso esto es una prueba, no la campaña.
⚠ Y cubre SÓLO la mitad de mesa. La otra es que `libglvnd` vuelva a instalar su libEGL.so.1 de
despacho (hoy va `-Degl=false` justamente para no colisionar). Las dos mitades van en la MISMA
campaña o el resultado es el de hoy con otro disfraz. Queda escrito en las dos recetas.
── 1. Se RETIRA incoming-kde/networkmanager.toml ───────────────────────────────────────────────
networkmanager-qt b3:78bbdcda00d758fb3cc3a0be7da6a67395dfbe0e4d55c5d8e5cf8afaf728ff72
plasma-nm b3:496a9ff7794ae5d89ac2842bdfbca38c25c91df37465a049db7cf75ee0041b82
Ayer se promovió NetworkManager al corpus CON WiFi porque la variante de KDE iba con -Dwifi=false,
y quedaron dos. Esto cierra el arco: retirada la de la cola, `networkmanager-qt` resuelve al PADRE
y la distro tiene UNO solo. Consecuencia concreta: **KDE pasa a tener WiFi**, porque su applet
hablaba con un demonio construido sin soporte inalámbrico.
Medido con `yupana radio` antes de borrar: 2 rebuilds (networkmanager-qt, plasma-nm), los dos en
la cola KDE. Y comprobado sobre el ARTEFACTO del corpus que publica lo que nm-qt pide —`libnm.pc`
y `usr/include/libnm`— antes de quitarle el suelo. Los dos sellan.
── 2. upower al corpus: COSMIC dibujaba una batería sin nadie que se la contara ────────────────
libgudev b3:87ca5e73… · udev-pc b3:fa880b5c… (hash IDÉNTICO desde las dos partes ⇒ cache-hit)
upower b3:c82e655ff7011150f21f9413433c518f12c827c52ecf6f9fc3484f75d85c155c
Ayer escribí que esta cadena eran «3 promociones, una de ellas polkit, decisión de arquitectura».
Fui a mirar y polkit SALE de la ecuación con una perilla: la variante de KDE va `-Dpolkit=enabled`
«porque polkit YA está sellada» —cierto EN ESA COLA y falso desde el corpus—, y apagarlo cuesta
poco medido en FUNCIÓN: polkit en upower sólo gobierna las acciones privilegiadas (suspender e
hibernar por org.freedesktop.UPower, además deprecadas: hoy eso lo hace logind). Reportar batería,
carga, tiempo restante y línea de corriente NO pasa por polkit, que es justo lo que COSMIC quiere.
Se corrigió además la frase del comentario heredado que decía `-Dpolkit=enabled` al lado de un flag
que ahora dice `disabled`: una nota que desmiente al código de al lado es peor que no tener nota.
Y se le añadió su [[service]] (el corpus no lo traía; el label/id son los mismos que en GNOME a
propósito: el ULID identifica al SERVICIO, no al artefacto). No mueve el hash.
Comprobadas las NEEDED de upowerd con provee.py: libupower-glib, libffi.so.8, libz.so.1,
libudev.so.1 — las cuatro las publica el corpus y ya estaban en [deps]. Sin NEEDED colgante.
── 3. targets.toml ─────────────────────────────────────────────────────────────────────────────
escritorio-kde servicios += NetworkManager (llegaba por clausura de plasma-nm y NADIE lo
arrancaba: el applet sobre un demonio apagado)
escritorio-cosmic paquetes += upower · servicios += upowerd
⚠ Y se CORRIGE la nota de ayer que decía «NO declarar networkmanager en escritorio-kde porque
plasma-nm ya arrastra la variante de la cola». Ya no hay variante de cola.
El emisor lee la receta que viaja DENTRO del artefacto sellado (el sidecar
.hammer/recipe.toml) y no de recipes/, porque seal_product_rootfs sirve a las
DOS vetas —product desde recetas locales y product_from_repo desde el repo
firmado— y sólo una tiene recipes/ a mano. El sidecar es la única fuente que
ambas comparten, que es lo que sostiene que converjan al mismo product-rootfs.
LA TRAMPA QUEDA ARMADA, NO CERRADA: el sidecar no entra al ArtifactHash, así que
un artefacto anterior al bloque [[service]] es un cache-hit válido que no se
reconstruye solo. Si el emisor devolviera lista vacía sin protestar, el producto
saldría booteando, verde y SIN SSHD. Falla ruidosamente, y hay un test que exige
que el mensaje siga explicando la causa Y dando el arreglo (regla 3 del
CLAUDE.md: un ausente falla ruidosamente, un vacío llega hasta el final diciendo
que todo fue bien).
LA MIGRACIÓN, CON CONTROL, y el hecho nuevo que dejó: openssh no estaba
certificado como reproducible ("el criterio es construye y corre", dice su
receta). Manifiesto de 33 sha256+modos ANTES de tocar nada, respaldo por
hardlink, borrado, rebuild bajo flock -o, comparación. Resultado: 33/33
ficheros, modos idénticos, y la ÚNICA diferencia en todo el árbol es
.hammer/recipe.toml. openssh REPRODUCE BIT A BIT.
Gotcha que costó el primer intento: store/ es bind-mount de /dev/sdb y work/
vive en /dev/sdc ⇒ `cp -al` cruza mounts y muere con EXDEV. El respaldo va
DENTRO del mount del store; un dot-dir ahí es invisible para store-gc.sh, que
sólo mira dirs ^[0-9a-f]{64}-.
`vigia-sonames.py` corría por defecto SÓLO sobre los cinco perfiles de escritorio
(`p.startswith("escritorio")`). O sea que `base`, `cli` y `servidor` —el objetivo de la mudanza—
**nunca se miraron**. Apuntándolo a ellos a mano:
== servidor 147 nodos · 183 sonames provistos · 1 sin proveedor
FALTA libffi.so.8 ← lo piden: python3
`python3` no arrancaba en las tres imágenes, por el mecanismo EXACTO que `musl-shared` y
`zlib-shared` arreglaron dos días antes: la receta canónica de libffi es `--disable-shared`, ningún
artefacto del cierre publica ese soname, y dentro del lab el agujero lo tapa el rootfs de Alpine —
así que el build pasa y la imagen sale rota. `libffi-shared` ya existía en el corpus y ya estaba
declarada en otros dos perfiles; sólo faltaba en `base`.
servidor 148 nodos · 186 sonames · **0 sin proveedor** (base y cli, igual)
## Y el vigía pasa a mirar TODOS los perfiles
⚠ **Es la tercera vez en dos días que un guardián de este repo mide menos de lo que su resumen
afirma**: `static-audit.sh` sólo globeaba `recipes/` y no las 5 colas (3 mentiras ocultas); el bloque
de raíces sucias sólo corría al hidratar un perfil y no veía las 649 recetas fuera de toda imagen
(qdrant con 69 M de basura); y éste dejaba fuera medio sistema. **Un «0 huecos» sobre la mitad del
sistema se imprime igual que uno sobre todo él.**
⇒ Al leer un guardián, leer su SCOPE antes que su veredicto: el glob, el bucle, de dónde saca la
lista. Los ocho perfiles dan 0 sin proveedor, así que ampliar el alcance no mete ruido — sólo deja de
esconder.
Corrida real contra gioser: los 21 repos fallaron al crearse y el guión dijo «no se pudo crear» A
SECAS, porque mandaba el stderr de `gh` a /dev/null. La causa era el LÍMITE DE LA API DE GITHUB
agotado —que se resuelve esperando— y sin el mensaje eso parecía un problema de permisos, de nombre o
del propio guión: hubo que reproducirlo a mano para enterarse de algo que la herramienta ya sabía.
Dos arreglos:
· El error de `gh` se muestra, recortado a su primera línea.
· Ante «rate limit» se CORTA en el primero: los 20 restantes van a fallar igual y cada intento gasta
cuota. Dice a qué hora se repone (leído de `gh api rate_limit`) y recuerda que reintentar es
seguro, porque el guión es idempotente.
No quedó estado parcial de la corrida fallida: el `pushurl` se agrega DESPUÉS de crear el repo, así
que los 21 clones siguen con su remoto original intacto. Verificado en tres de ellos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
El vigía escribe `docs/state/subcomandos.txt` en cada ciclo del latido desde ayer, y decía
`TOTAL: 44 herramientas selladas que no se pueden invocar` a secas. Un número grande, constante y sin
explicación al lado entrena a saltearse el informe — y entonces el hueco NUEVO, que es el único que
pide acción, pasa desapercibido entre el ruido del viejo. Es la misma trampa que ya tienen resuelta
`static-audit.sh` (deuda decidida) y `--auditar-raices`.
Los 44 son todos del mismo hueco, y ahora el informe dice cuál y por qué:
• falta el binario `cargo` (el toolchain de Rust)
PENDIENTE DE DECISIÓN, no olvido. `cargo`/`rustc` viven hoy en el LAB y en la cadena de
selfhost (mrustc→1.91.1), no como receta del catálogo. Empaquetarlos es una decisión de
TOOLCHAIN —qué Rust publica la distro, y desde qué cadena— no trabajo de granja.
TOTAL: 44 … (44 de hueco CONOCIDO, 0 NUEVAS) ⇒ exit 0
⚠ **«PENDIENTE DE DECISIÓN» y no «deuda decidida»**, que es distinto: en el audit estático las tres
mentiras tienen un arreglo evaluado y descartado por su coste; acá nadie decidió nada todavía. La
etiqueta tiene que decir cuál de las dos cosas es, o dentro de tres meses se lee como cerrada.
El código de salida ahora refleja sólo los huecos NUEVOS. Probado con los dos controles: sacando
`cargo` de la tabla salen `44 NUEVAS` y exit 1; restaurándola, `0 NUEVAS` y exit 0.
⚠ Y el primer intento de ese control estaba MAL y daba verde: escribí `CONOCIDOS = {} or {…}` para
vaciar la tabla, y en Python `{}` es falsy ⇒ la expresión devuelve el segundo dict y la tabla seguía
llena. El control «pasaba» sin probar nada. Rehecho renombrando la clave.
Hace por un árbol entero lo que `scripts/espejo-setup.sh` hace por takana: `pushurl` doble sobre
`origin`, de modo que cualquier `git push origin` que ya exista empiece a espejar sin tocar ningún
script. Crea el repo en GitHub como PRIVADO.
Cuatro decisiones:
· **Por defecto NO hace nada**: imprime qué haría; hay que pedirlo con `--apply`. Crear repos y
empujar código a un servicio externo no es algo que deba pasar por correr un script sin leerlo.
· **Varios clones del MISMO origen van a UN repo remoto.** En gioser hay cinco de
`tawasuyu/tawasuyu`; cinco repos distintos multiplican la confusión y empujarlos todos al mismo
`main` los haría chocar. El primario empuja normal; un secundario sólo se empuja —bajo
`refs/heads/clon/<nombre>/*`— SI aporta historia propia, y eso se comprueba preguntándole al
primario si ya tiene esos objetos (`cat-file -e`). Medido: los cinco tienen CERO commits que no
estén en su remoto, así que los cuatro secundarios se saltan y se dice por qué.
· **Sólo agrega; nunca toca el `fetch` de `origin`**, que sigue siendo el canónico.
· **Lo sucio no viaja, y se dice.** Un espejo guarda historia commiteada, no el árbol de trabajo, y
`tawasuyu` tiene 980 ficheros sin commitear. Creer que está a salvo cuando la mitad del trabajo no
está commiteado es justo el fallo que este guión existe para evitar.
⚠ Y un criterio que escribí mal y corregí midiendo: el primario era «el clon con más commits
alcanzables», y contra los cinco de tawasuyu dio un EMPATE EXACTO en 9343 — `rev-list --all` cuenta
las refs de `origin`, que los cinco comparten, así que la métrica estaba dominada por lo común y no
medía nada. El desempate quedaba en el orden del `find` y elegía `tw-deploy` (HEAD del 6/9) sobre
`tawasuyu` (del 12/9), que es el que se está trabajando. Ahora manda la fecha del HEAD en ISO, que
distingue y ordena como texto.
Ensayo sobre gioser: 25 repos sin copia fuera ⇒ 21 a espejar, 4 saltados por redundantes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
Tras devolver protobuf y abseil a `zig-cc`, qdrant cayó a deuda y se reconstruyó en el worker:
2 ficheros, sólo `usr/`, 70 M, `qdrant 1.19.1`. La limpieza del `/out/src` —el DESTDIR global que
heredaba el `make install` anidado de protobuf-src— aguanta el cambio de dep. REPRODUCE bit a bit.
El corpus queda en 907/907 sellado con deuda 0.