# Runbook — GNOME en QEMU: el escritorio PINTA

Estado al 2026-07-29: **cerrado.** gnome-shell arranca desde fuente sobre el kernel de hammer, toma
el DRM master y los tres dispositivos de input por logind, expone `wayland-0`, y **dibuja el Overview
de GNOME**. Cinco muros caídos, cada uno por medición y no por conjetura:

| muro | causa real | cómo se midió |
|---|---|---|
| SIGSEGV en `get_seat_proxy` | object path de la Session mal escapado (`_1` en vez de `_31`) | el cliente calcula el path solo y no pregunta |
| cuelgue de la hebra de input | el fd de `TakeDevice` sin `O_NONBLOCK` | `wchan=evdev_read` en `/proc`, no el core |
| `Requiring <X>` en bucle | 7 typelibs de runtime + 2 capas más abajo | `js/misc/dependencies.js` + cierre de `<include>` |
| pantalla negra | `Main.start()` abortaba por un gschema de g-s-d | el stack trace, que estaba en el log desde el principio |
| (descartado) KMS legacy | NO era la causa | atomic tomó efecto y la pantalla siguió con 2 colores |

Hermano de [`kde-qemu-desktop.md`](kde-qemu-desktop.md), del que reusa el andamiaje: la base
`work/metal-rootfs`, la inyección de mesa-llvmpipe/LLVM/musl, el truco del console-getty y el propio
`scripts/kde/run-qemu-desktop.sh`.

## Ciclo completo

```sh
cd ~/hammer
bash scripts/gnome/hydrate-gnome.sh          # cierre de gnome-shell + las raíces de RUNTIME → work/gnome-rootfs
bash scripts/gnome/qemu-desktop-image.sh     # funde base+gnome, parchea, → work/hammer-gnome-qemu.img
IMG=work/hammer-gnome-qemu.img SERIAL=work/gnome-qemu-serial.log MON=work/gnome-monitor.sock \
  TIMEOUT=150 DISP=none bash scripts/kde/run-qemu-desktop.sh
grep -a "gnome-qemu ::" work/gnome-qemu-serial.log | awk '!seen[$0]++'
```

`DISP=gtk` para ver la ventana (necesita `DISPLAY=:0 GDK_BACKEND=x11`). **Regla heredada de KDE:
validar escritorios con PANTALLA, nunca sólo por serial** — y sin humano delante se cumple con el
`screendump` del monitor de QEMU, ver abajo. Aplicarla es lo que destapó el muro de hoy: el serial
decía «sesión viva» y la pantalla estaba negra.

## Sacar el core y el backtrace

El `gnome-start` deja el core en la raíz ext4 y hace `sync`, así que sobrevive al SIGKILL de QEMU.

```sh
sfdisk -J work/hammer-gnome-qemu.img      # localizar hammer-root (part 2)
dd if=work/hammer-gnome-qemu.img of=/tmp/root.img bs=1M skip=129 count=5113 status=none
debugfs -R "dump /core.gnome-shell.<pid> /tmp/core.gnome-shell" /tmp/root.img
gdb -batch -q -ex "set sysroot $PWD/work/gnome-qemu-rootfs" \
    -ex "core-file /tmp/core.gnome-shell" -ex "bt 25" work/gnome-qemu-rootfs/usr/bin/gnome-shell
```

## 🏔 GNOME PINTA (2026-07-29)

![gnome-shell en QEMU](../evidencia/gnome-shell-qemu-2026-07-29.png)

El Overview de GNOME Shell, construido entero desde fuente sobre el kernel de hammer, arje-zero como
PID1 y musl: el conmutador de espacios de trabajo arriba a la izquierda, el reloj en el panel, la
miniatura del escritorio, el dash abajo, y la notificación estándar de GNOME por sesión de root
(«Logged in as a privileged user»).

**Cómo se validó, y es la parte reusable**: `screendump` por el monitor de QEMU, sin humano delante.

```sh
printf 'screendump /tmp/gnome.ppm\n' | socat - UNIX-CONNECT:work/gnome-monitor.sock
python3 -c "from PIL import Image; Image.open('/tmp/gnome.ppm').save('/tmp/gnome.png')"
```

