Files
Sergio ffa203910f CLAUDE.md §3.bis: el scratch de build caía en el overlay desechable, con 223 G al lado sin usar
`work_root` sale de `dirname(store)/work`, y con el store en `/store` el padre es `/`: todo el
scratch iba al overlay de 69 G de la jaula, que además es capa DESECHABLE — la caché de fuentes
vivía en algo que se tira. Mientras tanto `/dev/sdc`, 255 G, estaba al 9 %.

De los 6,8 G de `work/sources`, 5,4 G eran DOS COPIAS del mismo commit de tawasuyu: los árboles se
nombran por receta, no por commit, así que cada receta del monorepo cuesta otros 2,7 G. Queda
anotado que es deliberado (aislamiento del ADR 0012) y que no se deduplica de paso.

Arreglado con enlaces a `/work/sergio/work` en vez de con `TAKANA_WORK`: el override existe y
funciona, pero una docena de scripts llama a `takana build` y un export que hay que recordar se
olvida. Comprobado con un build real sin ninguna variable — rc=0, el árbol cayó en sdc y el hash
salió idéntico al de la corrida anterior. Overlay de 6,9 G a 14 G libres.

Anotado también que el worker NO tiene este problema (un solo disco de 196 G), que los enlaces se
van si la jaula se rehace, y que la premisa del `seal` (rename atómico ⇒ mismo filesystem) ya
estaba rota en el hub antes de esto, porque `/work` y `/store` son discos distintos.
2026-09-21 15:00:59 +00:00

278 lines
16 KiB
Markdown

