Una imagen se escribe con `dd` sobre un disco casi siempre más grande que ella. La del perfil
servidor son 7 G; la caja hcloud tiene 76,3 G. **Sobraban 69 G que nadie podía usar**, y la caja
arrancaba perfecta con el disco a un décimo — el fallo que no falla, otra vez.
Dos cambios que van juntos:
1. **El store pasa a ser la ÚLTIMA partición** (p1 bios, p2 `/`, p3 `/var/lib/hammer`, p4 `/store`).
Sólo la última puede extenderse sin mover datos, y el store es justamente la que crece con el uso.
Con él en el medio, la partición extensible era la de ESTADO, de 512 M, que no le sirve a nadie.
El cambio es seguro porque nada referencia números de partición: `root=PARTLABEL=hammer-root` y
el wrapper monta por etiqueta con `findfs`.
2. **El wrapper de `/sbin/init` la extiende en el primer arranque**, antes de montarla:
`sfdisk -N <n> ', +'` → `partx -u` → `e2fsck -pf` → `resize2fs`. Idempotente: si ya llega al
final, los dos últimos no hacen nada. Guardado tras `-x /sbin/sfdisk` para no romper un rootfs
que no lo traiga, y el aviso va a `/dev/kmsg`, que es donde se lee.
Probado en QEMU volcando la imagen de 7 G en un disco de 20 G: el arranque imprime
`init: store: /dev/sda4 extendido al final de /dev/sda` y `/store` queda en **13,3 G** con 12,6 G
libres, sin tocar nada a mano.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
El wrapper que `install-image.sh` escribe como `/sbin/init` monta las dos particiones dedicadas
antes de hacer `exec` del init real, y las nombraba `/dev/vda3` y `/dev/vda4`. Eso vale sólo donde
el disco es virtio-blk. **En una caja Hetzner Cloud la controladora es virtio-SCSI y el disco es
`sda`** (medido: el driver de `sda` es `sd` y `virtio_scsi` está cargado), así que los dos montajes
fallaban.
Y fallaban del peor modo posible: **el sistema arranca igual**. Sólo que `hammerd` —que la semilla
de arje lanza con `--store /store --journal /var/lib/hammer/journal`— queda sin store y sin diario,
con dos líneas de aviso perdidas en el arranque. Es exactamente la regla 3 de CLAUDE.md: un ausente
falla ruidosamente, un vacío llega hasta el final diciendo que todo fue bien.
Ahora el disco se DERIVA de dónde está montada la raíz (`/proc/mounts`), que es la única fuente que
no depende ni del nombre del dispositivo ni de udev. Probado en los dos sentidos con /proc/mounts
sintéticos: `/dev/sda2` → `/dev/sda3`, `/dev/vda2` → `/dev/vda3`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
Dos cosas encontradas armando la primera imagen del `perfil.servidor` (SDD 28 §1b).
**1. `[ -e "$ROOTFS/sbin/init" ]` rechazaba el rootfs CORRECTO y aceptaba el equivocado.** `-e`
sigue el symlink y lo resuelve contra el HOST. El `/sbin/init` del product-rootfs es un symlink
ABSOLUTO a `/usr/bin/arje-zero`, que en el host no existe ⇒ la guarda daba falso. Y al revés: un
rootfs cuyo init es `../bin/busybox` —symlink RELATIVO, que sí resuelve dentro del árbol— la pasaba
sin chistar. O sea que la única guarda que separaba «esto arranca takana» de «esto arranca otra
cosa» estaba exactamente al revés. Ahora acepta `-L`, y se probó en los dos sentidos: acepta el
symlink absoluto a arje-zero, sigue rechazando un directorio sin init.
Esto no es teórico: hidratar `perfil.servidor` sobre el product-rootfs **pisa `/sbin/init`** —
busybox entra en la clausura y su symlink gana por llegar después—. La imagen habría arrancado
perfecto con el init de busybox en vez de arje-zero, que es peor que no arrancar. Se restaura el
symlink al ensamblar y el cmdline lleva `init=/usr/bin/arje-zero` explícito, por las dos puntas.
**2. `STAGE` estaba cableado a `work/.install-stage`.** El staging HARDLINKEA desde `$ROOTFS`, y un
hardlink no cruza un montaje aunque sea el mismo disco. Con el layout normal de este repo —`store`
y `work/out` son bind-mounts de /dev/sdb, `work/` está en /dev/sdc— el rootfs vive del otro lado y
`cp -al` muere con EXDEV fichero por fichero. Ahora es env, documentado en la cabecera.
De paso: el fichero no tenía bit de ejecución (100644) mientras sus siete hermanos son 100755, así
que su propio ejemplo de uso `./scripts/install-image.sh` no funcionaba. Venía de antes; corregido.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
ADR 0016 los listaba entre los CONGELADOS; el usuario pidió descongelarlos al abrir el SDD 28. La
enmienda queda escrita en el propio ADR, que si no la etiqueta deja de describir el hecho.
`hammer-install.sh` → `takana-install.sh`, `hammer-live-install.sh` → `takana-live-install.sh`,
`hammer-banner.txt` → `takana-banner.txt`, `BRIEFING-hammer.md` → `BRIEFING-takana.md`.
**Lo que hacía caro esto no es el nombre del fichero.** El instalador se inyecta en el ISO como
`/usr/bin/hammer-install` y su éxito se detecta con un `grep` de `HAMMER-INSTALL-OK` desde TRES
scripts de prueba. Renombrar un solo lado los deja casando NADA — sin fallar —, que es literalmente
el modo en que `atribuir-fallos.py` quedó mudo cuando el renombre movió el target de `tracing`.
Se renombraron las dos puntas en el mismo commit (`/usr/bin/takana-install`,
`TAKANA-INSTALL-OK/FAIL`, `TAKANA_INSTALL_*`, `work/takana-install.img`, `/run/takana-install`),
se comprobó por `grep` que no quede ningún token viejo fuera del ADR, y —lo que decide— se CORRIÓ
`install-tui-test.sh`: 4/4 casos verdes.
Las tres `TAKANA_INSTALL_*` caen al nombre viejo (`${TAKANA_INSTALL_X:-${HAMMER_INSTALL_X:-}}`):
el llamador puede ser un ISO anterior al renombre. Misma convención que `TAKANA_ROOT_PW` unas
líneas más arriba en ese mismo script.
NO se tocó `/usr/sbin/hammer-recover` ni su hook de arranque —renombrarlo rompe máquinas YA
INSTALADAS, no el repo—: sobrevive intacto dentro del script renombrado, verificado por conteo
antes y después (8 ocurrencias). Tampoco `hammerd`, `hammer-edit` (su `name` está en la ruta del
store), `/var/lib/hammer`, `HAMMER_LIVE` ni los siete literales de hash.
De paso: `scripts/.hammer-banner.txt.kate-swp` era un swap de editor commiteado por error. Fuera.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.
El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.
Y el hallazgo caro: casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.
Además 14 rutas de módulo en docs, que el barrido anterior no tocó
porque no es frontera de palabra.
`hammer install <device>`: vuelca el product-rootfs lean sobre un disco
destino (block device real /dev/sdX, o un fichero pre-dimensionado para
pruebas) con el mismo layout GPT + GRUB BIOS de E1 pero IN-PLACE. Tras
instalar, el disco arranca solo (SeaBIOS → GRUB → arje-zero → hammerd+
getty+sshd). Contraparte "a disco real" de product-image.sh (imagen portátil).
- install-image.sh generalizado: si IMG es block device (o PREALLOC=1 sobre
fichero pre-dimensionado) NO trunca ni borra el nodo — valida capacidad e
instala in-place. sfdisk/mke2fs -d/dd-splice/GRUB son idénticos (dd a un
offset de device = a un offset de fichero). El camino rootless de E1
(unshare -r + dd conv=sparse,notrunc + patch GRUB) sirve igual al device.
- hammer-install.sh: resuelve el product-rootfs, provisiona authorized_keys
(AUTHKEYS=<file> o TESTKEY=1 efímera), guardas de seguridad (FORCE=1 para
un block device, rechazo si está montado), y delega a install-image.sh.
Con BOOT=1+TESTKEY=1 arranca el target y valida por SSH.
Validado: instalación IN-PLACE sobre un fichero pre-dimensionado de 2200M
(ejercita el camino exacto del device, sin hardware real) → el disco
instalado auto-bootea (sin -kernel) y sirve SSH: ls = "uutils coreutils
0.9.0", /dev/vda3→/store y /dev/vda4→/var/lib/hammer montadas. El device
real sólo añade el guard FORCE + autodetección [-b].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primer entregable de release engineering (SDD 13 nuevo): scripts/install-image.sh
produce una imagen GPT que se BOOTEA SOLA en QEMU (`-drive file=img`, sin
-kernel ni firmware extra) — SeaBIOS → MBR → GRUB → kernel del propio disco →
arje-zero PID 1. Capaz de arrancar en hardware real.
Layout: vda1=BIOS boot (ef02), vda2=/ (con /boot/bzImage + /boot/grub),
vda3=/store, vda4=/var/lib/hammer. Reusa el particionado sin-root de B3
(sfdisk + mke2fs -d bajo unshare -r + dd conv=sparse) y el wrapper /sbin/init.
GRUB instalado SIN root ni loop: grub-bios-setup sondea el disco físico del
host (/dev/nvme…, 660 root:disk) para adivinar el root device del dir -d y falla
sin privilegios. Lo reemplaza un patch binario determinista sobre la ABI estable
de GRUB i386-pc: grub-mkimage arma core.img; un script Python escribe core.img
en la BIOS boot partition y parchea los punteros (boot.img off 0x5c kernel_sector
→ LBA de core.img; core.img off 0x1F4 blocklist.start → resto de core.img),
con asserts de los valores por defecto (1 y 2) para fallar ruidoso si la ABI
cambia. boot.img va al MBR sin pisar la GPT protective (sólo 440 B).
Verificado in-VM (KVM, sin -kernel): SeaBIOS → GRUB 2.14 → Linux 6.16.12 →
vda1..4 detectadas, vda2 root + vda3/vda4 montadas por el wrapper (0 errores) →
arje-zero PID 1 + hammerd (store=/store journal=/var/lib/hammer/journal).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>