Métrica barata para saber si pinta antes de mirar: **contar colores distintos**. Con la pantalla
negra eran **2** (negro + el gris de la consola del kernel); pintando son **582**, con el azul GNOME
`(2,60,136)` y los grises del shell `(34,34,38)` al frente.

### El muro que parecía de KMS y era de GSettings

El diagnóstico anterior de este runbook culpaba al KMS: «el plano primario de virtio-gpu no publica
formatos, sólo hay `Queue mode set` y ningún page flip». **Era una correlación, no la causa.** Se
probó quitando `MUTTER_DEBUG_FORCE_KMS_MODE=simple` —el log pasó a decir `using atomic mode setting`,
o sea que el cambio SÍ tomó efecto— y la pantalla siguió con exactamente 2 colores. Refutada.

La causa estaba en el stack trace, en el log, desde el principio:

```
GNOME Shell-CRITICAL: Failed to setup quick settings:
  Error: GSettings schema org.gnome.settings-daemon.peripherals.touchscreen not found
Stack trace: systemActions.js:156 → status/system.js:236 → quickSettings.js:29
```

Esa excepción ocurre **dentro de `Main.start()`**, construyendo los quick settings: el panel nunca se
termina de armar, así que no hay nada que pintar. El proceso queda vivo y el compositor con DRM
master — de ahí la pantalla negra con `gnome-shell=2`. **Mutter no tenía un frame que presentar; no
es que no supiera presentarlo.**

Lo resolvió la receta `gsd-schemas`: los gschemas de gnome-settings-daemon **sin** g-s-d (que sigue
aparcada por GTK3/X11). Tercera vez que aparece la misma lección — un gschema no es un fichero de
datos del demonio, es una interfaz publicada que consume otro programa.

### Dos trampas del camino, las dos de diagnóstico

1. **`glib-compile-schemas` sale con 0 aunque RECHACE un esquema.** Instalé los once XML, el log dijo
   `esquemas OK`, y el shell seguía diciendo `schema ... not found`. Lo que faltaba era
   `org.gnome.settings-daemon.enums.xml` —que meson genera con `glib-mkenums` y no es uno de los
   `.in`—: sin los `<enum>`, glib descarta el esquema entero, avisa por stderr y sigue. `gnome-start`
   ahora **vuelca ese stderr siempre**, no sólo cuando el comando falla.
2. **El log serial persiste entre arranques.** Dos veces leí un `STATUS +30s` que era del arranque
   anterior y saqué conclusiones de una VM muerta. Truncar (`: > serial.log`) antes de arrancar, y
   esperar a que la imagen esté hecha Y no haya otro QEMU con el disco tomado (`Failed to get "write"
   lock` es el aviso de que hay uno vivo).

### Iconos y cursor: `adwaita-icon-theme` (cerrado)

`No cursor theme available` no era cosmético: **con el cursor por software** —obligatorio en
virtio-gpu con render software— mutter dibuja la imagen que le da el tema, y sin tema el puntero se
movía invisible. Con `adwaita-icon-theme` (+ `hicolor-icon-theme`, que su `index.theme` hereda) el
cursor aparece y la notificación del shell tiene su icono. Colores distintos: 582 → 629.

Dos gotchas de esas dos recetas: **hicolor 0.18 pasó de autotools a meson** (su tarball ya no trae
`configure`, y un `./configure` heredado sale con 127), y adwaita declara `gtk-update-icon-cache`
como `required: true` para un `add_install_script` que **su propio autor marcó
`skip_if_destdir: true`** — exige un binario que en un build empaquetado no puede ejecutar. Se borran
los dos bloques; `required: false` no alcanza porque meson no propaga el disabler ahí dentro.

### El setuid del launch-helper: una causa, dos síntomas (cerrado)

`dbus-daemon-launch-helper` es el que `dbus-daemon` ejecuta para activar un servicio del bus de
SISTEMA bajo demanda, y **comprueba sus propios permisos antes de hacer nada**: si no es setuid root
se niega, y el cliente recibe `The permission of the setuid helper is not correct` — que es lo que
reportaba colord. Sin eso **ninguna** activación por bus de sistema funciona. La imagen ahora lo deja
`root:messagebus 4750`, y el síntoma de colord cambió de «permiso incorrecto» a «timed out»: la
activación ya se intenta de verdad, y lo que queda es del demonio, no del bus.

