tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

feat(instalador): adoptar la particion en vez de formatearla, con la mudanza de verdad

El caso mas comun no tenia salida buena: una sola particion con un sistema
adentro y el hogar de la gente adentro de esa misma particion. Formatear pierde
lo unico que no se repone; instalar encima deja dos juegos de configuracion y
nada que diga cual manda.

Ahora el sistema viejo se muda a un subdirectorio y el hogar se queda donde
esta. No copia un byte: mover dentro del mismo sistema de archivos solo cambia
entradas de directorio. Y deja algo mejor que un respaldo, porque el sistema
anterior queda entero y accesible.

Vive en su propia libreria, aparte del instalador, y eso es la mitad del
trabajo: el instalador entero necesita privilegios y un disco, pero estas
funciones, sobre directorios, se prueban enteras en un segundo. Un mecanismo que
solo se puede probar con el hardware delante es un mecanismo que no se prueba, y
este es el que decide si alguien pierde sus archivos.

Van 25 comprobaciones. Las cinco que importan son las que pueden fallar en
silencio: que el hogar no se mueva, que adoptar dos veces no pise la primera
mudanza, que un disco de datos sin forma de sistema se rechace nombrando lo que
vio, y que si el sistema nuevo no entra no se mueva nada. Esa ultima lleva su
brazo de control, porque un verde tambien lo daria una guarda que rechaza
siempre.

Los usuarios del hogar adoptado se cosechan con el mismo codigo que los del
hogar en otra particion. Es la misma pregunta y dos implementaciones divergen en
el caso raro, con el sintoma de un escritorio que no puede escribir en su propia
carpeta.

Lo que falta es metal: montar un sistema de archivos de verdad y ponerle la
etiqueta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

