319 Commits
Author SHA1 Message Date
sergioandClaude Opus 4.8 85b5a9bc46 yupana: instrumentar duración de build en el worker → camino crítico PESADO
El peso que le faltaba a las ondas: sin él, critical-path = nº de pasos; con él,
ETA real en segundos.

INSTRUMENTACIÓN (worker):
  scripts/farm/build-timed.sh envuelve `hammer build` midiendo la pared y la
  registra en $STORE/.times/<hash>-<host>.json — un SIDECAR del store, NUNCA
  dentro del artefacto. La duración es no determinista (varía por máquina/carga)
  ⇒ no puede tocar el ArtifactHash. Keyed por hash (CAS-safe: dos workers no se
  pisan) + host (varias muestras por receta). Viaja al hub con el rsync de store
  que ya hace la cosecha; ninguna herramienta del store lo confunde con artefacto.
  Sólo builds REALES (pared ≥ UMBRAL 3s) — los cache-hits no envenenan la mediana.
  campana-deuda.sh ahora construye vía build-timed.sh (transparente, mismo exit).

CONSUMO (hub): yupana._tiempos() carga name→mediana de segundos; keystones
computa el CAMINO CRÍTICO pesado = longest weighted path del subgrafo de deuda
(peso = segundos de build). La cadena más larga hay que construirla en SERIE
aunque haya ∞ workers ⇒ es la ETA con paralelismo infinito. Degrada con gracia:
sin datos, peso=1 y el camino crítico = nº de pasos (lo que ya daban las ondas).

Verificado: con muestras sintéticas da "ETA 19 min sobre 5 pasos, kio → kparts →
frameworkintegration → breeze → plasma-integration"; sin datos degrada a "5 pasos
(SIN datos)". store/.times está git-ignorado (metadata de máquina, no se commitea).

LÍMITE honesto (en la cabecera de build-timed.sh): `hammer build` arrastra deps ⇒
la pared incluye deps no selladas. En orden topológico (drenar) las deps ya están
selladas y la pared mide sobre todo ESTA receta — cota superior buena para pesar.
La precisión exacta pediría instrumentar el sellado dentro de hammer-build (Rust).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:25:09 -04:00
sergioandClaude Opus 4.8 4f609080df yupana: los 2 "bugs" de duplicados eran falsos positivos MÍOS — detector afinado, 0 reales
El usuario pidió arreglar itstool y la sombra de dbus. Apliqué el reflejo
(`yupana radio` + diff completo antes de tocar) y NINGUNO era un bug: ambos eran
patrones deliberados y documentados que mi detector, demasiado superficial, leyó
mal. Si los "arreglaba" a ciegas rompía dos builds.

  · dbus: corpus/dbus (estático) e incoming-kde/dbus (dinámico) NO son redundantes
    — qtbase linkea libdbus-1.so dinámicamente para Qt6DBus. El detector comparó
    sólo versión+sha+deps, no el BUILD. Borrar la sombra rompía KDE.
  · itstool: es un STUB documentado (`[source]=carrier`). El itstool real es
    Python con libxml2-bindings ausentes en el lab; este genera un script inline y
    sólo PRESTA el tarball de gettext-tiny. Apuntarlo al itstool real rompía
    appstream (radio 5) con un source que ni compila acá.
    Los 3 *-hello son el mismo patrón carrier; prison/prison-scanner una variante
    cross-nombre (misma fuente, WITH_ZXING distinto).

FIX = el detector, no las recetas. Tres señales que le faltaban:
  1. huella de BUILD en la firma de sombra (estático≠dinámico ⇒ no redundante).
  2. CARRIER: ≤1 receta del grupo construye la fuente ⇒ el resto presta el tarball.
     Señal: ¿invoca make/meson/cmake/ninja/cargo? (comentarios strippeados — la
     prosa "invocación de meson" de un stub daba falso builder, lo cazó el guardián).
  3. variante cross-nombre: ≥2 builders con BUILD distinto = deliberado, no bug.
Resultado: 0 colisiones reales (invariante c se cumple), 4 carrier + 14 variantes
+ 12 sombras justificadas, todas benignas.

Guardián extendido: ancla que el detector agrupa por FUENTE (itstool↔gettext-tiny)
y NO cría lobos (itstool=carrier, dbus=variante deliberada).

LECCIÓN: el reflejo del radio/diff antes de tocar evitó dos borrados destructivos
guiados por un falso positivo de mi propia herramienta. Medir antes de creer, aun
a la propia yupana.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:14:10 -04:00
sergioandClaude Opus 4.8 619a30187c yupana: keystones — el camino crítico real, con la matemática correcta (AND, no OR)
`yupana keystones` rankea los nudos EN DEUDA por cuánto trabajo BLOQUEADO libera
su sellado (cierre inverso transitivo restringido a la deuda). Sellá de arriba
hacia abajo y la cascada cae lo antes posible.

CORRIGE UN ERROR MÍO: dije "árbol de dominadores = keystones". Lo medí y es
FALSO. El dominador ingenuo asume alcanzabilidad OR (basta un camino), pero
construir es AND (hacen falta TODAS las deps): kcoreaddons depende de dbus/libdrm
/mesa además de qtbase ⇒ hay un camino de desbloqueo que evita qtbase y el
dominador-OR lo pierde. La métrica correcta bajo AND es el cierre inverso
transitivo. Y el crudo lo gana el toolchain (make gatea 313, inútil) ⇒ el
keystone accionable es el cierre restringido a la deuda, y sólo cuenta si el
nudo mismo está en deuda (un sellado ya está disponible, no gatea nada).

Estado actual (qtbase ya sellado, era EL keystone con 117 gateados):
   kio        36   ← sellarlo libera 36 recetas KDE bloqueadas
   kparts     19
   kcmutils   17
   ksvg       14
   libplasma  13
Ése es el camino crítico ahora.

El dominador SÍ entra, en su uso legítimo: la columna `exclusivo` = retención
estilo GC (nodos que sólo dependen de mí). kwin=9 (sus plugins privados),
libksysguard=3. Responde "si suelto esta feature, cuánto más puedo soltar".

Sin datos nuevos (no necesita duración de build). Cuando instrumentemos tiempos,
el mismo cierre se vuelve camino crítico PESADO (ETA real).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:03:56 -04:00
sergioandClaude Opus 4.8 0f3a1dc394 yupana: invariante (c) CANÓNICO — detector de duplicados por evidencia de fuente
`yupana duplicados` caza nudos que son el MISMO paquete bajo nombres/colas
distintos, agrupando por lo que BAJA (sha/url), no por el nombre. Clasifica:

  colisión-fuente — mismo sha, familias de nombre DISTINTAS ⇒ casi seguro un
                    sha copiado por error. ACCIONABLE.
  variante        — mismo sha, misma familia (zlib/zlib-shared, mesa-*) ⇒
                    diversificación deliberada estático/shared, OK (14 vistas).
  sombra          — mismo nombre en >1 cola; redundante si versión+sha+deps
                    coinciden (una sobra), justificada si difieren.

HALLAZGO INMEDIATO — el detector encontró un bug real en su primera corrida:
  itstool.toml dice versión 2.0.7 pero su url+sha apuntan al tarball de
  gettext-tiny (29cc165e…). Construiría la fuente equivocada. radio itstool=1,
  gettext-tiny=78 ⇒ el sha copiado es el de itstool.
Otras 3 colisiones son demos (adwaita-hello/libadwaita, sourceview-hello/
gtksourceview, radio 0) o subcomponentes (prison/prison-scanner) — a revisar,
no urgentes. Y 1 sombra redundante: incoming-kde/dbus es byte-idéntica a
corpus/dbus (mismo hash de artefacto) ⇒ los 114 consumidores KDE podrían
apuntar a corpus/dbus sin rebuild.