### El bus de sistema, completo (cerrado): faltaba polkit

```
== gnome-qemu :: bus: org.freedesktop.login1      OK
== gnome-qemu :: bus: org.freedesktop.PolicyKit1  OK
== gnome-qemu :: bus: org.freedesktop.Accounts    OK
== gnome-qemu :: bus: org.freedesktop.UPower      OK
```

`accounts-daemon` y `upowerd` arrancaban, seguían vivos, y **nunca adquirían su nombre**: los dos se
bloquean en `polkit_authority_get_sync()` al iniciar, y `org.freedesktop.PolicyKit1` no lo servía
nadie —nuestra receta `polkit` es `libs-only` a propósito, porque el demonio lo pone arje—. gnome-shell
esperaba después 25 s por cada uno.

**El dato que lo resolvió no salió del log de los daemons sino de la LISTA DE NOMBRES DEL BUS.** Ahí
estaban como conexiones anónimas `:1.0`, `:1.1` — sin nombre bien conocido. Eso distingue tres cosas
que en el log del cliente se ven igual: «no arrancó», «arrancó y no llegó a pedir el nombre» y «lo
pidió y se lo negaron». Era la segunda. `gnome-start` ahora **espera cada nombre** con
`NameHasOwner` y, si no aparece, vuelca el log del daemon y la lista de nombres.

Lo sirve `recipes/arje-polkit-compat.toml` (b3:03e0a86f), el tercer shim de este tipo tras
arje-logind-compat y arje-sdlogin-compat. **⚠ Autoriza TODO** — es la postura de sistema confiado que
arje ya tenía tomada, no algo que la receta introduzca; está escrito en su comentario.

Dos gotchas del empaquetado, los dos medidos:
- **La política de polkit ya existe y NO alcanza**: permite `own` sólo al usuario `polkitd`, y el shim
  corre como root. Se agrega un `zz-arje-polkit-compat.conf` en vez de reescribir la de upstream —
  dbus lee `system.d` en orden alfabético y las reglas posteriores ganan.
- **Los ficheros del rootfs fundido son hardlinks de SÓLO LECTURA del store** (0444). Un `cat >`
  encima falla con `Permission denied` y rompió un build. Regla de este script: nunca sobrescribir un
  fichero que venga de un artefacto; agregar al lado, o `rm -f` primero — y entonces se está pisando
  algo sellado, que es una decisión, no un descuido.

### Audio: PipeWire (cerrado — decisión del usuario, 2026-07-29)

**El servidor de audio de la distro es PipeWire, no PulseAudio.** Cierra una pregunta que estaba
explícitamente abierta en varias recetas, y por eso `pulseaudio` se había construido `-Ddaemon=false`:
sólo el cliente, para que gvc hablara el protocolo sin cerrar la decisión de prestado.

Los dos conviven, y eso es lo que hace que la decisión no rompa nada: `pipewire` va con
`-Dlibpulse=enabled`, que produce **`pipewire-pulse`** — un servidor que habla el protocolo de
PulseAudio. gnome-shell usa libpulse sin enterarse de qué hay del otro lado. **No hubo que tocar una
línea del cliente.**

```
== gnome-qemu :: socket pipewire-0 OK
== gnome-qemu :: socket pulse/native OK — gvc va a poder conectar
Gvc-DEBUG: get server info / update server
Gvc-DEBUG: Updating client: index=32 name='pipewire'
Gvc-DEBUG: Updating sink: index=33 name='auto_null' description='Dummy Output'
```

El `Failed to connect context: Connection refused` desapareció y gvc ve el servidor. El sink es
`auto_null` («Dummy Output»), que es la respuesta honesta: la VM no tiene tarjeta de sonido.

### 🔊 AUDIO FUNCIONANDO (cerrado): el muro era `libudev-zero`