4b1f19d8d85eda4740af94810b184b1d192427f6 · ruwaq sergio · (20 p'unchaw ñawpaq) · tayta 789cff7a0 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 6

crates/takana-cli/src/metal.rs

7 qillqakuna · +491 −61

diff --git a/crates/takana-cli/src/metal.rs b/crates/takana-cli/src/metal.rs
index 8bb67914..143eb28e 100644
--- a/crates/takana-cli/src/metal.rs
+++ b/crates/takana-cli/src/metal.rs
@@ -111,6 +111,15 @@ pub enum MetalCmd {
         /// se puede pasar por acá. Usá `PARTUUID=`, que además es la identidad estable.
         #[arg(long)]
         keep: Option<String>,
+        /// NO FORMATEAR el destino: mover el sistema que ya hay ahí a `/sistema-anterior` y dejar
+        /// su `/home` donde está, con sus archivos y sus dueños.
+        ///
+        /// Es el caso más común de todos —una sola partición con un Linux y el `/home` adentro— y
+        /// el que peor resuelve formatear: lo único irreponible de una máquina es el `/home` de
+        /// quien la usa. Mover dentro del mismo filesystem no copia un byte, y el sistema anterior
+        /// queda entero y accesible, que es mejor que un respaldo.
+        #[arg(long)]
+        adopt: bool,
         /// Disco sobre el que buscar el hueco de `--free`.
         #[arg(long)]
         disk: Option<String>,
@@ -138,9 +147,9 @@ pub fn run(cmd: MetalCmd) -> Result<()> {
         MetalCmd::Check => check(),
         MetalCmd::Survey { json, payload } => survey(json, payload.as_deref()),
         MetalCmd::Install {
-            payload, root, esp, free, keep, disk, yes, unattended, load_only, jump, console,
+            payload, root, esp, free, keep, adopt, disk, yes, unattended, load_only, jump, console,
         } => install(Instalacion {
-            payload, root, esp, free, keep, disk, yes, unattended, load_only, jump, console,
+            payload, root, esp, free, keep, adopt, disk, yes, unattended, load_only, jump, console,
         }),
     }
 }
@@ -151,6 +160,7 @@ struct Instalacion {
     esp: Option<String>,
     free: bool,
     keep: Option<String>,
+    adopt: bool,
     disk: Option<String>,
     yes: bool,
     unattended: bool,
@@ -577,6 +587,9 @@ fn cmdline_de(i: &Instalacion) -> String {
     if let Some(k) = &i.keep {
         s.push_str(&format!(" takana.keep={k}"));
     }
+    if i.adopt {
+        s.push_str(" takana.adopt");
+    }
     if i.unattended {
         s.push_str(" takana.yes");
     }
@@ -611,7 +624,7 @@ mod tests {
 
     fn base() -> Instalacion {
         Instalacion {
-            payload: None, root: None, esp: None, free: false, keep: None, disk: None,
+            payload: None, root: None, esp: None, free: false, keep: None, adopt: false, disk: None,
             yes: false, unattended: false, load_only: false, jump: false,
             console: CONSOLA.to_string(),
         }
@@ -639,6 +652,23 @@ mod tests {
         assert!(!c.contains("takana.free"), "{c}");
     }
 
+    #[test]
+    fn adoptar_viaja_por_la_cmdline_y_no_aparece_si_no_se_pide() {
+        // La mitad que importa es la segunda: un `takana.adopt` de más hace que el instalador NO
+        // formatee cuando se le pidió formatear, y el síntoma es un sistema nuevo conviviendo con
+        // la mitad de uno viejo. Es más caro que el error contrario.
+        assert!(!cmdline_de(&base()).contains("takana.adopt"), "sin --adopt no viaja nada");
+        let i = Instalacion {
+            root: Some("PARTUUID=1111-2222".into()),
+            adopt: true,
+            ..base()
+        };
+        let c = cmdline_de(&i);
+        assert!(c.contains("takana.adopt"), "{c}");
+        // Y no se parece a `takana.free`: son dos decisiones distintas sobre el mismo disco.
+        assert!(!c.contains("takana.free"), "{c}");
+    }
+
     #[test]
     fn el_home_conservado_viaja_por_la_cmdline_y_no_aparece_si_no_se_pide() {
         // Que `--keep` NO aparezca cuando no se pide es la mitad que importa: un `takana.keep=` de
diff --git a/docs/32-instalador-de-usuario.md b/docs/32-instalador-de-usuario.md
index 2df38888..a2cb9fc1 100644
--- a/docs/32-instalador-de-usuario.md
+++ b/docs/32-instalador-de-usuario.md
@@ -209,7 +209,7 @@ Y una que es de este lado pero conviene saber: **`TOOL_<binario>=` funciona y
 salva el caso del initramfs** — resolvió `sfdisk`, `blkid`, `mke2fs`, `mmd`,
 `mcopy` y `mdir` sin store, tal como promete su comentario.
 
-## 7.ter 🔴 ADOPTAR la partición, en vez de formatearla (pedido, 2026-09-30)
+## 7.ter 🟡 ADOPTAR la partición, en vez de formatearla (HECHO 2026-10-01, falta metal)
 
 El caso que hoy no tiene salida buena: **una sola partición con un Linux adentro
 y el `/home` adentro de esa misma partición**. Es el más común de todos, y es el
@@ -248,6 +248,32 @@ Las guardas que pedimos, cada una por su motivo:
 7. El tamaño mínimo cambia de significado: «quedan ≥ N libres **después** de lo
    que ya hay», no «la partición mide ≥ N».
 
+**Hecho el 2026-10-01**, el mismo día que se pidió:
+
+```sh
+takana metal install --root PARTUUID=… --adopt       # no formatea: muda y se queda con el /home
+sh scripts/adoptar-test.sh                            # 25 comprobaciones, sin root, <1 s
+```
+
+Vive en `scripts/lib/adoptar.sh`, aparte del instalador, **para poder certificarlo sin root**: el
+instalador entero necesita losetup, mount y un disco; estas funciones, sobre directorios, se
+prueban enteras en un segundo. Un mecanismo que sólo se puede probar con el hardware delante es un
+mecanismo que no se prueba — y éste es el que decide si alguien pierde su `/home`.
+
+Las ocho guardas están puestas, y el banco mide las cinco que pueden salir mal en silencio: que
+`/home` no se mueva, que `lost+found` tampoco, que adoptar dos veces no pise la primera mudanza,
+que un disco de datos (sin forma de sistema) se rechace nombrando lo que vio, y que **si el sistema
+nuevo no entra no se mueva nada**. Esa última lleva su propio brazo de control: el mismo árbol, con
+el `df` de verdad, sí entra — sin eso el verde también lo daría una guarda que rechaza siempre.
+
+El `/home` de adentro se cosecha con **el mismo código** que el `/home` de `--keep`
+(`scripts/lib/usuarios-del-home.sh`): es la misma pregunta —cómo hace el sistema nuevo para que esos
+archivos sigan siendo de quien son— y dos implementaciones divergen en el caso raro, con el síntoma
+de un escritorio que no puede escribir en su propia carpeta.
+
+Lo que el banco **no** cubre, y se dice para que nadie lo lea como «probado entero»: montar un ext4
+de verdad y ponerle la etiqueta con `e2label`. Eso es metal.
+
 Del lado de churay ya está la mitad que le toca: `foreign-fs` lee la raíz del
 volumen **sin montarlo** y dice si la forma del árbol es la de un sistema (`etc`
 más programas) y si el `/home` está adentro — por la forma y no por un archivo
diff --git a/scripts/adoptar-test.sh b/scripts/adoptar-test.sh
new file mode 100755
index 00000000..6c1037c9
--- /dev/null
+++ b/scripts/adoptar-test.sh
@@ -0,0 +1,103 @@
+#!/bin/sh
+# adoptar-test.sh — la mudanza del sistema anterior, certificada SIN root y sin un disco.
+#
+# ── QUÉ PRUEBA, Y POR QUÉ ASÍ ──────────────────────────────────────────────────────────────────
+# Adoptar es la operación que decide si alguien pierde su /home, así que lo que se comprueba no es
+# lo que imprime sino lo que QUEDA en el árbol: qué se movió, qué no se movió, y que nada se borró.
+# Corre sobre directorios y por eso no necesita losetup, mount ni root — un mecanismo que sólo se
+# puede probar con el hardware delante es un mecanismo que no se prueba.
+#
+# Lo que este banco NO cubre, y se dice para que nadie lo lea como «adoptar está probado entero»:
+# montar un ext4 de verdad y ponerle la etiqueta con `e2label`. Eso vive en el banco con root
+# (`install-particion-test.sh`) y en metal.
+set -eu
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
+T="${T:-work/.adoptar-test}"; rm -rf "$T"; mkdir -p "$T"
+es_bloque() { [ -b "$1" ]; }
+. scripts/lib/usuarios-del-home.sh
+. scripts/lib/adoptar.sh
+
+OK=0; MAL=0
+ok()  { OK=$((OK+1));  printf '  ✓ %s\n' "$1"; }
+mal() { MAL=$((MAL+1)); printf '  ✗ %s\n' "$1"; }
+comprobar() { if [ "$2" = "$3" ]; then ok "$1"; else mal "$1 (esperaba «$3», salió «$2»)"; fi; }
+
+# Un sistema de mentira con un /home adentro, que es el caso del que trata todo esto.
+sistema() {
+    _s="$1"; rm -rf "$_s"; mkdir -p "$_s"/etc "$_s"/usr/bin "$_s"/var/log "$_s"/home/sergio "$_s"/lost+found
+    echo "viejo" > "$_s/etc/os-release"
+    echo "un programa" > "$_s/usr/bin/algo"
+    echo "la foto de la abuela" > "$_s/home/sergio/foto.jpg"
+    mkdir -p "$_s/home/sergio/.config/app"; echo "cfg" > "$_s/home/sergio/.config/app/x.conf"
+}
+# Y el árbol del sistema nuevo que se va a instalar encima.
+arbol_nuevo() {
+    _a="$1"; rm -rf "$_a"; mkdir -p "$_a"/etc "$_a"/usr/bin "$_a"/home
+    echo "nuevo" > "$_a/etc/os-release"
+    echo "etc/passwd" > "$_a/etc/passwd"; : > "$_a/etc/group"
+}
+
+echo "== 1. una partición con un sistema y un /home adentro se adopta =="
+DEST="$T/caso1"; sistema "$DEST"; arbol_nuevo "$T/arbol1"
+USE_FREE=""; ADOPT=1
+adopt_validar >/dev/null
+comprobar "el subdirectorio es /sistema-anterior" "$ADOPT_SUB" "sistema-anterior"
+case " $ADOPT_MUEVE " in *" etc "*) ok "etc se mueve" ;; *) mal "etc tendría que moverse" ;; esac
+case " $ADOPT_MUEVE " in *" home "*) mal "/home NO se mueve: es el punto entero" ;; *) ok "/home no se mueve" ;; esac
+case " $ADOPT_MUEVE " in *" lost+found "*) mal "lost+found no se mueve" ;; *) ok "lost+found no se mueve" ;; esac
+comprobar "cosechó al usuario del /home de adentro" "$(cut -d: -f1 "$ADOPT_USUARIOS" | tr '\n' ' ')" "sergio "
+adopt_mudar "$T/arbol1" >/dev/null
+comprobar "la foto sigue donde estaba" "$(cat "$DEST/home/sergio/foto.jpg")" "la foto de la abuela"
+comprobar "los dotfiles viajan con el /home" "$(cat "$DEST/home/sergio/.config/app/x.conf")" "cfg"
+comprobar "el sistema viejo está entero un nivel adentro" "$(cat "$DEST/sistema-anterior/etc/os-release")" "viejo"
+comprobar "y su programa también" "$(cat "$DEST/sistema-anterior/usr/bin/algo")" "un programa"
+comprobar "el sistema nuevo quedó en la raíz" "$(cat "$DEST/etc/os-release")" "nuevo"
+[ -f "$DEST/sistema-anterior/MUDANZA.txt" ] && ok "quedó la marca de la mudanza" || mal "falta MUDANZA.txt"
+grep -q "se-quedo-donde-estaba=/home" "$DEST/sistema-anterior/MUDANZA.txt" && ok "la marca dice qué se quedó" || mal "la marca no nombra el /home"
+
+echo "== 2. adoptar dos veces NO pisa la primera mudanza =="
+ADOPT_SUB=""; ADOPT_MUEVE=""; arbol_nuevo "$T/arbol2"
+adopt_validar >/dev/null
+comprobar "la segunda va a /sistema-anterior-2" "$ADOPT_SUB" "sistema-anterior-2"
+case " $ADOPT_MUEVE " in *" sistema-anterior "*) mal "la mudanza anterior no se vuelve a mover" ;; *) ok "la mudanza anterior se queda donde está" ;; esac
+adopt_mudar "$T/arbol2" >/dev/null
+comprobar "lo de la primera sigue ahí" "$(cat "$DEST/sistema-anterior/etc/os-release")" "viejo"
+comprobar "la foto NO se movió nunca" "$(cat "$DEST/home/sergio/foto.jpg")" "la foto de la abuela"
+
+echo "== 3. lo que no tiene forma de sistema no se adopta =="
+DEST="$T/caso3"; rm -rf "$DEST"; mkdir -p "$DEST/Fotos" "$DEST/Documentos"
+echo "mía" > "$DEST/Fotos/a.jpg"
+if ( adopt_validar >/dev/null 2>&1 ); then mal "un disco de datos no es un sistema que adoptar"; else ok "un disco de datos se rechaza, y se dice"; fi
+comprobar "y no le tocó nada" "$(cat "$DEST/Fotos/a.jpg")" "mía"
+
+echo "== 4. si el sistema nuevo NO ENTRA, no se mueve nada =="
+DEST="$T/caso4"; sistema "$DEST"; arbol_nuevo "$T/arbol4"
+adopt_validar >/dev/null
+# Un árbol que no entra: se fuerza el `df` a decir que queda poquísimo. La función tapa al binario.
+df() { printf 'Filesystem 1K-blocks Used Available Use%%%% Mounted\n x 100 99 1 99%%%% /\n'; }
+SALIDA=$(adopt_mudar "$T/arbol4" 2>&1) && mal "tendría que abortar por espacio" || ok "aborta por espacio"
+# **Y aborta por LO QUE CREEMOS**, no por otra cosa: un `du` que falta abortaría igual, y el test
+# diría que la guarda del espacio funciona cuando nunca llegó a mirarla.
+case "$SALIDA" in *"no entra"*) ok "y aborta por el espacio, no por otra cosa" ;;
+                  *) mal "abortó por otra razón: $(echo "$SALIDA" | head -1)" ;; esac
+unset -f df
+[ -d "$DEST/etc" ] && ok "etc sigue en la raíz: no se movió nada" || mal "movió algo pese a abortar"
+[ -e "$DEST/sistema-anterior" ] && mal "dejó el subdirectorio a medias" || ok "no dejó nada a medias"
+# El brazo de control de ESTE caso: el mismo árbol, con el `df` de verdad, SÍ entra. Sin esto el
+# verde de arriba también lo daría una guarda que rechaza siempre.
+adopt_mudar "$T/arbol4" >/dev/null && ok "control: con el espacio real, el mismo árbol sí entra" \
+    || mal "control: tendría que entrar y no entró"
+
+echo "== 5. el brazo de control del instrumento: ¿ve lo que tiene que ver? =="
+# Un «está igual que antes» sólo significa algo si el instrumento es capaz de ver una diferencia.
+DEST="$T/caso5"; sistema "$DEST"; arbol_nuevo "$T/arbol5"
+HUELLA() { find "$DEST" | sort | md5sum; }
+ANTES=$(HUELLA)
+comprobar "sin tocar nada, la huella no cambia" "$(HUELLA)" "$ANTES"
+adopt_validar >/dev/null; adopt_mudar "$T/arbol5" >/dev/null
+if [ "$(HUELLA)" = "$ANTES" ]; then mal "la huella NO vio la mudanza: el instrumento es ciego"
+else ok "la huella sí ve una mudanza (el instrumento mide algo)"; fi
+
+echo
+printf '%d bien · %d mal\n' "$OK" "$MAL"
+[ "$MAL" = "0" ] || exit 1
diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
index e765c4a7..2b16de06 100755
--- a/scripts/instalador-initramfs.sh
+++ b/scripts/instalador-initramfs.sh
@@ -24,7 +24,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
 # Los argumentos son OPCIONALES a propósito. Un initramfs con el destino horneado sirve para UNA
 # máquina y hay que rehacerlo para la siguiente — eso no es un instalador, es un apaño. Sin
 # argumentos, el mismo fichero arranca en cualquier máquina y decide allí: lee la cmdline del kernel