NO funde nada solo: reporta a docs/state/duplicados.json y deja el juicio al
humano (como el triaje). El radio de cada nudo dice cuál es el canónico (el de
mayor radio). Guardián extendido: test-yupana-radio.py falla si el detector
deja de ver colisiones de fuente (agrupar por nombre en vez de sha = invariante
c ciego).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:59:20 -04:00
sergioandClaude Opus 4.8 e4a0237b6d yupana: rename khipu→yupana — el registro es el khipu, la yupana es el ábaco que reckona sobre él
`khipu` colisionaba con una app de tawasuyu. En vez de un nombre a dedo, el
significado más profundo RESUELVE la colisión: en los Andes el khipu GUARDABA
(el registro de nudos) y la yupana CALCULABA sobre él (el ábaco). Acá igual, y
la distinción es arquitectura:

  docs/state/build-state.json = el KHIPU  (el registro firme: nudos y cuerdas)
  scripts/yupana.py           = la YUPANA (el motor que reckona: radio, ondas, clausura)

Nunca fue del todo un khipu lo que construimos; era la yupana. La colisión
empujó al nombre más exacto.

git mv preserva historia. Verificado tras el rename: `yupana radio libdrm` da
126/132/[kde,mirada]; el guardián de regresión pasa; build-state importa yupana
y --check sigue en 0. Cero referencias `khipu` colgadas en código.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:53:53 -04:00
sergioandClaude Opus 4.8 4d0a909bbd khipu: guardián de regresión del radio cruzado (la cicatriz de libdrm)
test-khipu-radio.py falla si el radio de un paquete compartido entre colas
vuelve a medirse recortado: verifica que libdrm lo consuman >1 cola y que su
membresía incluya escritorio-kde (la mentira original decía sólo mirada).