```
Audio
 ├─ Devices:   48. HDA Intel                      [alsa]
 ├─ Sinks:   * 52. HDA Intel Analog Stereo        [vol: 0.40]
 ├─ Sources: * 53. HDA Intel Analog Stereo        [vol: 1.00]
```

Y gvc lo ve con perfiles reales: `output:analog-stereo+input:analog-stereo (Current)`. Ya no hay
`auto_null`.

Hicieron falta TRES cosas, en este orden, y cada una parecía la última:

1. **ALSA en el kernel** (`linux-metal` b3:613bca15). Estaba `-d SOUND -d SND`.
2. **`wireplumber` + `lua`** — el gestor de sesión, sin el cual PipeWire acepta clientes y no enumera.
3. **Parchear `libudev-zero`** para enumerar devices de CLASE. Esto era el muro real: SPA pide el
   device `card` (`alsa-udev.c:178-183`), que no tiene `major:minor`, y libudev-zero recorría
   **sólo** `/sys/dev/{block,char}` — o sea sólo devices CON nodo. **El device que SPA necesita nunca
   entraba en la enumeración**: no faltaba una propiedad, faltaba la mitad del espacio de búsqueda.

**⚠ Y la primera versión del parche, que recorría `/sys/class` ENTERO, arregló el audio y ROMPIÓ EL
VÍDEO.** `/sys/class/drm` no trae sólo tarjetas: trae los conectores (`card0-Virtual-1`) y un fichero
`version`, ninguno con nodo. Al entrar en la enumeración, mutter los toma como candidatos:

```
Failed to use linear monitor configuration: No available CRTC for monitor 'RHT QEMU Monitor'
Page flip failed: drmModeAtomicCommit: Invalid argument      (en bucle)
```

Pantalla negra con el compositor vivo — el mismo cuadro de antes, por una causa nueva. El udev real
también enumera esos conectores, pero les adjunta las propiedades por las que mutter los descarta, y
ésas libudev-zero no las sintetiza (su tabla es fija y está orientada a input).

**La lección, y vale para cualquier reimplementación parcial de una API: ensanchar la enumeración
expone los huecos de PROPIEDADES a más consumidores. Los dos lados del contrato tienen que crecer
juntos.** El parche final va con una **lista acotada de subsistemas** (`sound` hoy); agregar otro es
una línea *y* una prueba de que el consumidor lo necesita y no se rompe.

**Por qué no eudev**, que era el camino que parecía obvio: el repo ya lo había probado en otro frente y
**rompió el input** — su `libudev.so.1` pisa el de libudev-zero y, sin udevd corriendo, no expone
`ID_INPUT`, así que libinput ignora todos los dispositivos en silencio. Verificación A/B en QEMU: eudev
= 0 «New device», libudev-zero = 5. eudev sin udevd es PEOR, y correr udevd contradice la arquitectura.

Costo del parche: `yupana radio libudev-zero` = **51 sellados a deuda** (9 en el cierre GNOME, que se
reconstruyeron acá; el resto —base, cli, KDE— quedan para el latido).

**`wireplumber` cerrado** (b3:39a9a06c), y con él `lua` (b3:e6c35f98), que hubo que autorar: el
Makefile de Lua **no produce `.so` ni `.pc`** —los agrega cada distro—, así que la receta enlaza la
compartida a mano desde la `.a` con `--whole-archive` y escribe el pkg-config. `-fPIC` es obligatorio:
`libwplua` es un objeto compartido y una `.a` no-PIC no entra (la regla del corpus que ya obligó a las
variantes `-shared` de zlib, sqlite, libxml2, libuuid y dbus).

Los TRES procesos y su orden, porque cada uno es cliente del anterior: `pipewire` → `pipewire-pulse`
→ `wireplumber`. `gnome-start` verifica cada paso y termina volcando `wpctl status`, que es el dato que
importa: no el pid, sino **qué ve la política**.

```
PipeWire 'pipewire-0' [1.2.7]
 └─ Clients:  32. pipewire · 34. WirePlumber · 47. WirePlumber [export] · 48. wpctl
Audio
 ├─ Devices:                                    ← vacío
 ├─ Sinks:    *  33. Dummy Output
```