-# (`takana.root=`, `takana.esp=`, `takana.free`) y, si no hay nada, muestra el censo del terreno y
+# (`takana.root=`, `takana.esp=`, `takana.free`, `takana.adopt`) y, si no hay nada, muestra el censo y
 # pregunta. Lo que se pase acá queda sólo como VALOR POR DEFECTO.
 ARGS_INSTALADOR=""
 while [ $# -gt 0 ]; do ARGS_INSTALADOR="$ARGS_INSTALADOR $1"; shift; done
@@ -228,6 +228,7 @@ for _w in $(cat /proc/cmdline 2>/dev/null); do
         takana.disk=*) ARGS="$ARGS --disk ${_w#takana.disk=}" ;;
         takana.free)   ARGS="$ARGS --use-free-space" ;;
         takana.keep=*) ARGS="$ARGS --keep ${_w#takana.keep=}" ;;
+        takana.adopt)  ARGS="$ARGS --adopt" ;;
         takana.yes)    YES=1 ;;
         takana.console=*) CONSOLA_PEDIDA="${_w#takana.console=}" ;;
     esac
@@ -321,6 +322,7 @@ echo "  · sin parámetros                  ⇒ censo del terreno + pregunta (si
 echo "  · takana.root=PARTUUID=…          ⇒ destino por identidad estable"
 echo "  · takana.free takana.disk=/dev/…  ⇒ instalar al lado, en espacio sin asignar"
 echo "  · takana.keep=PARTUUID=…:/home    ⇒ conservar ese /home sin formatearlo"
+echo "  · takana.adopt                    ⇒ NO formatear: mudar el sistema de ahí a /sistema-anterior"
 echo "  · takana.yes                      ⇒ sin preguntar (sólo junto a lo de arriba)"
 echo
 echo "Para SALTAR a él sin pasar por el firmware (el userspace de esta máquina muere ACÁ), lo normal