# Reglas del repo compartido
Este repo lo trabajan **varios agentes a la vez** (hoy: frente granja/store y frente kernel). Lo de
aquí abajo no son preferencias de estilo: son las dos formas conocidas de que un agente destruya el
trabajo de otro sin enterarse. El resto del diseño está en `docs/`.
## 1. Todo `takana build` va envuelto en `flock`
```sh
flock work/.farm-build.lock ./target/release/takana --store ./store build <receta>
```
Para una tanda, tomar el lock una sola vez y no por receta:
```sh
flock work/.farm-build.lock bash -c 'for r in ...; do ./target/release/takana --store ./store build "$r"; done'
```
**Por qué.** `takana build` comparte `work/sources/<dep>-<sha>` entre todas las recetas. Dos builds
concurrentes que compartan una dependencia se pisan el árbol de fuentes: uno hace fetch y lo borra
mientras el otro lo usa, y **el árbol queda roto para siempre** — reintentar no lo arregla. Medido a
escala: al invalidar libdrm, ~93 de 205 recetas KDE murieron con `/src/.zwrap/cc is not a full path
to an existing compiler tool`, que es el wrapper de zig que la receta deja EN EL ÁRBOL, barrido por
el fetch concurrente de otra receta. Es el **ADR 0012**, sin decidir; hasta que se decida
(lock por árbol / árbol privado / caché inmutable + copia), serializar es la única mitigación
correcta. Por eso el worker corre con `JOBS=1`.
**Y usar `flock -o`, no `flock` a secas — esto es medido, no teórico (2026-09-08).** El lock lo
sostiene la *descripción de fichero abierta*, y los hijos la HEREDAN: si un nieto se fuga, el lock
queda tomado para siempre aunque el `flock` haya terminado hace rato. Pasó: dos `firefox` colgados de
una caza de bugs sobrevivieron al `kill` del `bwrap` que los envolvía y **dejaron a la granja sin
poder compilar durante hora y media, en silencio** — nada falla, simplemente el siguiente `flock`
espera para siempre. Comprobado en los dos sentidos:
```sh
flock lock sh -c 'sleep 25 & exit 0' # ⇒ el nieto RETIENE el lock tras salir flock
flock -o lock sh -c 'sleep 25 & exit 0' # ⇒ lock LIBRE; -o cierra el fd antes de ejecutar
```
Si el lock parece tomado y no hay ningún build, `fuser -v work/.farm-build.lock` dice quién lo tiene;
casi siempre es un proceso fugado que nadie asocia con el lock. **Los scripts de `scripts/farm/` usan
el estilo `exec 9>` + `flock 9`, que es vulnerable igual** (haría falta `9>&-` en cada hijo): deuda
conocida, no barrida.
`scripts/farm/farm-worker-loop.sh` y `campana-deuda.sh` ya toman **ese mismo fichero de lock**, así
que usarlo nos serializa con la granja además de entre nosotros. **No está dentro de `takana build`
a propósito**: esos scripts lo toman por fuera y takana se bloquearía contra ellos.
## 1 bis. El worker es el LXC PRESTADO, y es GRATIS: no se levanta nada en Hetzner
**`dev.gioser.net` (154.197.1.13, hostname interno `PruebasIA`) es dónde se compila.** Es un LXC
prestado en el Proxmox de gioser: 6 cores, 16 G RAM + 8 G swap, 196 G de disco, **coste €0**.
Se usa exactamente por eso — **para no gastar Hetzner**.
**No levantar cajas hcloud** (`farm-up.sh`, `farm-run.sh`) salvo que el usuario lo pida por su
nombre. El modelo efímero de pago sigue documentado y funcionando, pero está SUPERADO como sitio de
trabajo desde el 2026-09-05. Un `farm-up` por reflejo cuesta dinero real y no hace falta.
```sh
ssh -i ~/.ssh/id_ed25519 root@dev.gioser.net # la que entra HOY; NO hay entrada en ssh/config
```
⚠ **Los scripts piden `~/.ssh/github5`, ese fichero NO EXISTE en el hub, y aun así todo funciona —
por accidente** (medido 2026-09-18). Una docena larga de scripts trae
`SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"`: `estado-granja.sh`, `cosecha-cron.sh`, `harvest-go.sh`,
`farm-up.sh`, `farm-down.sh`, `harkaq-vol.sh`… El único fichero en `~/.ssh` es `id_ed25519`, y sin
embargo `estado-granja.sh` con su default entra y reporta. **`ssh -i <fichero inexistente>` no
falla**: avisa por stderr y cae a las identidades por defecto, que es donde está la buena:
```
Warning: Identity file /home/sergio/.ssh/github5 not accessible: No such file or directory.
debug1: identity file /home/sergio/.ssh/id_ed25519 type 2
debug1: Offering public key: /home/sergio/.ssh/id_ed25519 ED25519 SHA256:kAqayFW4hSqq…
debug1: Server accepts key: … · Authenticated to dev.gioser.net using "publickey".
```
O sea que **el `-i` de todos esos scripts es hoy un no-op** y quien manda es el fallback. Se cae el
día que alguien ponga un `~/.ssh/github5` que no entre (el `-i` explícito GANA sobre el default), o
que aparezca un `IdentityFile`/`IdentitiesOnly` en `ssh/config`. Y el error dirá `Permission
denied`, sin mencionar `SSH_KEY` ni `github5`. Mismo patrón que el reaper salvándose por la
subcadena `gioser`: *una protección que funciona por accidente se ve igual que una que funciona por
diseño*. Barrer el default de los scripts es su propia unidad de trabajo — no tocarlos de paso.
**Se enchufa por `scripts/farm/.fleet`** (`<nombre> <ip>` por línea), que **está en `.gitignore`**:
o sea que un hub recién clonado nace con la flota VACÍA y la granja queda desconectada **sin que
nada falle** — la cosecha dice «flota vacía» cada 30 min y los artefactos del worker no vuelven.
Pasó, y se descubrió por casualidad. Si `estado-granja.sh` dice «no hay worker vivo», mirá `.fleet`
antes de creerle.
echo "dev.gioser.net 154.197.1.13" > scripts/farm/.fleet
**Y hasta el 2026-09-08 eso sobrevivía por una CASUALIDAD DE NOMBRE.** El reaper de
`cosecha-cron.sh` saca de `.fleet` a todo lo que no esté en hcloud; el LXC se salvaba sólo porque su
nombre contiene la subcadena `gioser` y caía en una lista negra que existe para proteger al HUB de
Hetzner y no tiene nada que ver con él. Medido: el mismo host llamado `pruebasia-lxc` era expulsado
en el primer ciclo. **Arreglado** — ahora el reaper pregunta por SSH si el host responde en vez de
mirarle el nombre — pero la lección queda: *una protección que funciona por accidente se ve igual
que una que funciona por diseño*.
## 2. Nunca `git add -A` — y acotar el COMMIT, no sólo el `add`
Sólo rutas explícitas. Un `add -A` arrastra al commit los ficheros a medias de otro agente. Commits
granulares, en español, directo sobre `main`, y `git push` tras cada unidad de trabajo (el `origin`
empuja a gitea **y** al espejo privado de GitHub; ver `scripts/espejo-setup.sh`).
**Desde la jaula qorpa ese `push` doble SÓLO llega a gitea: el espejo de GitHub falla siempre**
(medido 2026-09-18). El `PATH` del lab pone primero el git del CORPUS —`/work/sergio/bin/git`,
2.54.0, musl— y ese build **no trae los helpers `remote-http`/`remote-https`**. El push termina así,
con gitea ya actualizado y GitHub intacto:
```
To ssh://git.gioser.net:2345/sergio/takana.git
317ce48c..fd065720 main -> main ← gitea OK
git: 'remote-https' is not a git command. ← y el espejo se queda atrás
fatal: remote helper 'https' aborted session
```
Sale con estado ≠ 0, pero **la primera línea dice «main -> main» y se lee como éxito**. El git de la
imagen sí los tiene (`/usr/bin/git`, 2.55.0), así que el espejo se empuja con él:
```sh
/usr/bin/git push https://github.com/sergiovelasquezzeballos/takana.git main
```
Corolario: el espejo deriva solo mientras se trabaje desde adentro — el 2026-09-18 estaba una hora
atrás y nadie se había enterado. Comprobar con `git log --oneline -1` de los dos lados, no con la
salida del push.
**«Rutas explícitas» en el `add` NO ALCANZA, y esto es medido, no teórico** (2026-09-06): el
commit `ff0b556` se llevó dentro un rename y un borrado de otro agente **habiendo usado
`git add recipes/firefox.toml` y `git commit` sin `-a`**. La causa es que **el índice es estado
COMPARTIDO**: `git commit` commitea el índice ENTERO, no lo que vos acabás de añadir, así que
cualquier cosa que el otro agente dejó en `git add` viaja en tu commit. Comprobado en un repo de
juguete, en los dos sentidos:
```sh
git add mio.txt && git commit -m … # ⇒ arrastra ajeno.txt (estaba staged por el otro)
git commit -m … -- mio.txt # ⇒ SÓLO mio.txt; lo del otro queda staged e intacto
```
**Entonces: `git commit -m "…" -- <rutas>`.** El `--` acota el commit por pathspec y es lo único que
aísla de verdad. Vale también para `git commit -F -`. Corolario: no dar por bueno el alcance sin
mirarlo — `git show --stat` sobre el commit recién hecho cuesta un segundo y es la única forma de
enterarse el mismo día en vez de por el otro agente.
**Pero el `--` NO protege la ruta que vos nombrás, y la intuición es la contraria** (medido
2026-09-15, repo de juguete): con un fichero en estado `MM` —stageado *y* modificado por otro—,
`git commit -- f.txt` commitea **el ÁRBOL, no lo stageado**, y además deja el índice en la versión
del árbol, o sea que **lo que el otro agente tenía stageado en ESA ruta desaparece**.
```sh
# f.txt: v2 en el índice (del otro), v3 en el árbol (mío)
git commit -m … -- f.txt # ⇒ commitea v3 · el índice queda en v3 · v2 se perdió
```
El pathspec aísla de lo que el otro dejó en **otras** rutas — que es lo del párrafo de arriba y
sigue siendo cierto. Corolario: **un fichero que aparece `MM` y que vos no tocaste no se commitea
ni con pathspec — se avisa.** Pasó con el `Cargo.lock` de tawasuyu (SDD 26 §7.quinquies), donde el
árbol del otro agente ya traía la reparación que hacía falta.
## 2 ter. `git pull --rebase` puede DESCARTAR tu commit sin decir nada
**Medido acá el 2026-09-16, y cuesta una hora de trabajo si no sabés mirar el reflog.**
Secuencia real: commit propio en `main` (10 ficheros nuevos) → `git push`**gitea lo rechaza**
porque otro agente empujó → `git pull --rebase` → el push vuelve a fallar y **el commit ya no está**,
ni en el log ni en el ÁRBOL: los diez ficheros habían desaparecido del disco.
El reflog lo explica y es lo único que lo explica:
```
11a9ead6 HEAD@{0}: pull --rebase (finish): returning to refs/heads/main
11a9ead6 HEAD@{1}: pull --rebase (start): checkout 11a9ead6
f3d7be6c HEAD@{2}: commit: los 9 daemons propios entran al catálogo… ← el mío
```
**Entre `start` y `finish` no hay ni un `pick`**: el rebase no encontró NADA que reaplicar, y con
`-q` eso se ve igual que un pull limpio.
**PASÓ DOS VECES EL MISMO DÍA** (2026-09-16 y 2026-09-17), con el mismo reflog: `commit` propio,
`(start): checkout <otro>`, `(finish): returning to refs/heads/main` — y ni un `pick` en el medio.
**Y el comando NO es el culpable: se reprodujo en un repo de juguete y ahí funciona bien.** Dos
clones, el otro empuja, yo commiteo local, `git pull --rebase -q origin main` ⇒ **mi commit se
reaplica y mi fichero sigue ahí**. O sea que lo que falla no es `pull --rebase`: es que este árbol
lo comparten varios agentes y **alguien mueve `refs/heads/main` mientras el rebase está en vuelo**.
La receta práctica no cambia; el diagnóstico sí, y evita ir a buscar el bug al lugar equivocado.
**La CAUSA exacta no está determinada, y conviene decirlo así en vez de inventarla.** Lo medido es el
reflog de arriba y que los diez ficheros desaparecieron también del ÁRBOL, que es lo que un
`reset --hard` o un `checkout` concurrente de otro agente sí explicaría. Lo que NO es evidencia —y
se escribió como si lo fuera— es que los commits que trajo el `pull` cuelguen del commit anterior al
mío: eso es lo normal cuando el otro lado empujó desde una máquina que había hecho `fetch` antes, y
no dice nada de lo que pasó acá. **Que el árbol perdiera los ficheros es el dato fuerte; el resto,
hipótesis.**
**Qué hacer:**
```sh
git reflog -15 # tu commit está acá, con su sha
git cherry-pick <sha> # vuelve entero, ficheros incluidos
```
Y la comprobación que evita el susto: **`git log --oneline -1` después de cada `pull --rebase`.** Si
tu commit no es el primero, no se rebasó — se perdió de la rama, y el reflog lo tiene.
**El espejo de GitHub queda DIVERGENTE en ese escenario**, y es normal: el `push` doble puede
triunfar en un remoto y fallar en el otro (pasó: gitea rechazó, GitHub aceptó). Tras recuperar el
commit, el espejo tiene un sha huérfano con el mismo contenido. Se repara empujando **sólo la URL de
GitHub** con `--force-with-lease=main:<sha-huérfano>` — con lease, que verifica que el remoto sigue
donde creés antes de pisarlo. Comprobar antes que el contenido no se pierde:
`git diff --stat <huérfano> HEAD`.
## 3. Antes de dar un artefacto por presente, mirá que tenga contenido
Un directorio **vacío** en el store no es un artefacto: es un nombre. `Store::has` ya lo rechaza y
`respaldo-storagebox.sh --listar` los separa a `work/respaldo-vacios.txt`, pero la regla general
sigue valiendo para cualquier código nuevo — **un ausente falla ruidosamente; un vacío llega hasta
el final diciendo que todo fue bien**.
## 3 bis. En el HUB, el scratch de build no cae donde parece — y el disco bueno estaba sin usar
**Medido el 2026-09-21.** `work_root` no es una ruta elegida: sale de `dirname(store)/work`
(`crates/takana-build/src/config.rs`). Con el store en `/store`, el padre es `/` ⇒ **todo el scratch
de build cae en el overlay de la jaula**. En esta caja eso son 69 G compartidos, y el disco de
verdad —`/dev/sdc`, 255 G— estaba **al 9 %, sin usar**.
Dos cosas lo empeoran, y ninguna se ve desde `df`:
- **El overlay es capa DESECHABLE.** `/work` no tiene montaje propio: si la jaula se rehace, los
árboles de fuentes se van con ella. La caché de build vivía en algo que se tira.
- **Los árboles se nombran por RECETA, no por commit.** `boveda` y `shuma-pregunta` salen del mismo
`b80f7567…` de tawasuyu y ocupaban **2,7 G cada una** — 5,4 G de los 6,8 G totales eran el mismo
contenido dos veces. Cada receta nueva de ese monorepo cuesta otros 2,7 G. Es deliberado (árbol
por receta = el aislamiento que evita el destrozo del ADR 0012); **no deduplicar de paso.**
**Arreglado con enlaces, no con una variable de entorno**, porque una docena de scripts llama a
`takana build` y un `export` que hay que recordar se olvida:
```sh
ln -sfn /work/sergio/work/sources /work/sources
ln -sfn /work/sergio/work/out /work/out
```
Comprobado con un build real contra un store tirable y **sin ninguna variable**: `rc=0`, el árbol
cayó en `sdc` y el hash salió idéntico al de la corrida anterior. El overlay pasó de **6,9 G a 14 G
libres**. Existe `TAKANA_WORK` como override (junto a `TAKANA_LAB`/`ROOTFS`/`ZIG`/`CACHE`) y
funciona, pero el enlace no se puede olvidar.
**Si la jaula se rehace, los enlaces se van y el scratch vuelve al overlay en silencio** — nada
falla, sólo se llena otra vez. Rehacerlos es lo de arriba; el contenido sigue en `/work/sergio/work`.
**El WORKER no tiene este problema:** un único disco de 196 G con todo encima (`/`, `/opt/takana`,
su store), así que ahí `work_root` y el store comparten filesystem y el `seal` —que es un `rename`,
atómico sólo dentro del mismo fs— cumple su premisa. **En el hub esa premisa ya estaba rota** antes
de este cambio: `/work` en `sda4` y `/store` en `sdb` son discos distintos. Mover el store al disco
grande lo restauraría, pero es su propia unidad de trabajo — toca la granja, el hub y los scripts.
## 4. La superficie de la CLI va en INGLÉS; los mensajes, en castellano
Subcomandos, flags y nombres de opciones: **inglés** (`build`, `hash`, `hydrate`, `mirror push`,
`kernel closure`, `qorpa pull`). Lo que el usuario LEE —ayuda, errores, logs— va en castellano, y
los nombres propios del proyecto son quechua (`takana`, `qorpa`, `harkaq`, `yupana`, `arje`).
**La única excepción, y es cerrada:** el ADR 0016 admite **alias de marca** en castellano sobre un
verbo canónico —hoy uno solo, `forja``build`— para que la invocación de la hoja de marca
funcione. El alias **no reemplaza nada**: el canónico sigue siendo el inglés y es el que usan los
scripts, el cron y los runbooks. Un alias es superficie de marca; el contrato es el canónico.
Agregar otro alias es una decisión de ADR, no algo que se hace de paso.
**Por qué.** Un verbo de CLI es contrato: entra en scripts, cron y runbooks, y renombrarlo después
rompe llamadores que nadie recuerda. Media superficie en cada idioma obliga a adivinar en cada
comando nuevo, y ya pasó: el ADR 0015 nació con `traer/crear/correr` y hubo que corregirlo.
**Deuda conocida, NO barrida todavía:** varios scripts de `scripts/` exponen flags en castellano
(`--crear`, `--traer`, `--listar`, `--sha`). El barrido es su propia unidad de trabajo — tocarlos de
paso rompe cron y la granja. Código NUEVO nace en inglés desde hoy.