**Y ese `Devices:` vacío NO es un fallo de wireplumber: el kernel de esta imagen no tiene ALSA.**
`recipes/linux-metal.toml:111` lo apaga explícitamente — `-d SOUND -d SND`. No hay `/dev/snd` ni
`/sys/class/sound` que enumerar. Se verificó agregando una `ich9-intel-hda` emulada a QEMU
(`AUDIO=1`, ahora el default de `run-qemu-desktop.sh`): con tarjeta y sin ALSA en el kernel, el
resultado no cambia. **Encender el sonido en la distro es una decisión con costo** —re-sellar el
kernel y rehacer la imagen metal— y queda para quien la tome, no escondida en un default.

Un bug propio que esto destapó: el bloque de wireplumber quedó DUPLICADO en `gnome-start` por un
merge descuidado, y el resultado eran **dos wireplumber vivos peleándose el grafo**. El síntoma no era
un error sino una lista de clientes que se lee normal si no se cuenta: dos «WirePlumber» con pids
distintos en `wpctl status`.

Gotcha de empaquetado, medido: ya existía `incoming-kde/pipewire.toml` sellada y **no se puede reusar**
hidratándola acá — los dos cierres traen glib y **no son la misma glib**: `incoming-kde/glib-shared` y
`incoming-gnome/glib` producen `libglib-2.0.so.0.8800.1` con bytes distintos (`cmp`) y comparten 370
rutas. Proyectar las dos deja que una gane por orden ⇒ dos registros de GType en un proceso, el cuadro
de colord. Hay copia en la cola GNOME. Lo que SÍ fue gratis: `alsa-lib`, cuya única dep resuelve al
catálogo padre ⇒ hash idéntico y cache hit.

### Lo que queda, y ninguno impide el escritorio

- **colord** sigue sin arrancar (gestión de color de pantalla). Su activación por bus ya está
  permitida; falta ver por qué el demonio no sube.
- `Missing required core component Settings` — es gnome-control-center, que no está en el corpus.

### Dos lecciones de método que costaron una iteración cada una

1. **Un pid no es un servicio.** La primera versión del lanzamiento de upowerd decía «lanzado» y el
   shell seguía esperando. Reportar el arranque de un daemon sin comprobar que adquirió su NOMBRE es
   reportar una intención, no un hecho.
2. **El log serial persiste, y la imagen tarda más que el polling.** Tres veces leí un marcador del
   arranque anterior y saqué conclusiones de una VM muerta. El orden correcto: esperar a que la imagen
   esté hecha, *después* esperar el marcador. Truncar no alcanza si la truncada ocurre a mitad de la
   cadena.

## CÓMO SE CERRÓ LA CAPA JS (2026-07-29)

Era el muro anterior, y quedó cerrado. Vale la pena dejar cómo, porque el método es reusable.

El síntoma era: el compositor sube, expone `wayland-0`, y el shell sale con código 1 porque a
`imports.gi.*` le falta un typelib. Cada vez que se cerraba uno, aparecía el siguiente.

**Y ESA ITERACIÓN ERA INNECESARIA — la lista está declarada en el propio shell.**
`js/misc/dependencies.js` enumera EXACTAMENTE lo que el shell exige al arrancar. Cruzada contra el
rootfs hidratado, la frontera completa es de **siete typelibs**, de cinco recetas nuevas y una a
rehacer (medido, no estimado):

| typelib | lo provee | estado |
|---|---|---|
| `GnomeDesktop-4.0`, `GnomeBG-4.0` | `gnome-desktop` | **ya sellada, pero con `-Dintrospection=false` y estática** ⇒ hace falta la variante de isla dinámica |
| `Geoclue-2.0` | geoclue | sin receta |
| `GWeather-4.0` | libgweather | sin receta |
| `IBus-1.0` | ibus | sin receta |
| `Rsvg-2.0` | librsvg | sin receta (**es Rust**) |
| `UPowerGlib-1.0` | upower | sin receta |