diff --git a/scripts/lib/adoptar.sh b/scripts/lib/adoptar.sh
new file mode 100755
index 00000000..c08a6017
--- /dev/null
+++ b/scripts/lib/adoptar.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+# lib/adoptar.sh — adoptar una partición en vez de formatearla.
+#
+# Vive aparte de `takana-install-partition.sh` por una razón práctica: así se puede **certificar sin
+# root**. El instalador entero necesita losetup, mount y un disco; estas funciones, sobre un
+# directorio, se prueban enteras en un segundo (`scripts/adoptar-test.sh`). Un mecanismo que sólo se
+# puede probar con el hardware delante es un mecanismo que no se prueba.
+#
+# Lo sourcea el instalador; usa `$DEST` y las funciones `es_bloque` de quien la carga.
+
+# ── ADOPTAR LA PARTICIÓN (`--adopt`) — ni formatear, ni convivir ──────────────────────────────
+#
+# El caso más común de todos: **una sola partición, con un Linux adentro y el `/home` adentro de esa
+# misma partición**. De las tres salidas posibles sólo una sirve:
+#
+#   · formatear               → se pierde el /home, que es lo único irreponible de una máquina.
+#   · instalar encima          → quedan DOS juegos de /etc y de paquetes, y nada que diga cuál manda.
+#                                No es una opción; no se ofrece.
+#   · **adoptar**              → el sistema viejo se muda a `/sistema-anterior` y el /home se queda.
+#
+# Lo que la hace barata: mover dentro del mismo filesystem es un `rename(2)` — instantáneo y sin
+# copiar un byte. Y deja algo MEJOR que un respaldo: el sistema anterior queda entero y accesible,
+# se le puede sacar un archivo o volver a él. Formatear no deja nada.
+#
+# Las guardas, cada una por lo que pasaría sin ella:
+#   1. El espacio se comprueba **antes de la primera mudanza**, con el tamaño real del árbol nuevo.
+#      Descubrirlo a mitad deja un sistema partido en dos sitios y ninguno arrancable.
+#   2. Un `mv` por entrada de primer nivel, cada uno atómico: un corte de luz deja un árbol a
+#      medias pero ningún archivo roto, y el estado se puede leer.
+#   3. Queda una marca con la lista de lo que se movió y cuándo.
+#   4. **`/home` no se mueve.** Es el punto entero.
+#   5. `lost+found` tampoco, y si ya hay un `/sistema-anterior` se numera en vez de pisarlo.
+#   6. Si lo que hay adentro no tiene forma de sistema, no se adopta: se dice, y se usa el camino
+#      de siempre. Mover cuatro directorios de datos a `/sistema-anterior` no le sirve a nadie.
+
+# adopt_montar <modo> — monta el destino en /run/takana-adopt. Si ya estaba montado, lo usa donde
+# está y NO lo desmonta después: un `umount` de un montaje que no hicimos puede propagarse.
+adopt_montar() {
+    _am_modo="$1"
+    # Un DIRECTORIO como destino es el modo de prueba: ya está «montado», se usa tal cual. Es lo que
+    # permite certificar la mudanza entera sin losetup, sin mount y sin root — y lo que hace que
+    # este mecanismo tenga pruebas en vez de tener sólo un banco que casi nadie corre.
+    if [ -d "$DEST" ]; then ADOPT_DIR="$DEST"; ADOPT_MONTE_YO=""; return 0; fi
+    ADOPT_DIR=$(awk -v d="$(readlink -f "$DEST")" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
+    if [ -n "$ADOPT_DIR" ]; then
+        ADOPT_MONTE_YO=""
+        return 0
+    fi
+    ADOPT_DIR="/run/takana-adopt"
+    mkdir -p "$ADOPT_DIR"
+    # Un fichero de prueba se monta por loop; un dispositivo, directo. El modo de prueba sin
+    # hardware es el que hace que esto se pueda certificar sin un disco de verdad.
+    if es_bloque "$DEST"; then
+        mount -o "$_am_modo" "$DEST" "$ADOPT_DIR" 2>/dev/null
+    else
+        mount -o "loop,$_am_modo" "$DEST" "$ADOPT_DIR" 2>/dev/null
+    fi || {
+        echo "takana-install-partition: --adopt no puede montar $DEST (probé $_am_modo)." >&2
+        echo "                          Sin montarlo no hay nada que adoptar, y NADA se tocó." >&2; exit 1; }
+    ADOPT_MONTE_YO=1
+}
+
+adopt_desmontar() {
+    [ -n "$ADOPT_MONTE_YO" ] || return 0
+    umount "$ADOPT_DIR" 2>/dev/null || echo "    ⚠ no pude desmontar $ADOPT_DIR." >&2
+    ADOPT_MONTE_YO=""
+}
+
+# adopt_validar — corre ANTES del plan y ANTES de tocar nada: mira qué hay, elige el subdirectorio y
+# cosecha los usuarios, para que el plan pueda decirlo todo con el disco todavía intacto.
+ADOPT_USUARIOS="${TMPDIR:-/tmp}/takana-adopt-usuarios.$$"
+adopt_validar() {
+    [ -z "$USE_FREE" ] || {
+        echo "takana-install-partition: --adopt y --use-free-space se excluyen." >&2
+        echo "                          En un hueco sin asignar no hay ningún sistema que adoptar." >&2; exit 2; }
+    adopt_montar ro,nodev,nosuid,noexec
+    # 1) ¿Tiene forma de sistema? Por la FORMA del árbol y no por un archivo de una distribución
+    #    concreta: esto tiene que servir en la máquina de cualquiera, no en la de quien lo escribe.
+    _av_etc=""; [ -d "$ADOPT_DIR/etc" ] && _av_etc=1
+    _av_prog=""
+    for _av_d in usr bin sbin; do [ -d "$ADOPT_DIR/$_av_d" ] && _av_prog=1; done
+    [ -n "$_av_etc" ] && [ -n "$_av_prog" ] || {
+        echo "takana-install-partition: en $DEST no hay un sistema que adoptar." >&2
+        echo "                          Vi: $(ls -A "$ADOPT_DIR" 2>/dev/null | tr '\n' ' ' | cut -c1-200)" >&2
+        echo "                          Para instalar acá, hacelo SIN --adopt: se formatea." >&2
+        adopt_desmontar; exit 1; }
+    # 2) El subdirectorio, sin pisar uno anterior. Que se numere y no se pise es lo que hace que
+    #    adoptar dos veces no borre la primera mudanza.
+    ADOPT_SUB="sistema-anterior"
+    if [ -e "$ADOPT_DIR/$ADOPT_SUB" ]; then
+        _av_n=2
+        while [ -e "$ADOPT_DIR/sistema-anterior-$_av_n" ] && [ "$_av_n" -lt 10 ]; do _av_n=$((_av_n + 1)); done
+        [ "$_av_n" -lt 10 ] || {
+            echo "takana-install-partition: ya hay nueve /sistema-anterior en $DEST. No sigo." >&2
+            adopt_desmontar; exit 1; }
+        ADOPT_SUB="sistema-anterior-$_av_n"
+        echo "    (ya había un /sistema-anterior: el de ahora va a $ADOPT_SUB)"
+    fi
+    # 3) Qué se mueve, y qué no. `/home` y `lost+found` se quedan; lo demás viaja.
+    ADOPT_MUEVE=""
+    for _av_e in "$ADOPT_DIR"/* "$ADOPT_DIR"/.*; do
+        [ -e "$_av_e" ] || continue
+        _av_b=$(basename "$_av_e")
+        case "$_av_b" in
+            .|..|home|lost+found|sistema-anterior|sistema-anterior-[0-9]) continue ;;
+        esac
+        ADOPT_MUEVE="$ADOPT_MUEVE $_av_b"
+    done
+    # 4) Los usuarios del /home que está adentro: el mismo criterio que --keep.
+    : > "$ADOPT_USUARIOS"
+    if [ -d "$ADOPT_DIR/home" ]; then cosechar_usuarios "$ADOPT_DIR/home" "$ADOPT_USUARIOS"; fi
+    adopt_desmontar
+}
+
+# adopt_mudar <árbol> — la mudanza de verdad. Corre DESPUÉS de armar el stage, porque hasta que el
+# árbol no existe no se sabe cuánto ocupa, y el espacio es la guarda que no se puede hacer a ojo.
+adopt_mudar() {
+    _ad_t="$1"
+    adopt_montar rw
+    # 1) EL ESPACIO, antes de mover nada. Con el tamaño real del árbol más un margen: un filesystem
+    #    al borde del lleno falla en el último fichero, que es cuando ya no hay vuelta atrás.
+    _ad_necesita=$(du -sk "$_ad_t" 2>/dev/null | awk '{print $1}' || echo "")
+    _ad_libre=$(df -k "$ADOPT_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || echo "")
+    case "${_ad_necesita:-x}${_ad_libre:-x}" in
+        *x*) echo "takana-install-partition: no pude medir el espacio de $DEST (du/df). No sigo." >&2
+             adopt_desmontar; exit 1 ;;
+    esac
+    _ad_con_margen=$(( _ad_necesita + _ad_necesita / 10 + 262144 ))
+    [ "$_ad_libre" -ge "$_ad_con_margen" ] || {
+        echo "takana-install-partition: no entra. El sistema nuevo ocupa $((_ad_necesita / 1024)) MiB" >&2
+        echo "                          y en $DEST quedan $((_ad_libre / 1024)) MiB libres (hacen falta" >&2
+        echo "                          $((_ad_con_margen / 1024)) MiB con margen)." >&2
+        echo "                          NADA se movió: el disco está como estaba." >&2
+        adopt_desmontar; exit 1; }
+    echo "    espacio: ocupa $((_ad_necesita / 1024)) MiB · libres $((_ad_libre / 1024)) MiB ✓"
+    # 2) La marca, ANTES de mover: si se corta la luz a la mitad, lo que quedó a medias se puede
+    #    leer. Una marca escrita al final sólo documenta los casos que salieron bien.
+    mkdir -p "$ADOPT_DIR/$ADOPT_SUB"
+    {
+        echo "# El sistema que había en esta partición, movido acá por el instalador de takana."
+        echo "# Está ENTERO: no se copió ni se borró nada, sólo cambiaron de directorio."
+        echo "# Para volver a usarlo, sus archivos están donde siempre, un nivel más adentro."
+        echo "cuando=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo desconocido)"
+        echo "particion=$DEST"
+        echo "se-quedo-donde-estaba=/home"
+        echo "movido=$ADOPT_MUEVE"
+    } > "$ADOPT_DIR/$ADOPT_SUB/MUDANZA.txt"
+    # 3) Un `mv` por entrada, cada uno atómico. Si uno falla se para ahí y se dice cuál: seguir
+    #    moviendo después de un fallo reparte el sistema viejo entre dos sitios.
+    for _ad_e in $ADOPT_MUEVE; do
+        mv "$ADOPT_DIR/$_ad_e" "$ADOPT_DIR/$ADOPT_SUB/$_ad_e" || {
+            echo "takana-install-partition: no pude mover «$_ad_e» a $ADOPT_SUB. Paro acá." >&2
+            echo "                          Lo que ya se movió está en $ADOPT_SUB y se puede volver a sacar." >&2
+            adopt_desmontar; exit 1; }
+    done
+    echo "    ✓ el sistema anterior está en /$ADOPT_SUB, entero ($(echo $ADOPT_MUEVE | wc -w) entradas)"
+    # 4) El sistema nuevo, encima. `/home` del árbol viene vacío, así que no pisa el que ya está.
+    cp -a "$_ad_t/." "$ADOPT_DIR/" || {
+        echo "takana-install-partition: falló la copia del sistema nuevo." >&2
+        echo "                          El anterior sigue entero en /$ADOPT_SUB." >&2
+        adopt_desmontar; exit 1; }
+    adopt_desmontar
+    # 5) La etiqueta, que es por donde el arranque encuentra la raíz. Sin esto la partición
+    #    adoptada tiene el sistema nuevo adentro y nadie la reconoce al bootear.
+    if [ -d "$DEST" ]; then
+        return 0   # modo de prueba: un directorio no tiene etiqueta de filesystem que poner
+    elif [ -n "${E2LABEL:-}" ] && [ -x "$E2LABEL" ]; then
+        "$E2LABEL" "$DEST" hammer-root || {
+            echo "takana-install-partition: no pude ponerle LABEL=hammer-root a $DEST." >&2; exit 1; }
+    else
+        echo "takana-install-partition: hace falta e2label para --adopt y no lo encontré." >&2; exit 1
+    fi
+}
+
diff --git a/scripts/lib/usuarios-del-home.sh b/scripts/lib/usuarios-del-home.sh
new file mode 100755
index 00000000..631cb65c
--- /dev/null
+++ b/scripts/lib/usuarios-del-home.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+# lib/usuarios-del-home.sh — los usuarios que ya existen en un `/home`, llevados al sistema nuevo.
+#
+# Lo comparten los dos caminos que conservan datos de alguien: `--keep` (el /home está en otra
+# partición) y `--adopt` (el /home está dentro de la partición que se adopta). Es la misma pregunta
+# —cómo hace el sistema nuevo para que esos archivos sigan siendo de quien son— y por eso hay una
+# sola respuesta: dos implementaciones de esto divergen en el caso raro, y el síntoma es un
+# escritorio que no puede escribir en su propia carpeta.
+#
+# Vive aparte para poder certificarla sin root, igual que `lib/adoptar.sh`.
+
+# cosechar_usuarios <dir-de-home> <fichero-salida> — un usuario por DIRECTORIO de primer nivel, con
+# el dueño de ese directorio. Es el mismo criterio para el `/home` que está en otra partición
+# (`--keep`) y para el que está DENTRO de la partición que se adopta (`--adopt`): el uid de los
+# datos manda, y el login es el nombre del directorio. Una sola copia, porque dos criterios de
+# cosecha que se separan dan dos juegos de usuarios distintos para el mismo disco.
+cosechar_usuarios() {
+    _cu_dir="$1"; _cu_out="$2"
+    : > "$_cu_out"
+    for _cu_h in "$_cu_dir"/*; do
+        [ -d "$_cu_h" ] || continue
+        _cu_n=$(basename "$_cu_h")
+        [ "$_cu_n" = "lost+found" ] && continue
+        # Un login que no es un login se rechaza NOMBRÁNDOLO, no se salta en silencio: un directorio
+        # con un nombre raro en /home es justo el que alguien va a echar de menos.
+        case "$_cu_n" in
+            *[!a-z0-9_-]*|-*|"") echo "    ⚠ «$_cu_n» no es un nombre de usuario válido: no creo usuario para ese directorio (los datos NO se tocan)." ;;
+            *)
+                _cu_u=$(stat -c %u "$_cu_h" 2>/dev/null || echo "")
+                _cu_g=$(stat -c %g "$_cu_h" 2>/dev/null || echo "")
+                case "${_cu_u:-x}${_cu_g:-x}" in *x*) echo "    ⚠ no pude leer el dueño de $_cu_h: lo salto." ; continue ;; esac
+                [ "$_cu_u" = "0" ] && { echo "    ⚠ $_cu_h es de root (uid 0): no creo usuario, y los datos NO se tocan."; continue; }
+                echo "${_cu_n}:${_cu_u}:${_cu_g}" >> "$_cu_out"
+                ;;
+        esac
+    done
+}
+
+# usuarios_aplicar <árbol> <lista> — crea en la raíz NUEVA los usuarios que ya existen en el
+# `/home` que se conserva, **con el uid que tienen sus archivos**. Lo comparten `--keep` (el /home
+# en otra partición) y `--adopt` (el /home dentro de la partición que se adopta): es la misma
+# pregunta —cómo hace el sistema nuevo para que esos archivos sigan siendo de quien son— y una sola
+# respuesta, porque dos implementaciones de esto divergen en el caso raro y el síntoma es un
+# escritorio que no puede escribir en su propia carpeta.
+usuarios_aplicar() {
+    _ua_t="$1"; _ua_lista="$2"
+    # ⚠ **Nada se edita EN SITIO.** El árbol del stage son HARDLINKS del rootfs sellado (ADR 0005):
+    # un `>>` sobre `$_ua_t/etc/passwd` escribiría dentro del ARTEFACTO DEL STORE y envenenaría el
+    # corpus para todo el mundo, sin que nada falle. Se escribe un fichero nuevo y se `mv` encima,
+    # que rompe el enlace y deja el inode del store intacto.
+    for _ua_f in etc/passwd etc/group etc/shadow; do
+        [ -e "$_ua_t/$_ua_f" ] || continue
+        cp "$_ua_t/$_ua_f" "$_ua_t/$_ua_f.keep-tmp"
+        mv "$_ua_t/$_ua_f.keep-tmp" "$_ua_t/$_ua_f"
+    done
+    mkdir -p "$_ua_t/etc" "$_ua_t/home"
+    [ -e "$_ua_t/etc/passwd" ] || : > "$_ua_t/etc/passwd"
+    [ -e "$_ua_t/etc/group" ]  || : > "$_ua_t/etc/group"
+    # 1) Los usuarios, con SU uid. Un choque de uid con un usuario de sistema (sshd es 74) no se
+    #    arregla solo: se aborta ANTES de formatear y se dice cuál, porque la salida es del usuario
+    #    —renombrar o renumerar— y no nuestra.
+    while IFS=':' read -r _ua_n _ua_u _ua_g; do
+        [ -n "$_ua_n" ] || continue
+        _ua_choque=$(awk -F: -v u="$_ua_u" -v n="$_ua_n" '$3==u && $1!=n {print $1; exit}' "$_ua_t/etc/passwd")
+        [ -n "$_ua_choque" ] && {
+            echo "takana-install-partition: el uid $_ua_u del /home conservado (usuario «$_ua_n») ya es de" >&2
+            echo "                          «$_ua_choque» en el sistema nuevo. No sigo, y NADA se formateó." >&2
+            echo "                          Salidas: renombrá ese directorio, o instalá sin --keep y copiá a mano." >&2
+            exit 1; }
+        if awk -F: -v n="$_ua_n" '$1==n {found=1} END {exit !found}' "$_ua_t/etc/passwd"; then
+            echo "    · «$_ua_n» ya existe en el sistema nuevo: no lo vuelvo a crear."
+            continue
+        fi
+        printf '%s:x:%s:%s:%s:/home/%s:/bin/sh\n' "$_ua_n" "$_ua_u" "$_ua_g" "$_ua_n" "$_ua_n" >> "$_ua_t/etc/passwd"
+        awk -F: -v g="$_ua_g" 'BEGIN{r=1} $3==g {r=0} END{exit r}' "$_ua_t/etc/group" \
+            || printf '%s:x:%s:\n' "$_ua_n" "$_ua_g" >> "$_ua_t/etc/group"
+        # Contraseña BLOQUEADA, no vacía: `!` no casa con ningún hash, así que no hay login por
+        # consola hasta que alguien le ponga una. Una entrada con campo vacío es login SIN CLAVE.
+        [ -e "$_ua_t/etc/shadow" ] && printf '%s:!:20000:0:99999:7:::\n' "$_ua_n" >> "$_ua_t/etc/shadow"
+        echo "    · usuario «$_ua_n» creado con uid $_ua_u (el mismo que tienen sus archivos)"
+    done < "$_ua_lista"
+}
+
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index de25c03a..a65d4b91 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -64,6 +64,14 @@
 #                              uid, y con el nombre del directorio como login. Se comprueba que
 #                              monte ANTES de tocar la raíz: descubrir que el /home que se iba a
 #                              conservar no monta, con la raíz ya formateada, es el peor momento.
+#   --adopt                    NO FORMATEAR el destino: mover el sistema que ya hay ahí a
+#                              `/sistema-anterior` y dejar su `/home` donde está. Es el caso más
+#                              común de todos —una sola partición con un Linux y el /home adentro—
+#                              y el que peor resuelve formatear: lo único irreponible de una
+#                              máquina es el /home de quien la usa. Mover dentro del mismo
+#                              filesystem es un `rename(2)`: no copia un byte. El sistema anterior
+#                              queda ENTERO y accesible, que es mejor que un respaldo.
+#                              Incompatible con `--use-free-space` (ahí no hay nada que adoptar).
 #   --no-nvram                 no tocar la NVRAM (pruebas, o firmware que se registra a mano).
 #   --yes                      no preguntar (para guiones; en interactivo pide confirmación).
 # Variables: ROOTFS KERNEL BUSYBOX (como install-image-efi.sh) · STAGE (def work/.install-part-stage)
@@ -73,6 +81,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
 DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
 USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"; SOLO_CENSO=""; CENSO_JSON=""; SOLO_ELEGIR=""
 KEEP_SPEC=""; KEEP_PUNTO=""; KEEP_DEV=""; KEEP_UUID=""; KEEP_PARTUUID=""; KEEP_DIR=""; KEEP_MONTE_YO=""
+ADOPT=""; ADOPT_DIR=""; ADOPT_MONTE_YO=""; ADOPT_SUB=""; ADOPT_MUEVE=""
 while [ $# -gt 0 ]; do
     case "$1" in
         --root-part) DEST="${2:?}"; shift 2 ;;
@@ -86,6 +95,7 @@ while [ $# -gt 0 ]; do
         --pick)      SOLO_ELEGIR="${2:?--pick necesita el fichero donde dejar la respuesta}"; shift 2 ;;
         --use-free-space) USE_FREE=1; shift ;;
         --keep)      KEEP_SPEC="${2:?--keep necesita <dev|PARTUUID=…|UUID=…|LABEL=…>:<punto>}"; shift 2 ;;
+        --adopt)     ADOPT=1; shift ;;
         --on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
         --yes|-y)    ASSUME_YES=1; shift ;;
         -h|--help)   sed -n '2,40p' "$0"; exit 0 ;;
@@ -103,7 +113,19 @@ case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root:
 # las GUARDAS, o sea antes de tocar nada. Salen del store por hash vigente; `TOOL_<binario>=` las
 # inyecta cuando no hay store (dentro del initramfs de un instalador, p. ej.).
 . "$(dirname "$0")/lib/herramientas-desde-store.sh"
+# Los dos caminos que conservan datos ajenos viven en sus propias librerías, y eso es lo que permite
+# certificarlos SIN root (`scripts/adoptar-test.sh`): el instalador entero necesita losetup, mount y
+# un disco; estas funciones, sobre directorios, se prueban enteras en un segundo.
+. "$(dirname "$0")/lib/usuarios-del-home.sh"
+. "$(dirname "$0")/lib/adoptar.sh"
 SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
+# `e2label` sólo hace falta para `--adopt`, que no formatea: ahí la etiqueta por la que el arranque
+# encuentra la raíz hay que ponerla a mano. Se resuelve acá arriba —con el resto— y sólo cuando se
+# pidió adoptar, para no exigir una herramienta que el camino normal no usa.
+E2LABEL=""
+if [ -n "$ADOPT" ]; then
+    E2LABEL=$(herramienta e2fsprogs sbin/e2label e2label) || exit 1
+fi
 # Releer la tabla es su propio problema: tras añadir una entrada, el kernel NO se entera solo y el
 # device nuevo no aparece. `partx -a` se lo dice; `blockdev --rereadpt` es el ioctl crudo, que falla
 # si alguna partición del disco está montada — por eso se intentan los dos, en ese orden. Los dos
@@ -979,24 +1001,7 @@ keep_validar() {
         KEEP_MONTE_YO=1
     fi
     # 5) La cosecha: un usuario por DIRECTORIO de primer nivel, con el dueño de ese directorio.
-    : > "$KEEP_USUARIOS"
-    for _kv_h in "$KEEP_DIR"/*; do
-        [ -d "$_kv_h" ] || continue
-        _kv_n=$(basename "$_kv_h")
-        [ "$_kv_n" = "lost+found" ] && continue
-        # Un login que no es un login se rechaza NOMBRÁNDOLO, no se salta en silencio: un directorio
-        # con un nombre raro en /home es justo el que alguien va a echar de menos.
-        case "$_kv_n" in
-            *[!a-z0-9_-]*|-*|"") echo "    ⚠ «$_kv_n» no es un nombre de usuario válido: no creo usuario para ese directorio (los datos NO se tocan)." ;;
-            *)
-                _kv_u=$(stat -c %u "$_kv_h" 2>/dev/null || echo "")
-                _kv_g=$(stat -c %g "$_kv_h" 2>/dev/null || echo "")
-                case "${_kv_u:-x}${_kv_g:-x}" in *x*) echo "    ⚠ no pude leer el dueño de $_kv_h: lo salto." ; continue ;; esac
-                [ "$_kv_u" = "0" ] && { echo "    ⚠ $_kv_h es de root (uid 0): no creo usuario, y los datos NO se tocan."; continue; }
-                echo "${_kv_n}:${_kv_u}:${_kv_g}" >> "$KEEP_USUARIOS"
-                ;;
-        esac
-    done
+    cosechar_usuarios "$KEEP_DIR" "$KEEP_USUARIOS"
     # 6) ¿Tiene espacio? Avisa y no aborta: un /home lleno es un problema del usuario, no un fallo
     #    de la instalación. Pero callarlo lo convierte en «el escritorio no arranca» la semana que viene.
     _kv_libre=$(df -m "$KEEP_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || echo "")
@@ -1017,41 +1022,7 @@ keep_desmontar() {
 # Corre DESPUÉS de armar el árbol y ANTES del `mke2fs`: si algo falla acá, todavía no se destruyó nada.
 keep_aplicar() {
     _ka_t="$1"
-    # ⚠ **Nada se edita EN SITIO.** El árbol del stage son HARDLINKS del rootfs sellado (ADR 0005):
-    # un `>>` sobre `$_ka_t/etc/passwd` escribiría dentro del ARTEFACTO DEL STORE y envenenaría el
-    # corpus para todo el mundo, sin que nada falle. Se escribe un fichero nuevo y se `mv` encima,
-    # que rompe el enlace y deja el inode del store intacto.
-    for _ka_f in etc/passwd etc/group etc/shadow; do
-        [ -e "$_ka_t/$_ka_f" ] || continue
-        cp "$_ka_t/$_ka_f" "$_ka_t/$_ka_f.keep-tmp"
-        mv "$_ka_t/$_ka_f.keep-tmp" "$_ka_t/$_ka_f"
-    done
-    mkdir -p "$_ka_t/etc" "$_ka_t/home"
-    [ -e "$_ka_t/etc/passwd" ] || : > "$_ka_t/etc/passwd"
-    [ -e "$_ka_t/etc/group" ]  || : > "$_ka_t/etc/group"
-    # 1) Los usuarios, con SU uid. Un choque de uid con un usuario de sistema (sshd es 74) no se
-    #    arregla solo: se aborta ANTES de formatear y se dice cuál, porque la salida es del usuario
-    #    —renombrar o renumerar— y no nuestra.
-    while IFS=':' read -r _ka_n _ka_u _ka_g; do
-        [ -n "$_ka_n" ] || continue
-        _ka_choque=$(awk -F: -v u="$_ka_u" -v n="$_ka_n" '$3==u && $1!=n {print $1; exit}' "$_ka_t/etc/passwd")
-        [ -n "$_ka_choque" ] && {
-            echo "takana-install-partition: el uid $_ka_u del /home conservado (usuario «$_ka_n») ya es de" >&2
-            echo "                          «$_ka_choque» en el sistema nuevo. No sigo, y NADA se formateó." >&2
-            echo "                          Salidas: renombrá ese directorio, o instalá sin --keep y copiá a mano." >&2
-            exit 1; }
-        if awk -F: -v n="$_ka_n" '$1==n {found=1} END {exit !found}' "$_ka_t/etc/passwd"; then
-            echo "    · «$_ka_n» ya existe en el sistema nuevo: no lo vuelvo a crear."
-            continue
-        fi
-        printf '%s:x:%s:%s:%s:/home/%s:/bin/sh\n' "$_ka_n" "$_ka_u" "$_ka_g" "$_ka_n" "$_ka_n" >> "$_ka_t/etc/passwd"
-        awk -F: -v g="$_ka_g" 'BEGIN{r=1} $3==g {r=0} END{exit r}' "$_ka_t/etc/group" \
-            || printf '%s:x:%s:\n' "$_ka_n" "$_ka_g" >> "$_ka_t/etc/group"
-        # Contraseña BLOQUEADA, no vacía: `!` no casa con ningún hash, así que no hay login por
-        # consola hasta que alguien le ponga una. Una entrada con campo vacío es login SIN CLAVE.
-        [ -e "$_ka_t/etc/shadow" ] && printf '%s:!:20000:0:99999:7:::\n' "$_ka_n" >> "$_ka_t/etc/shadow"
-        echo "    · usuario «$_ka_n» creado con uid $_ka_u (el mismo que tienen sus archivos)"
-    done < "$KEEP_USUARIOS"
+    usuarios_aplicar "$_ka_t" "$KEEP_USUARIOS"
     # 2) La declaración que lee el initramfs de pivote justo antes del switch_root.
     mkdir -p "$_ka_t/etc/takana"
     {
@@ -1072,9 +1043,19 @@ if [ -n "$KEEP_SPEC" ]; then
     echo "==> conservar: $KEEP_DEV → $KEEP_PUNTO   (NO se formatea)"
     keep_validar
 fi
+# `--adopt` se valida ACÁ por lo mismo que `--keep`: para que el plan pueda decir qué se mueve y a
+# quién se va a crear, y para que un destino sin sistema adentro aborte con el disco todavía intacto.
+if [ -n "$ADOPT" ]; then
+    echo "==> adoptar: $DEST   (NO se formatea — el sistema de ahí se muda a un subdirectorio)"
+    adopt_validar
+fi
 echo "==> destino: $DEST   ($( [ -b "$DEST" ] && echo 'partición real' || echo 'fichero de prueba'))"
 if [ -b "$DEST" ] && command -v blkid >/dev/null 2>&1; then
-    echo "    lo que hay HOY ahí (se va a PERDER):"
+    if [ -n "$ADOPT" ]; then
+        echo "    lo que hay HOY ahí (NO se pierde: se mueve a /$ADOPT_SUB):"
+    else
+        echo "    lo que hay HOY ahí (se va a PERDER):"
+    fi
     blkid "$DEST" 2>/dev/null | sed 's/^/      /' || echo "      (sin filesystem reconocible)"
 fi
 if [ -n "$ESP" ] && [ -b "$ESP" ] && [ -x "$HAMMER" ]; then
@@ -1097,8 +1078,27 @@ if [ -n "$KEEP_SPEC" ]; then
         echo "    (no encontré ningún usuario ahí: se conserva igual, montado en $KEEP_PUNTO)"
     fi
 fi
+if [ -n "$ADOPT" ]; then
+    echo "    se MUEVE a /$ADOPT_SUB (sigue entero, no se copia ni se borra):"
+    echo "      $ADOPT_MUEVE"
+    echo "    se QUEDA donde está, con todos sus archivos: /home"
+    if [ -s "$ADOPT_USUARIOS" ]; then
+        echo "    usuarios que voy a crear en el sistema nuevo, con el uid que ya tienen sus archivos:"
+        while IFS=':' read -r _pa_n _pa_u _pa_g; do
+            [ -n "$_pa_n" ] && printf "      · %s (uid %s, gid %s) → /home/%s\n" "$_pa_n" "$_pa_u" "$_pa_g" "$_pa_n"
+        done < "$ADOPT_USUARIOS"
+        echo "    ⚠ los dotfiles del sistema viejo se conservan tal cual. Un .config de una app vieja"
+        echo "      puede hacer que la app nueva arranque rota, y el síntoma no menciona el archivo."
+    else
+        echo "    (no hay ningún /home con usuarios adentro: se adopta igual)"
+    fi
+fi
 if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
-    printf "    ¿formateo %s y dejo el resto del disco INTACTO? [escribí si] " "$DEST"
+    if [ -n "$ADOPT" ]; then
+        printf "    ¿muevo el sistema de %s a /%s e instalo encima, SIN formatear? [escribí si] " "$DEST" "$ADOPT_SUB"
+    else
+        printf "    ¿formateo %s y dejo el resto del disco INTACTO? [escribí si] " "$DEST"
+    fi
     read -r _r; [ "$_r" = "si" ] || { echo "    cancelado."; exit 1; }
 fi
 
@@ -1120,11 +1120,18 @@ ROOT_TREE="$STAGE/root"; IRD="$STAGE/initramfs.cpio.gz"
 # El /home conservado se declara en el ÁRBOL, antes de que el árbol entre al filesystem nuevo. Si
 # hay un choque de uid, `keep_aplicar` aborta acá — con el destino todavía sin tocar.
 if [ -n "$KEEP_SPEC" ]; then keep_aplicar "$ROOT_TREE"; fi
+# El `/home` adoptado NO se declara en `home.conf`: no es otra partición que haya que montar, está
+# dentro de la raíz. Lo único que hace falta es que sus dueños existan en el sistema nuevo.
+if [ -n "$ADOPT" ] && [ -s "$ADOPT_USUARIOS" ]; then usuarios_aplicar "$ROOT_TREE" "$ADOPT_USUARIOS"; fi
 
 # ── 2) Formatear SÓLO la partición destino, y poblarla SIN MONTARLA ────────────────────────────
 # `mke2fs -d` escribe el árbol dentro del filesystem nuevo sin `mount` y sin loop. Que no haya
 # montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
 # este script existe para no tener. Si no somos root, `unshare -r` da los dueños root sin sudo.
+if [ -n "$ADOPT" ]; then
+echo "==> adoptando $DEST: el sistema anterior se muda y el nuevo se instala encima (NO se formatea)"
+adopt_mudar "$ROOT_TREE"
+else
 echo "==> ext4 LABEL=hammer-root en $DEST (única escritura destructiva de todo el script)"
 # ⚠ La detección de root NO puede fallar en silencio. Medido dentro del initramfs el 2026-09-23: sin
 # el applet `id` el `$(id -u)` quedaba VACÍO, la comparación daba falso, el script se iba a la rama
@@ -1139,6 +1146,7 @@ else
         echo "takana-install-partition: no soy root (uid $_uid) y no hay 'unshare' para suplirlo" >&2; exit 1; }
     unshare -r "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
 fi
+fi
 # Y no se da por hecho: se mira. Regla 3 del CLAUDE.md — un ausente falla ruidosamente, un vacío
 # llega hasta el final diciendo que todo fue bien. Acá el vacío sería un destino sin filesystem.
 # ⚠ La verificación tiene que funcionar con LOS DOS `blkid`, y la primera versión no lo hacía:
@@ -1157,7 +1165,11 @@ _lbl_post=$(blkid "$DEST" 2>/dev/null | grep -o ' LABEL="[^"]*"' | head -1 | sed
 [ "$_lbl_post" = "hammer-root" ] || {
     echo "takana-install-partition: $DEST NO quedó con LABEL=hammer-root (blkid dice '${_lbl_post:-nada}')." >&2
     echo "                          El formateo no se completó: no sigo." >&2; exit 1; }
-echo "    ✓ $DEST formateada y poblada (LABEL=hammer-root verificada)"
+if [ -n "$ADOPT" ]; then
+    echo "    ✓ $DEST adoptada y poblada (LABEL=hammer-root verificada) — el sistema anterior en /$ADOPT_SUB"
+else
+    echo "    ✓ $DEST formateada y poblada (LABEL=hammer-root verificada)"
+fi
 
 # ── 3) La ESP: sólo nuestra carpeta de vendor, con mtools y SIN montar ─────────────────────────
 if [ -n "$ESP" ]; then