Regla que blinda: cuando khipu se equivoca, no sólo se arregla — queda el test
que impide repetirlo. Es la automejora arquitectónica hecha práctica.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:45:37 -04:00
sergioandClaude Opus 4.8 8da6ef6122 khipu: el motor de dependencias inversas que cruza TODAS las colas (y arregla la mentira de perfiles)
El 2026-07-22 un cambio a libdrm (default_library=both, para gtk4) invalidó 118
recetas KDE. El radio se midió contra recipes/*.toml y no contra
recipes/incoming-kde/ ⇒ 8 en vez de 118. Y build-state.py sin --kde cometía el
MISMO error: cargaba sólo el corpus, y su campo `perfiles` AFIRMABA que tocar
libdrm sólo afectaba a escritorio-mirada. Una mentira con confianza.

Causa estructural: las dependencias INVERSAS son un hecho del disco entero, no
de la vista que uno cargó. khipu.py las calcula SIEMPRE sobre todas las colas,
con resolución sibling-first (el nodo se identifica por (cola, nombre), porque
corpus/fontconfig y incoming-kde/fontconfig son nudos distintos).

  QUÉ NODOS PUNTÚO es decisión de vista. QUIÉN ME CONSUME es un hecho.

khipu es la PUERTA ÚNICA de la metodología: radio/perfiles nativos (cruzan todo
el repo) + delega estado/drenar/triaje/frontera/objetivo a sus órganos.

  khipu radio libdrm → 126 directos, 132 transitivos, [escritorio-kde,
                        escritorio-mirada], 11 sellados que caen. El número
                        honesto que habría frenado el cambio.

build-state.py ahora saca `perfiles` y el nuevo `dependientes_total` de khipu
(grafo entero), no de los nodos que cargó. Verificado: el grafo por defecto ya
atribuye libdrm a escritorio-kde; sin_perfil bajó 674→666 (8 recetas del corpus
que sólo KDE consume, ahora bien atribuidas). --check sigue en 0, grafo cierra.

Repo-wide por diseño, no KDE-céntrico: zlib toca las 4 imágenes (233 transitivos),
make 313.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:43:40 -04:00
sergioandClaude Opus 4.8 9b67268a65 triaje: los 170 candidatos de frontera clasificados, y la cadena se ejerce entera
Evidencia dura primero: NINGUNO de los 170 bloquea un build. Toda receta que
pide uno de estos ya está sellada al menos una vez ⇒ hammer construye sin
ninguno. Eso descarta empíricamente "hueco de build" para los 170 y deja sólo
la pregunta de runtime.

VEREDICTOS
  provisto  11 — no faltan: ya los da otra receta con otro nombre. nixpkgs parte
                 en varios paquetes lo que acá es uno solo. Verificado contra el
                 store: wayland-scanner→wayland, mesa-libgbm→mesa (nuestros tres
                 mesa producen libgbm.so.1), libxcb-*→xcb-util-*, poppler-qt6→
                 poppler, gmp-with-cxx→gmp, uname→coreutils.
  nix-ismo   5 — andamiaje de nixpkgs (env wrappers, helpers del stdenv, glibc
                 asomando por su libc).
  opcional 150 — software real que nixpkgs habilita y hammer no necesita, con el
                 porqué agrupado: systemd (usamos arje-zero), X11 heredado (el
                 escritorio es Wayland), Vulkan/shaders, audio/multimedia,
                 conectores de BD, tooling de docs/tests, bindings Python de Qt,
                 paquetería ajena (tenemos .swm).
  hueco      4 — gaps de RUNTIME, no de build: shared-mime-info (sin base MIME
                 no hay tipos de fichero), xwayland (ninguna app X11 corre),
                 polkit-qt-1 (sin diálogos de autorización), qqc2-breeze-style
                 (los controles QML caen a un estilo genérico).

CORRIGE UN ERROR MÍO DE P3: dije que mesa-libgbm/libglvnd eran "el muro de
GBM/EGL". Falso — libgbm ya lo produce nuestro mesa. El muro era softpipe vs
llvmpipe, no un paquete ausente.

CUARTO VEREDICTO NUEVO (`provisto`) con su propio lazo: sale a alias-triaje.txt
y seed-graph.py lo carga en MAPA ⇒ esos 11 nombres dejan de contarse como hueco
para siempre. Junto con nixismos-triaje.txt, el sembrador aprende de su triaje.

Y LA CADENA SE EJERCE ENTERA POR PRIMERA VEZ: los 4 huecos entraron como raíces
de escritorio-kde → nacieron 4 nodos `wanted` (raíces 11, sin receta 4; el grafo
sigue cerrando, --check exit 0) → seed-graph los sembró (42 aristas conocidas,
21 candidatos nuevos de frontera) → drenar.py los ordena marcándolos [semilla],
que es la regla de la fuente única a la vista. qqc2-breeze-style no cae en la
onda 1 porque sus deps SEMBRADAS la traban (kcodecs, kirigami…): el andamio
funcionando como se diseñó.

De paso: qtbase se selló mientras corría esto ⇒ la onda 1 de KDE se abrió de 1 a
13 recetas. El cuello de botella que reportó P4 ya está destrabado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:30:19 -04:00
sergioandClaude Opus 4.8 ec28a9b394 catálogo objetivo P5: tandas a archivo, y el triaje que cierra el lazo
tandas/README.md declara los 55 ficheros ARCHIVO de campañas pasadas (no se
borran: su git log es la crónica de cada frente) y documenta de dónde sale el
trabajo ahora. import-batch.sh -f sigue vivo como vía de escape, no como camino.

Lo que faltaba para que la cola fuera derivada de verdad era el paso humano, y
estaba sin forma: 137 candidatos clasificables sólo en la cabeza de alguien.
triaje.py les da forma durable — docs/state/frontera-triaje.toml, 170 candidatos
unificados de los 3 perfiles. Cada veredicto cierra un lazo distinto:

  hueco    → raíz en targets.toml ⇒ nace un nodo `wanted`, el sembrador lo
             siembra y el drenaje lo ordena (= trabajo nuevo para el worker)
  opcional → registrado, deja de reaparecer como pregunta
  nix-ismo → vuelve a seed-graph.py como descarte ⇒ el sembrador APRENDE de su
             propio triaje y cada corrida sale más limpia

Sincronizar NUNCA pisa un juicio humano: agrega los nuevos y marca ausente=true
los que dejaron de aparecer (progreso, o cambio de nixpkgs — se ve en el diff).
`sugerencia` propone con evidencia mecánica pero `veredicto` arranca en
`pendiente`: la heurística abarata el juicio, no lo cierra.

Lazo verificado punta a punta: python3-3.14.6-env marcado nix-ismo → --aplicar
lo escribe al descarte → seed-graph.normalizar() lo devuelve clasificado y deja
de contarlo como hueco. Probado y revertido; los 170 siguen pendientes.

Cadena cerrada y corriendo: targets.toml → build-state.json → drenaje.json →
worker, regenerada por el latido cada 30min. El único paso que pide una persona
es el triaje.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:20:30 -04:00
sergioandClaude Opus 4.8 25e3878331 farm: JOBS=1 en el worker-loop mientras el ADR 0012 esté sin decidir
`build-farm.sh` usa `xargs -P$JOBS`; con >1, dos recetas que comparten dep disparan la carrera del
árbol de fuentes del ADR 0012 y el árbol queda roto para siempre. Se vio a escala: al invalidar
libdrm, 118 de las 205 recetas KDE pasaron de cache-hit a rebuild real y ~93 murieron con
"/src/.zwrap/cc is not a full path to an existing compiler tool" — el wrapper de zig que la receta
deja en el ÁRBOL DE FUENTE, barrido por el fetch concurrente de otra receta sobre el mismo qtbase.
El reintento serial de build-farm tampoco las recupera: el árbol ya quedó inconsistente.

La caché estaba ocultando la carrera, no evitándola. Serializar es la única mitigación correcta
hasta que el ADR 0012 elija salida.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:15:38 -04:00
sergioandClaude Opus 4.8 b08e582c89 catálogo objetivo P4: la cola de la granja pasa a ser derivada del grafo
drenar.py parte la deuda en ONDAS topológicas (una receta sólo espera por deps
que también estén en deuda; las selladas ya están en el store). Onda 1 =
construible ya; dentro de cada onda, orden por `unblocks` desc.

Da lo que una lista plana no da: el orden correcto, la PROFUNDIDAD real de la
cadena (pasos secuenciales = el reloj de pared) y qué paraleliza sin pisarse.

   base                0 en deuda   imagen completa
   cli                 0 en deuda   imagen completa
   escritorio-kde     77 en deuda   12 ondas   onda 1 = 1 receta: qtbase
   escritorio-mirada   4 en deuda    2 ondas   onda 1 = 3

HALLAZGO: el escritorio KDE está serializado detrás de UNA receta. qtbase
destraba 117 nodos y es lo único de la onda 1 — hasta que no esté sellada no hay
nada que paralelizar, por más workers que se enciendan. Cambia la pregunta de
"¿cuántas faltan?" (77, poco informativo) a "¿cuál es el camino crítico?".

Dos correcciones salidas de mirar la salida:
· el grafo se elige por la `cola` declarada en targets.toml, NO por cuál tiene
  más nodos: incoming-kde SOMBREA recetas canónicas, así que medir
  escritorio-mirada contra el grafo KDE medía una imagen que nadie construye
  (3 en deuda en vez de 4). El mismo atajo estaba en seed-graph.py --frontera;
  corregido en los dos.
· regla de la fuente única implementada en deps_de(): manda la receta si existe,
  la semilla sólo si el nodo es `wanted`, marcando procedencia en la salida.

Enchufado al latido: cosecha-cron regenera docs/state/drenaje.json junto al
grafo y lo commitea ⇒ cola derivada y versionada, el git diff entre ciclos
muestra qué salió de la deuda. NO lanza builds: eso cuesta € y sigue siendo
decisión explícita (farm-up + campana-deuda.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:14:37 -04:00
sergioandClaude Opus 4.8 0093c1f19c catálogo objetivo P3: sembrador de aristas desde nixpkgs, calibrado contra la verdad
seed-graph.py extrae deps de la metadata de nixpkgs sin construir nada (leerla
no viola el ADR 0004, que prohíbe *construir* con nix). `--calibrar` mide la
semilla contra la única verdad disponible: las recetas escritas a mano.

VEREDICTO (60 recetas, 686 deps):
   directas     recall 39%*, precisión 59% (269 de 455)
   transitivas  recall 84%,  precisión  6% (390 de 6760)
   * contra la verdad APLANADA — comparación injusta, y el modo transitivo
     prueba que la información sí está; sólo falta aplanarla.

Decisión: sembrar aristas DIRECTAS, la transitividad la hace el grafo. hammer
aplana la clausura en [deps].build (cada dep = capa --overlay-src), nixpkgs
declara directas y propaga; reproducir el aplanado arrastra 4629 nodos fantasma
del bootstrap de nixpkgs.

Lo que enseñó la calibración (3 rondas, cada fix salido de un dato):
· 4 clases de discrepancia, no 1: convención hammer (make/binutils/linux-headers,
  implícitas en el stdenv de nix), variante nuestra (zlib vs zlib-shared),
  provisto por el lab (cargo/rustc — las recetas Rust declaran deps=[]), y recién
  después ruido real. Contarlas juntas hacía parecer irreducible lo clasificable.
· desalineación aburrida y arreglable: caja (libx11/libX11), implementación
  (gettext→gettext-tiny, ninja→samurai), versión pegada al pname, hooks del
  stdenv. Precisión 45%→59%; "sin attr en nixpkgs" 22/40 → 6/60.
· el escritorio KDE cuelga de kdePackages., no del top-level: sin ese fallback
  el perfil objetivo era insembrable.

Hallazgo de secuencia: hay 0 nodos `wanted` (targets.toml se pobló por
lift-and-shift de lo existente), así que el sembrador no tenía a quién sembrar.
De ahí `--frontera <perfil>`: siembra las recetas que YA existen y reporta lo que
nixpkgs pide y hammer no puede construir. 137 candidatos en escritorio-kde
(kdoctools ×6, milou, polkit-qt-1, libkscreen, y mesa-libgbm/libglvnd/spirv-tools
= el muro de GBM/EGL ya documentado), 51 en cli, 46 en escritorio-mirada.

Cada candidato es una HIPÓTESIS a clasificar a mano: dep opcional no habilitada,
nix-ismo por filtrar, o hueco real. Nada se promueve a receta automáticamente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:01:08 -04:00
sergioandClaude Opus 4.8 a27d9a94a4 catálogo objetivo P2: estado wanted + membresía de perfil en el grafo
El grafo cruza ahora el corpus con el manifiesto de objetivo:

· estado `wanted` — una raíz declarada sin receta se materializa como nodo de
  frontera. Se crea ANTES de buscar huérfanas a propósito: un objetivo declarado
  no es una arista colgando, así `--check` conserva su significado (verificado:
  exit 0, grafo CIERRA, topo OK). `wanted` ≠ `unhashable`.
· campo `perfiles[]` por nodo — qué imágenes lo alcanzan desde sus raíces.
· bloque `by_profile` — el "cuánto falta", POR IMAGEN.

Primeros números, derivados de raíces y no de listas a mano:
   base               51/51   listo
   cli                74/74   listo
   escritorio-mirada  27/31   (falta libinput, mesa-swrast, mirada-{compositor,greeter})
   escritorio-kde     85/162  faltan 77   ← de 7 raíces, antes ilegible
   674 recetas no las alcanza NINGUNA imagen (catálogo, no distro)

La clausura es COTA INFERIOR mientras haya nodos `wanted`: sus deps no se
conocen hasta construirlos (o sembrarlas, P3). Está documentado en el header.

`totals.recipes` sigue contando sólo recetas con fichero (los consumidores lo
leen como tamaño del corpus); `totals.nodes` incluye la frontera. La vista HTML
regenera sin cambios de contrato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:43:40 -04:00
sergioandClaude Opus 4.8 c5ae42b92e espejo: push a gitea + GitHub privado, y script para reponerlo tras un clon
git.tawasuyu.net resuelve a gioser, marcado como FIJO y SIN BACKUP. Sin espejo, las dos únicas
copias del repo eran el laptop —que ya se corrompió una vez por un corte sucio, el 2026-07-22— y
esa. Ahora hay una tercera, en otro proveedor y PRIVADA.

Se implementa con `git remote set-url --add --push origin` en vez de un remoto `github` aparte,
para que TODO `git push origin main` que ya existe en los scripts (cosecha-cron.sh, el latido)
espeje solo, sin tocar un script. Si GitHub falla, el push devuelve != 0 aunque gitea haya
aceptado; cosecha-cron.sh ya lo tolera ("push falló, reintenta próximo ciclo") ⇒ el latido no se
rompe por eso.

La credencial va por HTTPS con el token de `gh`, no por SSH: las tres claves SSH del laptop
(github5, key25, sergiogithub) son DEPLOY KEYS de repos ajenos y dan "Repository not found" contra
este. El bloque `Host github.com` del ~/.ssh/config además no tiene `IdentitiesOnly`, así que ssh
ofrece todas y gana una deploy key.

El script existe porque las dos cosas que configura viven en `.git/config`, que NO se versiona: se
perderían exactamente en el escenario para el que se pusieron (el laptop muere, clonás de nuevo).
Incluye también el `core.fsync` del mismo incidente. Idempotente (los pushurl son acumulativos, así
que los reconstruye en vez de añadir) y con `--check` para auditar sin tocar nada.

Verificado: los dos remotos en el mismo commit, GitHub reporta PRIVATE, y correrlo tres veces deja
2 pushurl, no 6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:41:16 -04:00
sergioandClaude Opus 4.8 3a64b603d7 catálogo objetivo P1: manifiesto de perfiles — el set de la distro deja de ser un string de shell
El destino de la distro vivía en 2 strings de product-userland-from-repo.sh, 1
de mirada-usb.sh y 55 tandas planas. Ahora en docs/state/targets.toml: un perfil
= una imagen enviable, listando sólo las RAÍCES (lo que se pide por nombre); la
clausura la calcula el grafo, no un humano.

4 perfiles: base (26), cli (46, hereda base), escritorio-mirada (14),
escritorio-kde (7 raíces, cola incoming-kde). scripts/targets.py expande hereda
(transitivo, con detección de ciclo) preservando el orden — mirada lo necesita.

Lift-and-shift verificado: las 3 listas expandidas son byte-idénticas a los
strings que reemplazan. Ninguna imagen cambia de contenido.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:41:01 -04:00
sergioandClaude Opus 4.8 142c417d36 farm: que el loop DIGA que está esperando el lock, en vez de parecer que trabaja
El `echo "ciclo: N recetas en $Q"` sale ANTES de pedir el lock, así que con la campaña corriendo el
journal mostraba el anuncio del ciclo y después nada: parece un loop trabajando y es un loop
bloqueado. Es la misma clase de problema que el "no encuentro el ejecutable zig" apuntando al
directorio equivocado — un log que miente cuesta horas de diagnóstico.

`flock -n` primero, y sólo si falla se anuncia la espera y se bloquea. Sin coste cuando el lock
está libre, que es el caso normal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:21:24 -04:00
sergioandClaude Opus 4.8 1b436a717f farm: lock compartido entre campana-deuda y el worker-loop
Los dos construyen sobre el MISMO work/, y `fetch` nombra el árbol de fuentes de forma determinista
(`work/sources/<receta>-<sha16>`, sin nada que dependa de QUIÉN construye) ⇒ dos procesos que
necesiten la misma DEP apuntan al mismo directorio: uno hace `remove_dir_all` mientras el otro
corre `tar -x`. El árbol queda a medias y devuelve "Directory not empty" (os error 39), y así se
queda hasta que alguien lo borra a mano.

No es teórico: la campaña de deuda y el loop pidieron `mesa` a la vez (el loop lo arrastraba desde
la cola KDE al invalidarse libdrm) y se llevó puesta media cascada GUI, con un error que no nombra
la causa. Casi borro ese árbol a mano antes de ver que había un bwrap montándolo.

Grano: la campaña toma el lock para toda su corrida; el loop, por ciclo de cola. Más fino rompería
el `xargs -P2` de build-farm.sh.

Dos honestidades en los comentarios, para no prometer de más:
  - `flock` NO es FIFO. El loop vuelve a pedirlo enseguida y le gana a la campaña que espera:
    medido, la campaña entra al terminar TODAS las colas, no entre dos. La ventana real es el
    IDLE_SLEEP. Por eso espera con techo (LOCK_WAIT=7200) y sale limpia en vez de colgarse.
  - Esto NO cierra la carrera del todo: el `-P2` interno del loop puede correr dos recetas de la
    misma cola que compartan dep, y ésas se siguen pisando. El arreglo de fondo es un lock POR
    ÁRBOL dentro de `fetch`, que cubriría los dos casos.

Verificado con flock real: exclusión mutua (la campaña entra justo cuando el loop suelta), salida
limpia por timeout con rc=0, y la no-equidad de flock medida en vez de supuesta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:06:42 -04:00
sergioandClaude Opus 4.8 ebd8e7f929 farm: anclar el store con bind-mount, no symlink — el ln -s rompía toda receta con zig_version
Regresión introducida ayer al mover el store al volumen persistente (`ln -sfn /mnt/cosecha/store
$REMOTE/store`). El síntoma se leía como deuda de la cascada GUI y era otra cosa.

hammer deriva el lab del PADRE DEL STORE: `BuildConfig::defaults_for_store` hace
`project_root = store_root.parent()` y de ahí `.dev-fs/{alpine,tools/zig,cache}`. Con el store
symlinkeado, resuelve a /mnt/cosecha/store ⇒ busca /mnt/cosecha/.dev-fs, que no existe, y muere con
"no encuentro el ejecutable zig en …" — un mensaje que apunta al lugar equivocado: el zig 0.13.0
está, y está bien, sólo que en /opt/hammer/.dev-fs/tools/.

Golpea SÓLO a las recetas que fijan `zig_version` (20 en el corpus), porque son las únicas que
resuelven un zig hermano del por defecto. Por eso la campaña del 05:41 dio 26 selladas / 11
fallidas y las 11 eran exactamente ésas (tllist pango gtk4 libadwaita gtksourceview fcft
*-hello hammer-edit dwarves), y por eso la de las 19:48 —que era justo la lista de deuda— dio 0/14.

El bind-mount mantiene las DOS invariantes a la vez: los datos siguen en el volumen (sellar =
persistir, que es la razón del volumen) y `$REMOTE/store` vuelve a ser una ruta real bajo $REMOTE
⇒ el padre es /opt/hammer y .dev-fs se encuentra. Verificado en el worker vivo: con el bind-mount
y SIN overrides de entorno, libinput pasa la resolución de zig y falla por su dep real de Python.

Se persiste en fstab para que sobreviva al reboot, y el chequeo de verificación pasa de `test -L`
a `mountpoint -q`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:26:52 -04:00
sergioandClaude Opus 4.8 f3206d7520 latido: colgar el latido de la sesión, no del init (+ lock anti-solapamiento)
El latido vivía sólo en el crontab y eso resultó frágil: este laptop arranca a veces con
`init=/usr/local/sbin/arje-zero` y a veces con OpenRC (KDE), y no hay systemd en ninguno de los
dos. `cronie` es un servicio OpenRC ⇒ en el arranque arje no existe y el latido no late. Peor: no
late EN SILENCIO. Se destapó tras un corte sucio del 2026-07-22, en el que además se vio el
runlevel `default` quedar a medias (cronie/metalog/acpid caídos, NetworkManager/dbus arriba) ⇒ ni
siquiera arrancando OpenRC era garantía.

El costo del síntoma es caro y callado: sin latido el hub no siembra, el worker agota la cola y se
queda idle quemando € sin moler.

`latido.sh` cuelga el latido de la SESIÓN: cualquier terminal, en cualquier arranque, asegura que
haya exactamente un latido vivo (`--ensure` desde ~/.zshrc, ~5ms y mudo si ya hay uno). Sin root,
sin unidad de servicio, sin mantener lo mismo por duplicado en dos inits. Contra asumido: no late
sin sesión abierta — es un laptop, no un server, y el primer ciclo dispara al instante de abrir la
terminal (el cron esperaba hasta 30 min al próximo tick).

El lock va DENTRO de cosecha-cron.sh, no en el llamador, para que valga venga de donde venga. Eso
además tapa un bug latente que ya existía: nada impedía que dos ciclos se solaparan haciendo rsync
sobre el mismo store y `git commit`+`push` a la vez. Con el lock, el crontab puede quedarse: bajo
KDE dispara cron, bajo arje dispara la sesión, y nunca se pisan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:21:27 -04:00
sergioandClaude Opus 4.8 e09a48a69e farm: tandas.sh — encadenar campañas en orden, sin solaparse
En serie y no en paralelo: dos `hammer build` simultáneos compiten por el mismo work/sources y por
el watchdog de disco del worker-loop. El paralelismo vive DENTRO de cada build (-j$(nproc)).

En tandas y no en una lista plana: una tanda es una unidad topológica (la cadena GUI, el stack
wayland, los kernels). Cuando una se cae por su raíz —como pango arrastró a 7 recetas— se ve de un
vistazo en el resumen y se re-lanza sola; una lista de 40 nombres no dice nada al fallar.

Espera a que termine la campaña en vuelo, así se encola mientras otra muele.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:51:11 -04:00
sergioandClaude Opus 4.8 a8bb83add5 why-differs-barrido: no llamar "no-reproducción" a lo que no está probado
El artefacto guarda su recipe.toml pero NO los hashes de sus deps, y el ArtifactHash sí los
incluye ⇒ dos sellados con el mismo recipe.toml pueden diferir LEGÍTIMAMENTE porque cambió una
dependencia. La primera versión gritaba "NO-REPRODUCCIÓN REAL" sobre 128 paquetes; eso era un
superconjunto, no una prueba.

Ahora separa por la CAUSA, que sí discrimina: si TODO lo que difiere son metadatos que un cambio
de dep no explica (MTIME de gzip, cabecera ar, ruta de build, secciones ELF informativas), es
no-determinismo probable; si difiere .text/.data o hay ficheros de más, es indistinguible de un
cambio de dep sin reconstruir. 128 → 92 probables + 36 no concluyentes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:42:32 -04:00
sergioandClaude Opus 4.8 895401b479 cierre #2 del SDD 17: hammer why-differs — el diffoscope propio
Cuando un artefacto no reproduce, el store sólo sabe decir "el hash no coincide" y el resto es
trabajo artesanal. Esto responde POR QUÉ, en términos de la CAUSA y no del byte:

- gzip con MTIME embebido (bytes 4..8)      → remedio: `gzip -n`
- cabecera `ar` de un `.a` (mtime/uid/gid)  → remedio: modo determinista (`ar D`)
- secciones ELF, con lectura experta: sólo `.comment` ⇒ otra versión de compilador; sólo
  `.debug_*` ⇒ rutas de build; sólo `.symtab`/`.dynsym` ⇒ orden de símbolos (código idéntico);
  sólo build-id ⇒ residuo, no causa raíz. En un `.a` dice QUÉ MIEMBRO difiere.
- ruta del árbol de build embebida, texto (línea que difiere), y bytes como último recurso.

Y sobre todo trae la EVIDENCIA, no sólo la hipótesis: para las secciones de texto extrae las
cadenas que están en un ELF y no en el otro. Caso real que lo motivó (alsa-lib): la
interpretación decía "típicamente rutas de build" y la evidencia mostró
`/src/target/release/build/libsodium-sys-<hash-cargo>/out/…`. Sin la cadena era una corazonada;
reproducir eso a mano cuesta varios readelf, la herramienta lo da en 40ms.

Descenso, no comparación total: sólo baja donde los hashes difieren (el cruce con format/
reconcile del SDD 17). Sin dependencias externas — parsers gzip/ar/ELF propios, como manda el
ADR 0004: un diffoscope de verdad se apoya en medio mundo de binarios ajenos.

`--json` para el bucle agéntico; exit 0 si reproduce, 1 si diverge (encadenable en scripts).
`scripts/why-differs-barrido.sh` lo pasa por todo el store y separa los dos casos que se
confunden a ojo: recipe.toml distinto (divergencia esperada) vs recipe.toml IDÉNTICO y artefacto
distinto (no-reproducción a investigar).

5 tests nuevos; los 142 de hammer-core siguen en verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:35:03 -04:00
sergioandClaude Opus 4.8 6b38b46293 farm: campana-deuda.sh — el HUB dicta la lista, el worker sólo muele
`saldar-deuda-static.sh` calcula la deuda en vivo con `hammer hash --check` contra el store
LOCAL. En el worker ese store es PARCIAL (el del volumen) ⇒ mide 716 en vez de 37. Es la regla
de siempre con otra cara: el worker MIDE, el hub CLASIFICA. Acá el hub decide (DRY=1) y el
worker ejecuta una lista explícita, con las raíces del stack GUI primero (glib unblocks=14 →
harfbuzz/cairo/pango → gtk4 → libadwaita) para que la cascada caiga cuanto antes.

Incluye el PATH del `go` del store: `go mod vendor` corre host-side y una sesión SSH no
interactiva no carga /etc/profile ni cargo/env. Y HOME por defecto, que systemd-run no hereda
(con `set -u` era fatal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:10:31 -04:00
sergioandClaude Opus 4.8 c21602b707 farm: fix — farm-up tiraba el store HORNEADO de la golden (rm -rf) y el worker medía media distro como deuda
El anclaje del store al volumen hacía `rm -rf $REMOTE/store` antes de symlinkear. Eso borra
justo el catálogo cacheado que es la razón de ser de la imagen golden ("arranca con TODO el
catálogo ⇒ cache-hit instantáneo"). Consecuencia medida hoy: el worker calculó 716 recetas de
deuda donde el hub medía 37, y se puso a reconstruir media distro (fallando, además, porque
sin `go` en el PATH host-side las recetas Go mueren en `go mod vendor`).

Fix: fusionar en vez de borrar. El store es CAS (nombre = hash) ⇒ `mv -n` al volumen es seguro
por construcción: no pisa lo que el volumen ya tiene, y lo horneado queda disponible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:10:31 -04:00
sergioandClaude Opus 4.8 081fdd8a34 kde/metal: fix — el prompt nacía en el SERIAL; getty en tty1 (por eso el USB "no abría")
Síntoma: el USB de escritorio arranca, se ve el kernel, "arje-zero: despierta como PID 1",
el remount de la ext4 y el stack-depth de netup… y ahí la pantalla se congela para siempre.

Causa: el CMDLINE horneado es "console=tty0 console=ttyS0,115200 …" y el kernel hace
/dev/console = la ÚLTIMA console= ⇒ ttyS0. La seed card de la base metal supervisa un solo
getty, sobre `console` ⇒ en una laptop sin puerto serie el shell nace INVISIBLE. Los printk
sí van a las DOS consolas: por eso se ven los mensajes del kernel y después silencio. El
sistema nunca estuvo colgado — reproducido en OVMF: por el serial hay shell root (PID 98,
sshd arriba); en pantalla, nada. Nunca se vio antes porque toda validación fue -nographic,
donde el serial ES la pantalla (y el comentario de install-image-efi.sh afirmaba lo contrario:
"console=tty0 al final ⇒ el stdout va a la PANTALLA").

Fix en el script de imagen, no en el cmdline: un getty sobre tty1 es independiente del
cmdline (en metal siempre hay VT) y no obliga a recompilar/re-sellar el kernel (~60min).
Se conserva el getty de `console` para debug por serial.

- seed card del rootfs fundido += nodo tty1-getty (clona console-getty, argv → tty1)
- /usr/bin/console-login: muestra el motd y exec sh. Exporta PATH: arje-zero lanza el getty
  con envp VACÍO ⇒ sin él no se resolvía ni `cat` ni `plasma-start`.
- ambos escriben rompiendo el hardlink (os.replace / rm -f): $MERGED es cp -al de la base,
  escribir in-place mutaba work/metal-rootfs y toda imagen que comparta el inodo — ya había
  pasado con /etc/motd.

Validado en OVMF con pantalla (no -nographic): motd + prompt "/ #" visibles, y `ls /dev/dri`
tecleado por QMP responde card0 (teclado + PATH OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 21:32:06 -04:00
sergioandClaude Opus 4.8 0a3a00eb6e kde/metal: fix — la imagen dual usa linux-metal-dual (no linux-generic, que no bootea por EFI-stub)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:52:31 -04:00
sergioandClaude Opus 4.8 746d2dedd1 kde/metal: andamiaje escritorio dual-GPU (Intel i915 + NVIDIA nouveau) por software
Objetivo: USB "live" que arranca el escritorio KDE en metal y aguanta ambas máquinas
(TigerLake/iris y Pascal/nouveau), render por software (mesa-llvmpipe soberano) sobre
el KMS del kernel — uniforme en cualquier GPU, reusa los fixes validados en QEMU.

- recipes/linux-metal-dual.toml: = linux-generic (dual-GPU i915+nouveau+radeon) + el
  CMDLINE de pivote de linux-metal (initrd=/initramfs.cpio.gz rdinit=/init) para bootear
  por EFI-stub directo desde el ESP (root en disco, no RAM). Ni linux-generic (sin pivote)
  ni linux-metal (nouveau OFF) servían solos.
- scripts/kde/metal-desktop-image-dual.sh: arma la imagen — base metal + KDE + inyección
  mesa-llvmpipe/libLLVM/musl + firmware nvidia gp106 (nouveau modeset Pascal) + kernel
  linux-metal-dual.
- scripts/kde/plasma-start-metal-sw.sh: launcher software-GL para metal, detección de GPU
  real (i915/nouveau/amd), lanzamiento MANUAL (bringup seguro), con los fixes de la sesión
  QEMU (GBM_ALWAYS_SOFTWARE, USE_MODIFIERS=0, KWIN_COMPOSE=Q, FORCE_SW_CURSOR, cursor/iconos
  breeze, D-Bus sesión+sistema).

Pendiente: build del kernel (~60min, en curso) → rebuild imagen → validar OVMF → quemar USB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:32:46 -04:00
sergioandClaude Opus 4.8 44d50a76b5 kde/qemu: KWIN_FORCE_SW_CURSOR=1 — cursor visible y estable
El tema de cursor ya cargaba (sin "Failed to load cursor theme") pero el cursor
seguía apareciendo/desapareciendo: kwin usaba el plano de cursor por HARDWARE del
DRM, que en virtio-gpu con render software no se presenta bien. KWIN_FORCE_SW_CURSOR=1
⇒ kwin compone el cursor dentro del framebuffer principal (software) ⇒ visible y
estable. Verificado por screendump: la flecha breeze aparece compuesta en el scanout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:36:28 -04:00
sergioandClaude Opus 4.8 abf4e25c8f kde/qemu: tema de cursor breeze — mata "Failed to load cursor theme"
kwin buscaba el tema "default" (inexistente) ⇒ cursor invisible. El rootfs trae
breeze_cursors (115 cursores). Fix: XCURSOR_THEME=breeze_cursors + XCURSOR_PATH +
kcminputrc [Mouse] cursorTheme. El error desapareció del log (0 ocurrencias).

Estado final verificado por screendump: escritorio Plasma 6 completo (wallpaper +
panel con launcher/pager/bandeja + reloj), estable (139=0; el run previo aguantó
7.5 min a +450s con kwin=1 plasmashell=1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:30:32 -04:00
sergioandClaude Opus 4.8 d724db6556 kde/qemu: -vga none — el escritorio por fin SE VE (no más TianoCore congelado)
El escritorio booteaba y kwin pintaba por dentro (serial: kwin vivo), pero el scanout
REAL (verificado por `screendump` del monitor QEMU → PNG) seguía congelado en el logo
TianoCore. Causa: sin -vga none, QEMU añade una VGA estándar por defecto ADEMÁS del
virtio-gpu-pci. OVMF pinta su GOP en la VGA estándar ⇒ el kernel la envuelve con
simpledrm (card1) y ESE es el scanout que se muestra; kwin pinta en virtio-gpu (card0),
otro device que no se ve. Como son devices distintos, virtio no expulsa simpledrm ⇒
pantalla clavada en el frame EFI. (Con VARS reusados el timing lo tapaba; al resetear
los VARS quedó expuesto.)

Fix: -vga none ⇒ virtio-gpu-pci es el único display ⇒ OVMF pinta ahí ⇒ virtio-gpu-DRM
expulsa simpledrm (mismo device) ⇒ un solo card0, y el modeset de kwin toma la pantalla.
Verificado por screendump: se ve el wallpaper de Plasma 6 (1592x960), no TianoCore.

Herramienta nueva: -monitor unix socket en run-qemu-desktop.sh ⇒ `screendump` captura
el framebuffer REAL del guest (oráculo independiente de la ventana gtk, que puede
congelarse si mirada reinicia su Xwayland).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:22:07 -04:00
sergioandClaude Opus 4.8 82cbdb7e54 kde/qemu: KWIN_COMPOSE=Q (QPainter) — esquiva el segfault del rasterizador llvmpipe
El "pinta y vuelve a negro" NO era el output config: era kwin SEGFAULTEANDO (139) a
los segundos de pintar y respawneando. Core dump (core.llvmpipe-0, 176MB) + gdb del
host: el crash está en un hilo rasterizador de llvmpipe —
  #0 util_fill_rect  #1 util_fill_box  #2 lp_rast_clear_color  #3 rasterize_scene
  #4 thread_function (llvmpipe worker)
llvmpipe peta al limpiar el color buffer (bug de stride/geometría o vectorización del
fill en este entorno virtio+kms_swrast).

Fix: KWIN_COMPOSE=Q ⇒ kwin compone con QPainter (raster de Qt directo al buffer, SIN
armar escenas llvmpipe) ⇒ nunca entra a lp_rast_clear_color. (El segfault viejo con =Q
era el de gbm-init, ya resuelto por GBM_ALWAYS_SOFTWARE; llvmpipe sigue disponible para
gbm/EGL.) Verificado: QPainter compositing initialized, 139=0, kwin vivo a +15s/+30s.

+ STATUS monitor arreglado (busybox no tiene pgrep -c ⇒ pgrep|wc -l) con timestamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:09:38 -04:00
sergioandClaude Opus 4.8 2f99446ca1 kde/qemu: un solo output virtio (KWIN_DRM_DEVICES) — mata el negro-tras-pintar
Tras arreglar el FB (modifiers), el escritorio pintaba con plasmashell y volvía a
negro. Causa: kwin veía DOS outputs — virtio-gpu ("QEMU Monitor") Y el efifb/simpledrm
("Unknown-1", framebuffer EFI read-only que no fue expulsado al cargar virtio-gpu).
El conflicto entre ambos rompía el import/destroy de buffers EGL (spam
eglDestroyImageKHR EGL_BAD_PARAMETER) y tiraba la conexión de plasmashell
("error in client communication") ⇒ negro.

Fix: plasma-start detecta el card cuyo driver es virtio_gpu (robusto al numerado
card0/card1 que cambia entre boots) y lo pasa por KWIN_DRM_DEVICES ⇒ kwin ignora el
efifb. Resultado: un solo output, atomic modeset, y TODOS los errores correlacionados
con el negro a 0 (client-comm, eglDestroyImage, FB fail, output disabled). kwin carga
efectos normalmente.

+ monitor de estado cada 15s al serial (kwin/plasmashell vivos) para diagnóstico.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:01:16 -04:00
sergioandClaude Opus 4.8 6c0ef0867d kde/qemu: KWIN_DRM_USE_MODIFIERS=0 — mata la pantalla negra (FB rechazado por virtio)
Con el segfault ya resuelto, el escritorio salía NEGRO: kwin encontraba el conector
(1280x800) pero "Failed to create a framebuffer: Invalid argument" ⇒ "Failed to find
a working output layer" ⇒ sin scanout. Se veía el wallpaper un instante (el modeset
inicial usa un buffer dumb sin modifiers) y luego negro (los buffers del compositor
van por drmModeAddFB2WithModifiers).

virtio-gpu ADVIERTE el CAP de modifiers pero rechaza el modifier explícito (aún
LINEAR=0) con EINVAL. Fix: KWIN_DRM_USE_MODIFIERS=0 ⇒ kwin usa drmModeAddFB2 plano
(modifier implícito) que virtio sí acepta. "Failed to create a framebuffer" pasó de
2526 a 0; input Qt fluyendo; sin crash.

También: quitado KWIN_DRM_NO_AMS (era corazonada del segfault; atomic es el path
sólido en virtio) y arreglado el stream vivo del kwin.log al serial (busybox sed no
soporta -u ⇒ tail -f directo, sin sed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:52:59 -04:00
sergioandClaude Opus 4.8 6e5bc48c58 kde/qemu: system D-Bus — mata el spam udisks2 y completa el escritorio
plasmashell/solid spameaba "kf.solid.backends.udisks2: Not connected to D-Bus
server" en cada poll: sólo había bus de sesión, no de sistema. Ahora plasma-start
levanta dbus-daemon --system (socket estándar /run/dbus/system_bus_socket).

- qemu-desktop-image.sh: siembra el usuario messagebus:81 en passwd/group
  (system.conf hace setuid a él; el passwd base no lo traía). Rompe el hardlink
  read-only con `mv` de un temp, como el patch del seed.
- plasma-start-qemu.sh: arranca el system bus antes del de sesión + copia
  machine-id a /var/lib/dbus.

No hay udisksd/upowerd reales en el rootfs ⇒ sin discos/batería, pero el bus
existe y solid deja de errorear. Verificado: "Not connected" pasó de spam
continuo a 0; system bus ON; kwin+plasmashell+KSplash+kioworker vivos; 139=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:43:34 -04:00
sergioandClaude Opus 4.8 b53a822b42 kde/qemu: MURO 2 caído — GBM_ALWAYS_SOFTWARE=1 y el escritorio PINTA
El segfault-139 de kwin en QEMU NO era llvmpipe ni el composite: diagnóstico
por core dump + gdb del host (backtrace #0 0x0 ← dri2_initialize_drm ← eglInitialize
← EglDisplay::create ← DrmBackend::initialize; call *0x30(vtable swkmsMesaCoreExtension)
= queryCompatibleRenderOnlyDeviceFd = NULL en el driver software).

virtio-gpu-pci sin 3D expone un nodo KMS-only (no render-capable). En mesa 24.0.9
gbm sólo pone software=true vía dri_screen_create_sw, al que sólo se llega con
GBM_ALWAYS_SOFTWARE o si dri_screen_create falla. Con MESA_LOADER_DRIVER_OVERRIDE
kms_swrast, dri_screen_create tiene éxito ⇒ software=false ⇒ dri2_initialize_drm
llama el vtable NULL ⇒ SIGSEGV. Fix: GBM_ALWAYS_SOFTWARE=1 en plasma-start.

- plasma-start-qemu.sh: + GBM_ALWAYS_SOFTWARE=1 (el fix); + andamiaje de diagnóstico
  gated por DIAG=1 (logging kwin/mesa/egl verboso, stream vivo del kwin.log al serial,
  core dumps a /core + sync, sleep 3600 para congelar el serial).
- run-qemu-desktop.sh (nuevo): run reproducible (DISP=none/gtk, TIMEOUT, VARS OVMF rw).
- qemu-desktop-image.sh: restaurado bit +x.

Verificado: headless (139=0, sin queryCompatible) y ventana gtk en DISPLAY=:0 —
kwin_wayland + plasmashell + KSplash + kioworker vivos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:30:45 -04:00
sergioandClaude Opus 4.8 1c6b4ed54b kde/mesa: cadena llvmpipe soberana — LLVM18+mesa-llvmpipe from source, GBM cruzado
mesa-llvmpipe con gcc/libstdc++ (matchea el ABI de libLLVM; zig-cc usaba libc++ ⇒
undefined symbols en el JIT). qemu-desktop-image inyecta libLLVM.18+libgcc_s+libstdc++.
HITO: kwin toma DRM master de virtio-gpu, crea el GBM device (llvmpipe, ya no softpipe),
expone compositor y ACEPTA clientes. Muro restante: kwin segfaultea (139) al componer la
1ra superficie con llvmpipe — necesita backtrace (gdb) para diagnosticar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:15:21 -04:00
sergioandClaude Opus 4.8 dafd97be79 kde: pipeline escritorio QEMU (qemu-desktop-image + plasma-start-qemu)
Imagen QEMU booteable con Plasma auto-lanzado: merge base+kde-metal-rootfs, inyección
del loader musl (la imagen metal nunca pudo correr KDE dinámico — base estática + KDE
sin libc), parche swrast, seed patcheado (console-getty ejecuta plasma-start), PATH/dbus/
fuentes/iconos. HITO: kwin toma DRM master de virtio-gpu y expone wayland-0 (lo que el
anidado no podía). Muro pendiente: GBM/EGL de software — mesa es softpipe, falta llvmpipe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 14:21:37 -04:00
sergioandClaude Opus 4.8 7964e45bfa kde: nested-app siembra kdeglobals + activa KDEPlasmaPlatformTheme (iconos+Breeze)
Sin kdeglobals Qt caía al tema de iconos hicolor (vacío) ⇒ dolphin pelado. Ahora
siembra [Icons]Theme=breeze + QT_QPA_PLATFORMTHEME=kde ⇒ aplica iconos + estilo
Breeze + colores. El motor SVG (libqsvgicon/libqsvg/libQt6Svg) ya estaba hidratado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 22:03:48 -04:00
sergioandClaude Opus 4.8 4d418cd07c kde: nested-plasma.sh — sesión Plasma COMPLETA anidada en mirada (kwin+plasmashell)
kwin_wayland como compositor anidado en Xwayland :0 (backend X11; el wayland-nested
segfaultea en gbm/renderD128) + compositado por SOFTWARE (llvmpipe/swrast) + QtQuick
software backend (sin él plasmashell no crea contexto GL/EGL). plasmashell carga la
corona org.kde.plasma.desktop y renderiza. Cliente wayland, no DRM master ⇒ seguro
sobre la sesión viva. Verificado los 3 procesos vivos + Desktop.qml cargado.

Requirió hidratar al rootfs: kactivitymanagerd (daemon aparte de plasma-activities),
mesa-swrast, y GRAFT del módulo qml org.kde.kwindowsystem (ver deuda en recipe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:28:18 -04:00
sergioandClaude Opus 4.8 f78fdeef94 kde: QT_QPA_FONTDIR + SHELL en nested-app — mata los tofu
qtbase se construyó SIN fontconfig (libQt6Gui no enlaza libfontconfig) ⇒ Qt usa su
motor básico que busca fuentes en /usr/lib/fonts (vacío) → glyphs tofu. Atajo sin
rebuild: QT_QPA_FONTDIR=/usr/share/fonts/dejavu hace que el motor básico cargue los
ttf directo. SHELL=/bin/sh da shell a konsole (zsh no está en el rootfs). Fix 'correcto'
(qtbase con FEATURE_fontconfig) = rebuild que re-hashea el escritorio → deuda para la granja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:13:12 -04:00
sergioandClaude Opus 4.8 b2d80e6f69 kde: lanzador anidado (nested-app.sh) — apps KDE hidratadas como ventana en mirada
Corre una app KDE del rootfs hidratado como cliente wayland ANIDADO en el compositor
mirada vivo (WAYLAND_DISPLAY=/run/mirada-wayland), vía bwrap overlay (alpine+kde-rootfs
como /). Cliente, no master: renderiza por renderD128/iris, cero riesgo al display.
Resuelve dbus de sesión (root-en-userns + machine-id + /run/dbus). konsole y dolphin
verificados corriendo en vivo. Fix portabilidad: run-plasma-headless.sh += --setenv PATH.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:04:16 -04:00
sergioandClaude Opus 4.8 50d51bb4a8 granja: cosecha-heartbeat.sh — latido de cosecha bajo mirada (arje-zero sin crond)
El crontab */30 no dispara cuando el laptop bootea en mirada (PID1=arje-zero, sin
crond). Envuelve el one-shot cosecha-cron.sh en un loop setsid — el mecanismo que sí
corre sin root y sobrevive al fin de sesion (no al reboot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 11:52:16 -04:00
sergioandClaude Opus 4.8 19a3cda8ca granja: el worker recompila hammer al arrancar (evita el fósil de la golden)
Raíz de una noche de KDE atascada: la golden del 29-jun horneaba un hammer SIN el fix del
overlay lowerdir (e69eaaa, 12-jul) ⇒ toda receta de muchas deps (kio=52, kwin, plasma-*)
desbordaba el límite de 4KB de mount options y el sandbox ni arrancaba. 43 recetas 'en cola'
que en realidad reventaban al instante. El source llega fresco por farm-sync pero nadie
recompilaba. Ahora el worker-loop hace 'cargo build --release --bin hammer' al arrancar
(~24s cacheado). Binario del worker vivo ya rebuildeado a mano y verificado: kio cruza el
mount (funde 52 deps en una capa) y configura.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 05:07:13 -04:00
sergioandClaude Opus 4.8 20666643e2 granja: estado-granja.sh — foto on-demand (worker+avance KDE+cron) sin esperar el latido
Chequeo manual por si venís antes del cron o no corrió: salud del worker y qué muele,
barra de avance KDE desde el grafo, y últimas líneas del ciclo de cosecha. Filtra el
self-match del pgrep (el ']+.toml' que se colaba).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:55:44 -04:00
sergioandClaude Opus 4.8 2a52d7804f granja: cron infinito de cosecha+siembra (recoger/sembrar cada 30min) — latido sin tokens
El worker efímero muele KDE 24/7; este cron en el laptop recoge su store sellado
(worker->laptop, CAS merge seguro), siembra recetas nuevas (laptop->worker) y regenera
el grafo de estado, commiteando SÓLO docs/state/*.json (el avance que el humano sigue).
NO promueve incoming-kde->canónico (decisión de clasificación humana). Robusto a
worker-ausente (dead-man ya lo mató por idle). KDE: 66/206 sellado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:12:09 -04:00
sergioandClaude Opus 4.8 4357cd7b0f estado: buscar el artefacto por el name INTERNO, no el basename del fichero
El grafo buscaba store/<hash>-<basename> pero el artefacto se sella por el name INTERNO de la receta
(qtbase.toml → name=qt6-qtbase → store/<hash>-qt6-qtbase). Las qt6-* KDE salían 'debt' aunque
estuvieran selladas. Fix: nodo lleva iname=d['name']; state_of busca por iname. Las aristas de deps
siguen por basename (así resuelve resolve_dep_path). qtbase ahora sale sealed correctamente.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:32:14 -04:00
sergioandClaude Opus 4.8 ff79dc5e28 estado+kde: el grafo ahora cubre la cola incoming-kde; base KDE desbloqueada en el laptop
MOTIVO: 'estábamos haciendo kde'. La cola recipes/incoming-kde/ (206 recetas) era punto ciego del
grafo (sólo miraba recipes/ top-level). Modo --kde: build-state.py/build-state-view.py cubren la cola
KDE (sombreando canónicas homónimas sibling-first, como el sandbox) → build-state-kde.{json,html}.

CAUSA de la deriva (193/206 en deuda): fui YO — esta sesión re-sellé pkgconf y samurai (arreglos
static), y 194 recetas KDE declaran pkgconf, 142 samurai ⇒ re-hash en cascada de todo el árbol. El
sistema determinista funcionando: cambia un input base, cascan los dependientes. Deuda legítima.

DESBLOQUEO (el 'GUI rompe en el laptop' era sólo cairo/pango, NO la base): construida en el laptop
toda la base KDE guiado por el orden de ataque del grafo — extra-cmake-modules, util-macros,
xorgproto, xcb-proto, wayland, libdrm, dbus, icu4c, util-linux, fontconfig, pixman, glib, freetype,
harfbuzz, fribidi, libXau/libXdmcp, la cadena X11 (xtrans/libxcb/libX11/libXext/libXrender/libXfixes/
libXi), la familia xcb-util, libxkbcommon, wayland-protocols, mesa (24.0.9 iris-only, sin LLVM), y
los -shared. KDE 9→39 selladas. qtbase (↑117, la raíz) quedó DESBLOQUEADO y compilando.

DISCO: el build llenó el disco (100%). Liberados 82G borrando work/sources (57G, se re-extrae solo)
y .farm-harvest (26G, verificado 100% redundante en el store, CAS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:27:55 -04:00
sergioandClaude Opus 4.8 a267cf4f48 estado: clasificación Rust corregida (había ~180 Rust escondidas como C) + musl
Señal fuerte para Rust: la receta copia su binario de target/release/ en las fases. Medido 225
recetas, 0 con dep go ⇒ sin falsos positivos con Go (findutils entra bien: es uutils/findutils, find
en Rust). LÍMITE: un crate BuildSys::Cargo PURO sin fase install propia (zellij) no deja rastro en el
TOML y cae a 'c' — sólo se ve bajando la fuente, que el generador no hace.

Efecto: clases c 322→141, rust 45→226 (la mayoría eran Rust auto sin fase cargo explícita). Y la
LECTURA de la deuda cambia: la deuda C REAL es sólo 8 (casi toda saldada esta sesión), no 74. Lo que
queda es Rust (15, CLI pesados → granja), GUI (29 → granja), Go (5 → worker), kernel (4).

musl sellado (base). Avance: sealed 703→704, debt 53→52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 12:36:23 -04:00
sergioandClaude Opus 4.8 a30e0d3146 estado: orden de ataque por impacto de desbloqueo (lectura accionable del DAG)
El grafo ahora computa, sobre las dependencias, dos campos por receta:
  blocked_by — deps que están en deuda (lo que impide construirla ya)
  unblocks   — cuántas recetas EN DEUDA la declaran como dep (su impacto de desbloqueo)

Una receta en deuda sin blocked_by es construible YA; ordenadas por unblocks desc, dan el orden que
libera el grafo más rápido. La vista muestra la pastilla ↑N en las filas listas y ordena por ahí.

Top de impacto (todo el stack GUI concentrado, más zstd/perl de C-base):
  glib ↑14  wayland ↑13  pixman ↑12  libdrm ↑11  fontconfig ↑11  zstd ↑10  libxkbcommon ↑9  perl ↑8

Es la guía para cuando se levante la granja: construir esas primero desbloquea el grueso.

De paso, diagnóstico de las 8 'never' (ninguna es cruft ni bug): dwarves BLOQUEADA-documentada
(necesita libdw, elfutils da sólo libelf a propósito — sub-proyecto elfutils-libdw); llimphi-counter
es un EJEMPLO/plantilla intencional; las otras 6 son imports Go/Rust que van al worker. Y confirmado
parseando: 0 recetas con FIXME real en el campo sha256 (el grep decía 43, todas comentarios) — otra
vez grep miente, el grafo (parsea) dice la verdad.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:48:28 -04:00
sergioandClaude Opus 4.8 e19fb6347f estado: vista humana del grafo (build-state-view.py → build-state.html)
Dashboard self-contained (sin recursos externos, tema claro/oscuro) que rinde build-state.json para
VER y SEGUIR: barra de avance del corpus, tiles por estado, reparto de deuda por clase, y los huecos
en ORDEN DE ATAQUE. Ese último es el uso accionable del grafo: cada receta en deuda muestra sus deps
coloreadas por estado; una fila 'lista' tiene todas sus deps al día (construible ya), una 'espera N'
depende de N recetas también en deuda. Foto actual: 117 en deuda, 71 listas ahora.

Publicado como Artifact para verlo en el navegador; el HTML también queda en el repo (abrir con
file://). Regenerar ambos: scripts/build-state.py && scripts/build-state-view.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:34:02 -04:00