Lo demás de esa lista ya está: AccountsService, Atk, Atspi, Gcr, Gdk, Gdm, Gio, GioUnix,
GDesktopEnums, GdkPixbuf, Graphene, Pango, Polkit, PolkitAgent, Soup, y los cinco de mutter/shell
(Meta, Clutter, Cogl, Shell, St). `GnomeBluetooth`, `NM`/`NMA4` y `Malcontent` son condicionales de
compilación o de runtime y no bloquean.

**La regla que sale de acá, y es el precio de no haberla aplicado antes: cuando el muro es
`Requiring X`, no cierres X y vuelvas a arrancar — leé el fichero donde el programa DECLARA sus
dependencias de runtime y cerralas todas de una.** Cada ronda de este bucle cuesta un rebuild de
imagen y un arranque completo.

Cerrados por este camino hasta hoy, en orden: `AccountsService` → `DBus-1.0` (que incluye `Atspi`) →
`cairo-1.0` (que incluyen `Gdk` y `Pango`) → `Gdm-1.0`.

## CÓMO CAYÓ EL MURO ANTERIOR — la hebra de input (cerrado 2026-07-29)

Vale la pena dejarlo escrito porque **el diagnóstico que había acá era falso** y la forma de
refutarlo es reusable.

Decía: «el hilo principal espera en un condvar a que la Mutter Input Thread avise que terminó de
inicializarse; nunca avisa» —eso era correcto— «hipótesis principal: `libudev-zero`, que
`libinput_udev_assign_seat()` enumera por udev y el rootfs no trae udev de verdad». Eso era la
conjetura, apoyada en un episodio parecido de otro frente ([[mirada-usb-nvidia]]).

**La sonda que la refutó**: `libinput list-devices`, que hace exactamente lo que hace
`init_libinput()` de mutter —`udev_new` + `libinput_udev_create_context` +
`libinput_udev_assign_seat("seat0")`—. Volvió con `rc=0` y los tres dispositivos listados. La
enumeración por /sys con libudev-zero funciona perfecto. Está horneada en `gnome-start` y corre en
cada arranque, antes del shell.

**El dato que dio la causa**: `tid 201 [Mutter Input Th] wchan=evdev_read syscall=0`. El hilo estaba
dormido en un `read()` de evdev DENTRO DEL KERNEL. Ese volcado por hilo ya estaba en el script pero
corría *después* del `kill -ABRT`, sobre un `/proc/<pid>/task` que ya no existía: salía vacío y no
decía nada. El core tampoco servía — gdb no desenrolla a través de musl y devuelve `?? ()` para los
11 hilos que no son el principal. **Acá `/proc` le gana al core.**

**La causa**: `arje-logind-compat` abría el device de `TakeDevice` sin `O_NONBLOCK`. El compositor
no elige los flags —recibe el fd ya abierto por el bus—, y libinput asume no-bloqueante para poder
leer en bucle hasta `EAGAIN`; lo dice donde abre (`libinput/src/evdev.c:2401`) y le pasa
`O_RDWR|O_NONBLOCK|O_CLOEXEC` a `open_restricted()`, pero mutter, cuando hay logind, ignora esos
flags y delega la apertura. systemd-logind abre con `O_NONBLOCK`, así que la dependencia no se nota
hasta que la implementa otro. Arreglado en tawasuyu `3dd88f582`, con test.

Y por qué la sonda no lo veía: `libinput list-devices` abre el devnode **él mismo**, con
`O_NONBLOCK`. Sólo el camino por logind pasaba por el fd malo. Esa asimetría es justamente lo que la
convirtió en un experimento y no en una repetición.

Tres perillas de diagnóstico que quedaron y valen para el próximo muro:
`MUTTER_DEBUG` es una **lista de tópicos** (`g_parse_debug_string` sobre `meta_debug_keys`,
`src/core/util.c:42`), no un booleano — con `1` no encendía nada; con `backend` imprime
«Opening and taking control of device file '<path>'» por cada device y dice en cuál se para. El ping
D-Bus a login1 separa «daemon tildado» de «daemon vivo que ya contestó». Y los logs de `/tmp` (tmpfs)
se copian a la raíz ext4 antes de abortar, para sacarlos con `debugfs` junto al core.

