🔇 wireplumber: construido, corriendo… y NO era el bloqueo
b3:b8f3baf0, selló a la primera. Arranca desde cosmic-start ("wireplumber OK (pid
192) — hay gestor de sesión"), se conecta al grafo (dos clientes en wpctl status) y
wpctl pasa a mostrar una sección Video que antes no existía.
Y el Start de ScreenCast SIGUE sin Response a los 60s. Mismo resultado exacto.
LA HIPÓTESIS QUEDA MATADA POR MEDICIÓN. "Falta el gestor de sesión que mueva el nodo
de Paused a Streaming" era mía y era razonable; no era cierta. Queda escrita junto a
su refutación porque una hipótesis descartada CON EVIDENCIA vale más que una lista de
sospechosos: acota el próximo paso a lo que queda, que es el backend mismo.
El dato nuevo, que es por dónde seguir: con wireplumber corriendo, `wpctl status`
lista `Video → Streams` VACÍO durante el handshake. O sea que el nodo
`cosmic-screencast` que SÍ existe en `pw-cli ls Node` no llega a wireplumber como
stream gestionado. Próximo movimiento: RUST_LOG=trace sobre screencast_thread, no
otra dep.
── por qué receta propia y no la de GNOME ──────────────────────────────────────
La de incoming-gnome funciona pero NEEDea libglib/libgobject/libpipewire de la ISLA
DINÁMICA de GNOME: traerla metería una segunda glib y una segunda pipewire en la
imagen. De 19 deps, 14 dan hash idéntico; las que divergen divergen a propósito —
sobre todo `pipewire`, que acá apunta a la de COSMIC (338d1d8c), la que el escritorio
realmente ARRANCA. Un gestor de sesión contra otra pipewire que la que corre no
gestiona nada.
El cambio de fondo es glib → glib-shared. COSMIC usa la glib ESTÁTICA del corpus, que
le alcanza al portal porque es un ejecutable. Acá no: wireplumber produce un .so y 17
módulos, y libglib-2.0.a tiene 44.275 reubicaciones R_X86_64_32/32S ⇒ no es PIC. La
regla del frente GNOME (readelf -r ANTES de gastar el build) ahorró uno.
Y la salida no fue una campaña nueva sino una MEDICIÓN: incoming-kde/glib-shared
copiada a esta cola resuelve al MISMO hash (0584ce1f) y ya estaba sellada ⇒ cero
rebuild. Idem pcre2-shared, lua y libelogind. Las dos glib coexisten sin pisarse (.a
y .so son ficheros distintos) y el portal conserva sus 3 NEEDED.
escritorio-cosmic: 84 → 89 recetas, 0 faltantes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
10 archivos · +402 −5
- docs/evidencia/cosmic-wireplumber-wpctl-2026-08-05.png +— −—
- docs/runbooks/cosmic-desktop.md +46 −5
- docs/state/targets.toml +5 −0
- recipes/incoming-cosmic/glib-shared.toml +32 −0
- recipes/incoming-cosmic/libelogind.toml +69 −0
- recipes/incoming-cosmic/lua.toml +95 −0
- recipes/incoming-cosmic/pcre2-shared.toml +36 −0
- recipes/incoming-cosmic/wireplumber.toml +86 −0
- scripts/cosmic/cosmic-start-qemu.sh +28 −0
- scripts/cosmic/hydrate-cosmic.sh +5 −0
diff --git a/docs/evidencia/cosmic-wireplumber-wpctl-2026-08-05.png b/docs/evidencia/cosmic-wireplumber-wpctl-2026-08-05.png
new file mode 100644
index 00000000..c99c8516
Binary files /dev/null and b/docs/evidencia/cosmic-wireplumber-wpctl-2026-08-05.png differ
diff --git a/docs/runbooks/cosmic-desktop.md b/docs/runbooks/cosmic-desktop.md
index 5dab1c7d..999e8e76 100644
--- a/docs/runbooks/cosmic-desktop.md
+++ b/docs/runbooks/cosmic-desktop.md
@@ -1090,6 +1090,46 @@ esperarlo. Es una hipótesis, no una medición — construir `wireplumber` la co
- Y uno de shell, al automatizar por QMP: **`| head -N` bufferiza** y deja la terminal en blanco hasta
que el programa termina. Parece que la sonda colgó cuando en realidad está esperando.
+## 🔇 wireplumber: construido, corriendo… y NO era el bloqueo (2026-08-05)
+
+`recipes/incoming-cosmic/wireplumber.toml` 0.5.15 → **`b3:b8f3baf0`**, selló a la primera. Arranca
+desde `cosmic-start` (`wireplumber OK (pid 192) — hay gestor de sesión`), se conecta al grafo (dos
+clientes en `wpctl status`) y `wpctl` pasa a mostrar una sección **Video** que antes no existía.
+
+**Y el `Start` de ScreenCast sigue sin `Response` a los 60s. Exactamente el mismo resultado.**
+
+La hipótesis de la sección anterior —«falta el gestor de sesión que mueva el nodo de `Paused` a
+`Streaming`»— **queda MATADA por medición**. Era mía y era razonable; no era cierta. Se deja escrita
+junto con su refutación porque una hipótesis descartada con evidencia vale más que una lista de
+sospechosos: acota el próximo paso a lo que queda, que es el propio backend.
+
+El dato nuevo, que es por dónde seguir: con wireplumber corriendo, `wpctl status` lista
+`Video → Streams` **vacío** durante el handshake. O sea que el nodo `cosmic-screencast` que sí existe
+en `pw-cli ls Node` **no llega a wireplumber como stream gestionado**. El próximo movimiento es el log
+del backend con `RUST_LOG=trace` alrededor de `screencast_thread`, no otra dep.
+
+### Por qué es una receta propia y no la de GNOME
+
+La de `incoming-gnome` está sellada y funciona, pero su binario NEEDea `libglib-2.0.so.0`,
+`libgobject-2.0.so.0` y `libpipewire-0.3.so.0` **de la isla dinámica de GNOME**: traerla tal cual
+metería una segunda glib y una segunda pipewire en la imagen. De las 19 deps, **14 dan hash idéntico**
+y las que divergen divergen a propósito — sobre todo `pipewire`, que acá apunta a la de COSMIC
+(`338d1d8c`), la que el escritorio realmente arranca. Un gestor de sesión contra otra pipewire que la
+que corre no gestiona nada.
+
+**El cambio de fondo es `glib` → `glib-shared`.** COSMIC usa la glib ESTÁTICA del corpus, que le
+alcanza a `xdg-desktop-portal` porque es un ejecutable. Acá no: wireplumber produce
+`libwireplumber-0.5.so` y 17 módulos, y `libglib-2.0.a` del corpus tiene **44.275 reubicaciones
+`R_X86_64_32/32S`** ⇒ no es PIC y no entra en un `.so`. (Regla del frente GNOME: `readelf -r` ANTES de
+gastar el build — acá ahorró uno.)
+
+La salida no fue una campaña nueva sino una **medición**: `incoming-kde/glib-shared.toml` copiada a
+esta cola resuelve al **mismo hash** (`0584ce1f`) y **ya estaba sellada** ⇒ cero rebuild. Idem
+`pcre2-shared` (`f254abaa`), `lua` (`e6c35f98`) y `libelogind` (`f1a690b2`). Las dos glib **coexisten
+sin pisarse** en el rootfs: `.a` y `.so` son ficheros distintos, y el portal conserva sus 3 NEEDED.
+
+Cierre: `escritorio-cosmic` pasa de 84 a **89 recetas**, 0 faltantes.
+
### El `-L/usr/lib` que pkg-config no da
`pkg-config` **omite los `-L` de directorios que considera estándar del sistema**, asumiendo que el
@@ -1105,11 +1145,12 @@ existe, y eso cuesta igual que un bug.
## Lo que falta, en orden
-1. ~~Arrancar el demonio de pipewire~~ ✅ y ~~el cliente persistente~~ ✅ (`portal-probe`, `b3:af49d32d`).
- **Lo que queda es `wireplumber`**: el stream de ScreenCast se crea y llega a `Paused`, con nodo
- `cosmic-screencast` visible en pipewire, pero nadie lo pasa a `Streaming` y el backend no emite el
- `Response`. La hipótesis es que falta el gestor de sesión — construirlo la confirma o la mata.
- Falta también correr `portal-probe filechooser`, que ya puede capturar la URI.
+1. ~~Demonio de pipewire~~ ✅, ~~cliente persistente~~ ✅ (`portal-probe`, `b3:af49d32d`),
+ ~~`wireplumber`~~ ✅ (`b3:b8f3baf0`) — **y wireplumber NO era el bloqueo**: el `Start` sigue sin
+ `Response`. Lo que queda es el **backend mismo**: con `RUST_LOG=trace` alrededor de
+ `screencast_thread`, mirando por qué el nodo que sí existe en `pw-cli ls Node` no aparece como
+ `Video → Streams` en `wpctl`. Falta también correr `portal-probe filechooser`, que ya puede
+ capturar la URI.
2. **Decidir `gvfs`** (ver la corrección de arriba: las piezas ya están selladas, lo que falta es la
decisión sobre la segunda glib). Destrabaría los montajes remotos de cosmic-files y su applet.
3. **Enchufar la tienda a `.swm`**: servir `org.freedesktop.PackageKit` y encender la feature
diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index d59ce729..43ac29e5 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -137,6 +137,11 @@ paquetes = [
# porque nadie la declara como dep y sin ella el perfil no incluiría con qué verificarse a sí
# mismo — un handshake de portal sólo se puede ejercer DESDE la sesión, con bus y compositor vivos.
"portal-probe",
+ # El GESTOR DE SESIÓN de pipewire. Se lista como raíz porque ningún `[deps]` lo alcanza: es un
+ # CLIENTE del demonio, no una librería de nadie. Trae consigo la glib COMPARTIDA (`glib-shared`),
+ # que no es la estática que usa el resto de la cola — wireplumber produce `.so` y la estática del
+ # corpus no es PIC. Construido y corriendo; NO desbloqueó ScreenCast (ver el runbook).
+ "wireplumber",
# Datos, no binarios: el tema de iconos (y el hicolor del que hereda), las tablas de xkb y una
# tipografía. Ninguno es dep de build de nadie y sin ellos el escritorio arranca roto — sin puntero,
# sin teclado o con tofu.
diff --git a/recipes/incoming-cosmic/glib-shared.toml b/recipes/incoming-cosmic/glib-shared.toml
new file mode 100644
index 00000000..555e1116
--- /dev/null
+++ b/recipes/incoming-cosmic/glib-shared.toml
@@ -0,0 +1,32 @@
+# glib-shared 2.88.1 — variante DINÁMICA (libglib-2.0.so.0 + libgobject/libgio/libgmodule/libgthread)
+# para el escritorio KDE. El canónico (recipes/glib.toml) es `-Ddefault_library=static --prefer-static`
+# (sólo .a) ⇒ no enlaza dentro de un .so. Es la **deuda Capa 0 "glib shared-PIC"** que el ADR 0011 arrastra
+# desde plasma-workspace (se esquivó parcheando NetworkManagerQt a opcional).
+#
+# glib es foundational para el resto del escritorio: libnm (NetworkManager→plasma-nm), polkit (auth),
+# xdg-desktop-portal, gstreamer. Esta variante lo destraba.
+#
+# Deps: libffi ya es PIC (--with-pic en el canónico, enlazable en .so) ⇒ no necesita variante.
+# pcre2-shared SÍ (el pcre2 canónico no tiene --with-pic). zlib-shared por el libz.so versionado.
+# Se conservan los switches del canónico (tests/nls/libmount/selinux/introspection off) para no arrastrar
+# deps nuevas; sólo cambia el modo de librería.
+name = "glib-shared"
+version = "2.88.1"
+
+[source]
+tarball = "https://download.gnome.org/sources/glib/2.88/glib-2.88.1.tar.xz"
+sha256 = "51ab804c56f6eab3e5045c774d1290ac5e4c923d4f9a3d8e33123bee45c1840e"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+link = "dynamic"
+flags = []
+
+[build.phases]
+configure = "PYTHONPATH=/usr/lib/python3.12/site-packages meson setup output --prefix=/usr --buildtype=release -Ddefault_library=shared -Dtests=false -Dnls=disabled -Dlibmount=disabled -Dselinux=disabled -Dman-pages=disabled -Dintrospection=disabled -Dglib_debug=disabled -Dglib_assert=false -Dglib_checks=false"
+compile = "PYTHONPATH=/usr/lib/python3.12/site-packages ninja -C output"
+install = "PYTHONPATH=/usr/lib/python3.12/site-packages DESTDIR=/out meson install -C output --no-rebuild"
+
+[deps]
+build = ["meson", "samurai", "python3", "pkgconf", "libffi", "pcre2-shared", "zlib-shared"]
diff --git a/recipes/incoming-cosmic/libelogind.toml b/recipes/incoming-cosmic/libelogind.toml
new file mode 100644
index 00000000..d6dee0e5
--- /dev/null
+++ b/recipes/incoming-cosmic/libelogind.toml
@@ -0,0 +1,69 @@
+# libelogind — la C-ABI `sd-login` que le faltaba a la distro, implementada en arje-compat.
+#
+# QUÉ RESUELVE: mutter (y polkit, gcr, at-spi2-core) exigen un proveedor de logind por pkg-config —
+# `libsystemd` o `libelogind`— para llamar la API C sd-login. Esa API NO es un cliente D-Bus: lee
+# `/run/systemd/{sessions,users,seats}`. Los otros tres tienen perilla para esquivarla y se
+# construyeron por su camino degradado; **mutter no tiene**, porque `-Dudev=false` exige
+# `-Dlogind=false` (mutter/meson.build:257) y sin udev+logind no hay backend nativo KMS: no hay
+# compositor de verdad, sólo anidado.
+#
+# POR QUÉ NO ES elogind NI UN STUB: `arje-logind-compat` ya escribe ese estado en /run/systemd (ver
+# `arje_compat::login_state`). Lo único que faltaba era una librería que lo LEYERA y lo ofreciera en
+# C. `arje-sdlogin-compat` hace exactamente eso — no inventa datos ni levanta un daemon paralelo.
+# Empaquetar elogind sería traer un logind entero a competirle al que arje ya tiene.
+#
+# Corrige de paso lo que afirma el comentario de recipes/arje-logind-compat.toml («no hace falta la
+# C-ABI sd-login… los escritorios consultan login1 en RUNTIME por D-Bus»): es cierto para los
+# clientes D-Bus y FALSO para mutter, que la enlaza.
+#
+# HUB-ONLY, como toda receta de fuente tawasuyu: la gitea escucha en :2345 y el worker efímero es
+# secretless ⇒ recibe Connection refused en :22. Se muele en el hub.
+name = "libelogind"
+version = "0.0.1"
+
+[source]
+# El `commit` es el identificador inmutable; la URL es sólo locator y NO entra al ArtifactHash.
+# Rama `selfhost/arje-zero-attest-lockfile`, igual que arje-logind-compat, porque es la única que
+# versiona el `Cargo.lock` de la raíz: sin él, el vendoreo `--locked` del fetch no es reproducible
+# (el monorepo lo gitignora en main).
+repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git"
+commit = "749edfe41766e098d3eb18561c6a396610c5cd8e"
+# El monorepo COMMITEA su propio `vendor/` (`[patch.crates-io] smithay = { path = "vendor/smithay" }`,
+# la copia parcheada que mirada necesita para el tearing) y el `cargo vendor` de hammer escribe ahí
+# por defecto, pisándolo: el build muere con `failed to read /src/vendor/smithay/.cargo-checksum.json`
+# al resolver la dep git de taffy. Se vendorea a un subdir aparte, igual que arje-logind-compat.
+# Apareció recién ahora porque el `vendor/` entró a esta rama con el merge de main, no antes.
+# Ver [[hammer-cargo-vendor-clobbers-project-vendor]].
+cargo_vendor_dir = ".hammer-cargo-vendor"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+# dynamic, no static: el producto ES una librería compartida (cdylib). Con `link = "static"` el lab
+# exporta -static y no hay .so que valga.
+link = "dynamic"
+flags = ["-p", "arje-sdlogin-compat"]
+
+[build.phases]
+# Sólo se sobrescribe `install`: el `compile` por defecto del lab (cargo build --release, offline y
+# vendoreado) es el correcto. El default de Cargo copia EJECUTABLES a /usr/bin, y acá no hay ninguno:
+# hay una .so, una cabecera y un .pc.
+#
+# El SONAME (libelogind.so.0) lo fija el build.rs del crate, no esta receta, para que un `cargo build`
+# a mano también produzca algo enlazable. Por eso el fichero se instala directamente con ese nombre y
+# el `libelogind.so` es el symlink de desarrollo que busca el linker.
+#
+# La cabecera va a /usr/include/elogind/systemd/ y el .pc trae `-I${includedir}/elogind`: así el
+# `#include <systemd/sd-login.h>` de mutter resuelve sin que mutter sepa que somos nosotros. Es el
+# mismo arreglo de elogind upstream.
+install = '''
+set -e
+mkdir -p /out/usr/lib/pkgconfig /out/usr/include/elogind/systemd
+cp target/release/libelogind.so /out/usr/lib/libelogind.so.0
+ln -s libelogind.so.0 /out/usr/lib/libelogind.so
+cp 03_ukupacha/arje/arje-sdlogin-compat/include/systemd/sd-login.h /out/usr/include/elogind/systemd/
+# sd-daemon.h va aparte porque en systemd también son dos cabeceras y quien la usa la pide por su
+# nombre: gdm hace `#include <systemd/sd-daemon.h>` en common/gdm-log.c para `sd_booted()`.
+cp 03_ukupacha/arje/arje-sdlogin-compat/include/systemd/sd-daemon.h /out/usr/include/elogind/systemd/
+cp 03_ukupacha/arje/arje-sdlogin-compat/pkgconfig/libelogind.pc /out/usr/lib/pkgconfig/
+'''
diff --git a/recipes/incoming-cosmic/lua.toml b/recipes/incoming-cosmic/lua.toml
new file mode 100644
index 00000000..95c23872
--- /dev/null
+++ b/recipes/incoming-cosmic/lua.toml
@@ -0,0 +1,95 @@
+# lua 5.4.8 — el intérprete y **la librería compartida**, que es lo que hace falta acá.
+#
+# POR QUÉ ENTRA: `wireplumber` —el gestor de sesión de PipeWire, sin el cual PipeWire acepta clientes
+# pero no enumera ni enruta dispositivos— tiene su política escrita EN LUA y embebe un intérprete
+# (`lib/wplua`). Su `subprojects/lua.wrap` DESCARGA Lua 5.5 de lua.org, cosa imposible en el sandbox
+# hermético (`--wrap-mode=nodownload`), así que va con `-Dsystem-lua=true` y el sistema tiene que
+# tener Lua. Su meson acepta 5.5, 5.4 o 5.3 (meson.build:91-95); se elige **5.4**, la serie estable
+# larga que usan las distros, en vez de la 5.5 del wrap.
+#
+# ══ LO QUE ESTA RECETA TIENE QUE INVENTAR, porque upstream no lo da ═════════════════════════════
+# El Makefile de Lua sólo produce `liblua.a` y los binarios. **No hay regla de `.so` ni fichero
+# pkg-config**: los agrega cada distro. Acá se agregan las dos cosas, y a mano, porque no hay dónde
+# copiarlas de:
+#
+# 1. `-fPIC` en `MYCFLAGS`. Sin esto la `.a` no es reubicable y `libwplua` —que es un objeto
+# compartido— no linkea: es la regla del corpus sobre `.a` no-PIC dentro de un `.so`
+# («relocation R_X86_64_32 … recompile with -fPIC»), la misma que obligó a las variantes
+# `-shared` de zlib, sqlite, libxml2, libuuid y dbus.
+# 2. La `.so` se enlaza a mano desde la `.a` con `--whole-archive`, porque una librería estática sólo
+# aporta los objetos que alguien referencia y acá hay que llevárselos TODOS. SONAME
+# `liblua-5.4.so.0`, que es el que graba quien enlace.
+# 3. El `.pc` se escribe acá. El nombre importa: wireplumber prueba `lua-5.4`, `lua5.4` y `lua54` en
+# ese orden, así que el fichero se llama `lua5.4.pc` y ADEMÁS se deja `lua-5.4.pc` y `lua.pc` como
+# enlaces — es más barato satisfacer las tres convenciones que adivinar cuál buscará el próximo
+# consumidor.
+#
+# `-DLUA_USE_LINUX` es lo que activa `dlopen` y el resto de POSIX; sin él Lua se compila en modo ANSI
+# genérico y `require` de módulos C no funciona. **SIN readline a propósito**: sólo lo usa el REPL
+# interactivo, y el ncurses del catálogo es static-only y arrastra `-static` al link (el mismo
+# problema que hizo saltar `pw-top` en la receta de pipewire).
+#
+# `SYSLIBS` pierde el `-Wl,-E` de upstream: exportar todos los símbolos del EJECUTABLE sirve para que
+# módulos C cargados por `require` vean la API de Lua, y acá el que exporta es la `.so`.
+name = "lua"
+version = "5.4.8"
+
+[source]
+tarball = "https://www.lua.org/ftp/lua-5.4.8.tar.gz"
+sha256 = "4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+link = "dynamic"
+
+[build.phases]
+configure = "true"
+compile = '''
+set -e
+# CC se pasa explícito: el Makefile de Lua lo fija a `gcc -std=gnu99` en la línea 9 y ganaría sobre el
+# del entorno.
+make -C src all CC="hammer-zig-cc -std=gnu99" \
+ SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-ldl" MYCFLAGS="-fPIC"
+# La .so que upstream no construye. --whole-archive porque de una estática el linker sólo se lleva lo
+# referenciado, y acá hay que llevárselo todo.
+hammer-zig-cc -shared -o src/liblua-5.4.so.0.0.0 \
+ -Wl,-soname,liblua-5.4.so.0 \
+ -Wl,--whole-archive src/liblua.a -Wl,--no-whole-archive -lm
+'''
+install = '''
+set -e
+mkdir -p /out/usr/bin /out/usr/lib/pkgconfig /out/usr/include /out/usr/share/man/man1
+install -m755 src/lua /out/usr/bin/lua5.4
+install -m755 src/luac /out/usr/bin/luac5.4
+ln -s lua5.4 /out/usr/bin/lua
+ln -s luac5.4 /out/usr/bin/luac
+install -m644 src/lua.h src/luaconf.h src/lualib.h src/lauxlib.h src/lua.hpp /out/usr/include/
+install -m644 src/liblua.a /out/usr/lib/liblua-5.4.a
+install -m755 src/liblua-5.4.so.0.0.0 /out/usr/lib/
+ln -s liblua-5.4.so.0.0.0 /out/usr/lib/liblua-5.4.so.0
+ln -s liblua-5.4.so.0 /out/usr/lib/liblua-5.4.so
+ln -s liblua-5.4.so /out/usr/lib/liblua.so
+cat > /out/usr/lib/pkgconfig/lua5.4.pc <<'PC'
+prefix=/usr
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+INTERPRETER=${exec_prefix}/bin/lua5.4
+COMPILER=${exec_prefix}/bin/luac5.4
+
+Name: Lua
+Description: Lua language engine
+Version: 5.4.8
+Requires:
+Libs: -L${libdir} -llua-5.4 -lm
+Cflags: -I${includedir}
+PC
+# Las tres convenciones de nombre que se usan por ahí; wireplumber prueba lua-5.4, lua5.4 y lua54.
+ln -s lua5.4.pc /out/usr/lib/pkgconfig/lua-5.4.pc
+ln -s lua5.4.pc /out/usr/lib/pkgconfig/lua54.pc
+ln -s lua5.4.pc /out/usr/lib/pkgconfig/lua.pc
+'''
+
+[deps]
+build = ["make", "pkgconf"]
diff --git a/recipes/incoming-cosmic/pcre2-shared.toml b/recipes/incoming-cosmic/pcre2-shared.toml
new file mode 100644
index 00000000..7ebc72d7
--- /dev/null
+++ b/recipes/incoming-cosmic/pcre2-shared.toml
@@ -0,0 +1,36 @@
+# pcre2-shared 10.47 — variante DINÁMICA (libpcre2-8.so.0, PIC) para el escritorio KDE.
+# El canónico (recipes/pcre2.toml) es --disable-shared SIN --with-pic ⇒ su .a no enlaza dentro de un .so.
+# Lo exige glib-shared (libglib-2.0.so linkea pcre2 para GRegex). Deuda Capa 0 (static↔dinámico).
+#
+# compiler=gcc igual que el canónico: su comentario advierte que zig-cc MISCOMPILA libs C (patrón file/jq);
+# el gcc del rootfs es musl, y el .so resultante es ABI-compatible con los consumidores zig.
+# --disable-symvers como el canónico (musl no maneja symbol versioning de la forma que asume pcre2).
+name = "pcre2-shared"
+version = "10.47"
+
+[source]
+tarball = "https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.bz2"
+sha256 = "47fe8c99461250d42f89e6e8fdaeba9da057855d06eb7fc08d9ca03fd08d7bc7"
+
+[build]
+compiler = "gcc"
+target = "x86_64-linux-musl"
+link = "dynamic"
+flags = []
+
+[build.phases]
+compile = '''
+./configure \
+ --build=$CBUILD --host=$CHOST \
+ --prefix=/usr \
+ --enable-shared --disable-static \
+ --enable-pcre2-16 --enable-pcre2-32 \
+ --enable-jit \
+ --enable-pcre2grep-libz \
+ --disable-symvers
+make -j"$(nproc)"
+'''
+install = "make DESTDIR=/out install && find /out -name '*.la' -delete"
+
+[deps]
+build = ["zlib-shared"]
diff --git a/recipes/incoming-cosmic/wireplumber.toml b/recipes/incoming-cosmic/wireplumber.toml
new file mode 100644
index 00000000..ab1cec75
--- /dev/null
+++ b/recipes/incoming-cosmic/wireplumber.toml
@@ -0,0 +1,86 @@
+# wireplumber 0.5.15 — EL GESTOR DE SESIÓN de PipeWire, en la variante de COSMIC.
+#
+# ── POR QUÉ EXISTE ESTA COPIA Y NO SE REUSA LA DE GNOME ─────────────────────────────────────────
+# `recipes/incoming-gnome/wireplumber.toml` está sellada y funciona, pero su binario NEEDea
+# `libglib-2.0.so.0`, `libgobject-2.0.so.0` y `libpipewire-0.3.so.0` **de la isla dinámica de GNOME**.
+# Traerlo tal cual a la imagen de COSMIC significaría meter una segunda glib y una segunda pipewire —
+# justo el choque de 370 ficheros solapados que la campaña midió y evitó.
+#
+# La regla del runbook es que copiar entre colas sale gratis **sólo si toda la clausura resuelve
+# igual**. Acá NO resuelve igual, y eso es lo que se quiere: de las 19 deps, 14 dan hash idéntico y
+# las que divergen divergen A PROPÓSITO —`pipewire` apunta a la de COSMIC (`338d1d8c`), que es la que
+# el escritorio realmente ARRANCA. Un gestor de sesión contra otra pipewire que la que corre no
+# gestiona nada.
+#
+# ── EL CAMBIO DE FONDO: `glib` → `glib-shared` ──────────────────────────────────────────────────
+# COSMIC usa la `glib` ESTÁTICA del corpus (le sirve a `xdg-desktop-portal`, que es un ejecutable).
+# Acá no alcanza: wireplumber produce `libwireplumber-0.5.so` y sus módulos, y la estática del corpus
+# tiene **44.275 reubicaciones `R_X86_64_32/32S`** en `libglib-2.0.a` ⇒ no es PIC y no se puede
+# enlazar dentro de un `.so`. (Regla del frente GNOME: `readelf -r` ANTES de gastar un build.)
+#
+# La salida no fue una campaña nueva sino una MEDICIÓN: `incoming-kde/glib-shared.toml` copiada acá
+# resuelve al **mismo hash** (`0584ce1f`) y **ya está sellada** ⇒ cero rebuild. Idem `pcre2-shared`
+# (`f254abaa`), `lua` (`e6c35f98`) y `libelogind` (`f1a690b2`), traídas por la misma razón: no existen
+# en el catálogo padre y sus hashes coinciden con los de su cola de origen.
+#
+# ── LO QUE APORTA, Y POR QUÉ HACE FALTA ACÁ ─────────────────────────────────────────────────────
+# `pipewire` se construye con `-Dsession-managers=[]`: arranca, acepta clientes y sirve —el portal ya
+# figura como cliente suyo— pero **no hay política** de qué es entrada, qué es salida, ni quién mueve
+# un nodo de `Paused` a `Streaming`. Y ahí está parado ScreenCast: el backend crea el nodo
+# (`cosmic-screencast`, `Video/Source`, visible en `pw-cli ls Node`), su log dice
+# `state-changed 'Connecting' -> 'Paused'`… y no emite el `Response`. La hipótesis a confirmar o
+# matar con esta receta es que falta exactamente esto.
+#
+# La política está escrita EN LUA: embebe un intérprete (`lib/wplua`) y sus reglas viven en
+# `src/scripts/*.lua`. Por eso la dep interesante no es PipeWire sino **`lua`**.
+#
+# `-Dsystem-lua=true` es obligatorio: por defecto usa su `subprojects/lua.wrap`, que **DESCARGA Lua
+# 5.5 de lua.org**, y el sandbox va con `--wrap-mode=nodownload`. Con la perilla prendida busca por
+# pkg-config `lua-5.5`/`lua5.5`/`lua55`, después las 5.4 y después las 5.3 (meson.build:91-95):
+# nuestra `lua5.4.pc` entra por la segunda tanda.
+#
+# `-Delogind=enabled` — la C-ABI de logind otra vez, y acá la sirve `arje-sdlogin-compat`, el mismo
+# shim que en el resto del fractal. wireplumber la usa para saber si la sesión está activa antes de
+# tomar los dispositivos, que es justo el problema que ese shim resuelve.
+#
+# Apagados: `-Dsystemd=disabled` y sus units (PID1 es arje-zero y el arranque lo hace `cosmic-start`);
+# `-Ddoc=disabled` (falta sphinx/doxygen); `-Dtests=false -Ddbus-tests=false`; `-Dintrospection=disabled`
+# porque nadie carga `Wp-0.5` por typelib.
+name = "wireplumber"
+version = "0.5.15"
+
+[source]
+tarball = "https://gitlab.freedesktop.org/pipewire/wireplumber/-/archive/0.5.15/wireplumber-0.5.15.tar.gz"
+sha256 = "baa121bc918df5fa0e0e70755bb1c99ffab0ab107225ecf99aa470e2c6ba5e7b"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+link = "dynamic"
+
+[build.phases]
+# Sin `rm -rf output`: con el árbol de fuentes compartido (ADR 0012) borrar el dir de build es
+# sabotear a quien esté compilando en paralelo. Lección heredada de la receta de GNOME.
+configure = '''
+set -e
+PKG_CONFIG_PATH=/usr/lib/pkgconfig PYTHONPATH=/usr/lib/python3.12/site-packages meson setup output \
+ --prefix=/usr --buildtype=release --wrap-mode=nodownload \
+ -Ddefault_library=shared \
+ -Dsystem-lua=true -Dsystem-lua-version=auto \
+ -Delogind=enabled -Dsystemd=disabled \
+ -Dsystemd-system-service=false -Dsystemd-user-service=false \
+ -Dintrospection=disabled -Ddoc=disabled \
+ -Dtests=false -Ddbus-tests=false \
+ -Dc_args=-Wno-error=date-time
+'''
+compile = "PYTHONPATH=/usr/lib/python3.12/site-packages ninja -C output"
+install = "PYTHONPATH=/usr/lib/python3.12/site-packages DESTDIR=/out meson install -C output --no-rebuild"
+
+[deps]
+# Difiere de la de GNOME en cuatro nombres, y los cuatro a propósito: `glib-shared` en vez de `glib`
+# (la estática no es PIC), `pcre2-shared` en vez de `pcre2` (coherencia con lo que el `.pc` de
+# glib-shared declara), y `pipewire`/`pulseaudio` que resuelven a los de ESTA cola — los que el
+# escritorio arranca.
+build = ["meson", "samurai", "python3", "pkgconf", "gettext-tiny", "lua", "pipewire",
+ "glib-shared", "dbus-shared", "libelogind", "alsa-lib", "libsndfile", "pulseaudio",
+ "expat", "pcre2-shared", "libffi", "libudev-zero", "zlib-shared"]
diff --git a/scripts/cosmic/cosmic-start-qemu.sh b/scripts/cosmic/cosmic-start-qemu.sh
index 35d27df8..c5c48c40 100755
--- a/scripts/cosmic/cosmic-start-qemu.sh
+++ b/scripts/cosmic/cosmic-start-qemu.sh
@@ -170,6 +170,34 @@ else
say "!! sin /usr/bin/pipewire — ScreenCast no va a poder crear stream"
fi
+# ── EL GESTOR DE SESIÓN: wireplumber ────────────────────────────────────────────────────────────
+# pipewire se construye con `-Dsession-managers=[]`, o sea que arranca y sirve pero NO TIENE
+# POLÍTICA: nada decide qué es entrada, qué es salida, ni quién mueve un nodo de `Paused` a
+# `Streaming`. wireplumber es esa política, escrita en Lua.
+#
+# Por qué está acá y no antes: **necesita el socket de pipewire ya abierto**. Se lanza después a
+# propósito, no por orden estético — un gestor de sesión sin demonio que gestionar sale con error.
+#
+# Acá NO se puede esperar un socket, porque wireplumber no abre ninguno: es un CLIENTE de pipewire,
+# no un servidor. Lo que dice que hizo su trabajo es que aparezca como cliente en el grafo
+# (`pw-cli ls Client`), y eso es una medición del sistema, no una condición de arranque. Así que se
+# espera un instante y se reporta si el proceso sigue vivo — con el log a mano, que es lo que sirve
+# cuando falla.
+if [ -x /usr/bin/wireplumber ] && [ -S "$XDG_RUNTIME_DIR/pipewire-0" ]; then
+ /usr/bin/wireplumber >/tmp/wireplumber.log 2>&1 &
+ WP_PID=$!
+ sleep 2
+ if alive $WP_PID; then
+ say "wireplumber OK (pid $WP_PID) — hay gestor de sesión"
+ else
+ say "!! wireplumber MURIÓ:"; dump /tmp/wireplumber.log
+ fi
+elif [ -x /usr/bin/wireplumber ]; then
+ say "!! wireplumber presente pero sin socket de pipewire — no se lanza"
+else
+ say "-- sin wireplumber (pipewire sirve, pero sin política de sesión)"
+fi
+
# ── FONDO: color liso, porque la imagen por defecto NO SE PUEDE EMPAQUETAR ────────────────────
# El default de cosmic-bg apunta a `/usr/share/backgrounds/cosmic/orion_nebula_nasa_heic0601a.jpg`,
# que vive en el repo `cosmic-wallpapers`… en **git-lfs**: el tarball de GitHub pesa 20 KB y trae
diff --git a/scripts/cosmic/hydrate-cosmic.sh b/scripts/cosmic/hydrate-cosmic.sh
index 2e6ce37e..e329bba4 100755
--- a/scripts/cosmic/hydrate-cosmic.sh
+++ b/scripts/cosmic/hydrate-cosmic.sh
@@ -84,6 +84,11 @@ else
# con el bus y el compositor vivos— y sale estática, sin un NEEDED, justamente para que nunca
# falle por algo del sistema que está midiendo.
recipes/incoming-cosmic/portal-probe.toml
+ # El GESTOR DE SESIÓN de pipewire. Ningún `[deps]` lo alcanza —es un cliente del demonio, no una
+ # librería de nadie— así que es raíz a mano como todo lo que se lanza por PATH. Trae consigo la
+ # glib COMPARTIDA (`glib-shared`), que no es la estática que usa el resto de la cola: wireplumber
+ # produce `.so` y la estática del corpus no es PIC.
+ recipes/incoming-cosmic/wireplumber.toml
recipes/incoming-cosmic/cosmic-app-library.toml
recipes/incoming-cosmic/cosmic-workspaces-epoch.toml
# Ni binario ni librería: DATOS. El tema de iconos de COSMIC — sin él el escritorio no se ve