## LO QUE YA SE ARREGLÓ ANTES (dos bugs reales en arje-logind-compat)

Ambos encontrados arrancando esta imagen, ambos con test, ambos ya en tawasuyu y sellados:

1. **La sesión se creaba PEREZOSAMENTE.** `ensure_user` sólo corría dentro de un método del Manager,
   y `write_login_state` vive adentro. Pero mutter no empieza por D-Bus: lo primero que hace es
   `sd_pid_get_session()`, que arje-sdlogin-compat resuelve LEYENDO `/run/systemd/sessions`. Huevo y
   gallina — el estado en disco sólo se escribía después de un pedido que sólo ocurre si el estado
   ya existe. Ahora la sesión se crea al arrancar (`eager_session`, apagable con
   `ARJE_LOGIN_EAGER=0`, uid por `ARJE_LOGIN_UID`).
2. **El object path de la Session estaba mal escapado.** Era `/session/_1`; la convención de systemd
   (`bus_label_escape`) codifica como `_<hex>` todo lo que no sea `[A-Za-z0-9]` **y también el primer
   carácter si es dígito**, así que el id `"1"` da **`_31`**. Importa porque el cliente calcula el
   path por su cuenta y no pregunta: mutter reimplementa la misma regla en
   `meta-dbus-utils.c:escape_dbus_component`. Con `_1` no había nadie sirviendo ahí, la propiedad
   `Seat` volvía NULL y mutter —que no chequea— moría de **SIGSEGV en `get_seat_proxy`**. Los objetos
   `User` NO usan este escapado (systemd hardcodea `_<uid>`), así que `user_path()` queda igual.

De paso, un arreglo de portabilidad musl preexistente: `libc::ioctl` toma `c_ulong` en glibc y
`c_int` en musl, así que `DRM_IOCTL_SET_MASTER` se guarda pelado y se convierte en el sitio de uso.

**Y `ARJE_LOGIN_STATE=1` ya existía**: el comentario del daemon dice literalmente «en arje (sin
systemd) el launcher de sesión lo prende. Default off» — y el launcher es `gnome-start`. El puente
que escribía `/run/systemd/` a mano fue reinventar esa perilla; queda como fallback inerte.

El muro gráfico que este runbook listaba como «el que viene después» **nunca apareció**: los fixes
que KDE dejó pagos y precargados en `gnome-start-qemu.sh` (`GBM_ALWAYS_SOFTWARE`, `kms_swrast`,
`LP_NUM_THREADS=1`, `MUTTER_DEBUG_SEND_KMS_MODIFIERS=0`) alcanzaron: mutter crea el renderer gbm y
elige `/dev/dri/card0` como primaria sin quejarse. Es el rédito directo de la campaña anterior.

## Lo que falta, en orden

1. **Los seis proveedores de typelib de la tabla de arriba**, en una sola tanda. `gnome-desktop`
   dinámica+introspectada primero (ya está sellada, sólo cambia el modo), después geoclue,
   libgweather, upower, librsvg e ibus. Ojo al costo de `libelogind`: re-sellarla re-hashea todo lo
   que la declara (mutter, gnome-shell…) ⇒ pasar `yupana radio libelogind` ANTES de tocarla — ya
   pasó dos veces en esta campaña.
2. **Empaquetar la política D-Bus de login1** dentro del artefacto de arje-logind-compat (hoy la
   escribe el script de imagen).
3. Arreglar el `identity mismatch` del `Announce` al bus del fractal (hoy sólo un WARN).
4. Con la capa JS viva: validar CON PANTALLA (`DISP=gtk`), nunca sólo por serial — regla heredada de
   KDE ([[metal-dual-desktop-en-curso]]).

## Recetas nuevas de esta etapa

`accountsservice` (b3:0328b0d0, + parche `fgetspent_r` para musl) · `gi-foreign-typelibs`
(b3:c8a4d2f8 — un `.gir` NO es un typelib: gjs resuelve contra el compilado) · `libgdm`
(b3:46fac484 — sólo la librería cliente; el demonio sigue aparcado por linux-pam).
`libelogind` pasó de 14 a 25 símbolos en tres tandas.
