gioser

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 Definiciones

⎇ 32627251b1adcc995958bd60e72226040bff6125 CLAUDE.md Markdown · 304 líneas · 19.1 KB Historia crudo

docs(claude.md): y el último párrafo de 3bis decía que la premisa sigue rota 6c1c30b28 · Sergio ·

1# Reglas del repo compartido
2
3Este repo lo trabajan **varios agentes a la vez** (hoy: frente granja/store y frente kernel). Lo de
4aquí abajo no son preferencias de estilo: son las dos formas conocidas de que un agente destruya el
5trabajo de otro sin enterarse. El resto del diseño está en `docs/`.
6
7## 1. Todo `takana build` va envuelto en `flock`
8
9```sh
10flock work/.farm-build.lock ./target/release/takana --store ./store build <receta>
11```
12
13Para una tanda, tomar el lock una sola vez y no por receta:
14
15```sh
16flock work/.farm-build.lock bash -c 'for r in ...; do ./target/release/takana --store ./store build "$r"; done'
17```
18
19**Por qué.** `takana build` comparte `work/sources/<dep>-<sha>` entre todas las recetas. Dos builds
20concurrentes que compartan una dependencia se pisan el árbol de fuentes: uno hace fetch y lo borra
21mientras el otro lo usa, y **el árbol queda roto para siempre** — reintentar no lo arregla. Medido a
22escala: al invalidar libdrm, ~93 de 205 recetas KDE murieron con `/src/.zwrap/cc is not a full path
23to an existing compiler tool`, que es el wrapper de zig que la receta deja EN EL ÁRBOL, barrido por
24el fetch concurrente de otra receta. Es el **ADR 0012**, sin decidir; hasta que se decida
25(lock por árbol / árbol privado / caché inmutable + copia), serializar es la única mitigación
26correcta. Por eso el worker corre con `JOBS=1`.
27
28✅ **ACTUALIZADO 2026-09-22 — el ADR 0012 está DECIDIDO e IMPLEMENTADO, y la carrera ya no existe.**
29`takana build` toma un lock exclusivo POR RECETA en el `fetch` y lo sostiene hasta el `seal`
30(`fetch::GuardaArbol`), con re-chequeo de la tienda al obtenerlo: el que espera sale por cache hit.
31No serializa la granja —dos recetas distintas no comparten su árbol propio, y un consumidor recibe el
32ARTEFACTO de su dep, no su árbol— ni puede deadlockear, porque las deps se construyen ANTES de tomar
33el lock propio (los locks no se anidan). Verificado con dos builds concurrentes y con un control que
34reproduce la firma del apagón (`.zwrap/cc AUSENTE, 0/400 fuentes` sin lock; OK con él).
35
36⇒ **El `flock` de abajo SIGUE HACIENDO FALTA, pero por otra razón**: serializa contra
37`farm-worker-loop.sh` y `campana-deuda.sh`, que es control de RECURSOS (RAM, disco), no de
38corrupción. Y `JOBS=1` ya no es una necesidad de corrección: subirlo es una decisión de recursos —
39el worker son 6 cores / 16 G y esta clase de máquina ya provocó un `global_oom`.
40
41⚠ **Y usar `flock -o`, no `flock` a secas — esto es medido, no teórico (2026-09-08).** El lock lo
42sostiene la *descripción de fichero abierta*, y los hijos la HEREDAN: si un nieto se fuga, el lock
43queda tomado para siempre aunque el `flock` haya terminado hace rato. Pasó: dos `firefox` colgados de
44una caza de bugs sobrevivieron al `kill` del `bwrap` que los envolvía y **dejaron a la granja sin
45poder compilar durante hora y media, en silencio** — nada falla, simplemente el siguiente `flock`
46espera para siempre. Comprobado en los dos sentidos:
47
48```sh
49flock    lock sh -c 'sleep 25 & exit 0'   # ⇒ el nieto RETIENE el lock tras salir flock
50flock -o lock sh -c 'sleep 25 & exit 0'   # ⇒ lock LIBRE; -o cierra el fd antes de ejecutar
51```
52
53Si el lock parece tomado y no hay ningún build, `fuser -v work/.farm-build.lock` dice quién lo tiene;
54casi siempre es un proceso fugado que nadie asocia con el lock. **Los scripts de `scripts/farm/` usan
55el estilo `exec 9>` + `flock 9`, que es vulnerable igual** (haría falta `9>&-` en cada hijo): deuda
56conocida, no barrida.
57
58`scripts/farm/farm-worker-loop.sh` y `campana-deuda.sh` ya toman **ese mismo fichero de lock**, así
59que usarlo nos serializa con la granja además de entre nosotros. **No está dentro de `takana build`
60a propósito**: esos scripts lo toman por fuera y takana se bloquearía contra ellos.
61
62## 1 bis. El worker es el LXC PRESTADO, y es GRATIS: no se levanta nada en Hetzner
63
64**`dev.gioser.net` (154.197.1.13, hostname interno `PruebasIA`) es dónde se compila.** Es un LXC
65prestado en el Proxmox de gioser: 6 cores, 16 G RAM + 8 G swap, 196 G de disco, **coste €0**.
66Se usa exactamente por eso — **para no gastar Hetzner**.
67
68⇒ **No levantar cajas hcloud** (`farm-up.sh`, `farm-run.sh`) salvo que el usuario lo pida por su
69nombre. El modelo efímero de pago sigue documentado y funcionando, pero está SUPERADO como sitio de
70trabajo desde el 2026-09-05. Un `farm-up` por reflejo cuesta dinero real y no hace falta.
71
72```sh
73ssh -i ~/.ssh/id_ed25519 root@dev.gioser.net  # la que entra HOY; NO hay entrada en ssh/config
74```
75
76⚠ **Los scripts piden `~/.ssh/github5`, ese fichero NO EXISTE en el hub, y aun así todo funciona —
77por accidente** (medido 2026-09-18). Una docena larga de scripts trae
78`SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"`: `estado-granja.sh`, `cosecha-cron.sh`, `harvest-go.sh`,
79`farm-up.sh`, `farm-down.sh`, `harkaq-vol.sh`… El único fichero en `~/.ssh` es `id_ed25519`, y sin
80embargo `estado-granja.sh` con su default entra y reporta. **`ssh -i <fichero inexistente>` no
81falla**: avisa por stderr y cae a las identidades por defecto, que es donde está la buena:
82
83```
84Warning: Identity file /home/sergio/.ssh/github5 not accessible: No such file or directory.
85debug1: identity file /home/sergio/.ssh/id_ed25519 type 2
86debug1: Offering public key: /home/sergio/.ssh/id_ed25519 ED25519 SHA256:kAqayFW4hSqq…
87debug1: Server accepts key: … · Authenticated to dev.gioser.net using "publickey".
88```
89
90O sea que **el `-i` de todos esos scripts es hoy un no-op** y quien manda es el fallback. Se cae el
91día que alguien ponga un `~/.ssh/github5` que no entre (el `-i` explícito GANA sobre el default), o
92que aparezca un `IdentityFile`/`IdentitiesOnly` en `ssh/config`. Y el error dirá `Permission
93denied`, sin mencionar `SSH_KEY` ni `github5`. Mismo patrón que el reaper salvándose por la
94subcadena `gioser`: *una protección que funciona por accidente se ve igual que una que funciona por
95diseño*. Barrer el default de los scripts es su propia unidad de trabajo — no tocarlos de paso.
96
97**Se enchufa por `scripts/farm/.fleet`** (`<nombre> <ip>` por línea), que **está en `.gitignore`**:
98o sea que un hub recién clonado nace con la flota VACÍA y la granja queda desconectada **sin que
99nada falle** — la cosecha dice «flota vacía» cada 30 min y los artefactos del worker no vuelven.
100Pasó, y se descubrió por casualidad. Si `estado-granja.sh` dice «no hay worker vivo», mirá `.fleet`
101antes de creerle.
102
103    echo "dev.gioser.net 154.197.1.13" > scripts/farm/.fleet
104
105⚠ **Y hasta el 2026-09-08 eso sobrevivía por una CASUALIDAD DE NOMBRE.** El reaper de
106`cosecha-cron.sh` saca de `.fleet` a todo lo que no esté en hcloud; el LXC se salvaba sólo porque su
107nombre contiene la subcadena `gioser` y caía en una lista negra que existe para proteger al HUB de
108Hetzner y no tiene nada que ver con él. Medido: el mismo host llamado `pruebasia-lxc` era expulsado
109en el primer ciclo. **Arreglado** — ahora el reaper pregunta por SSH si el host responde en vez de
110mirarle el nombre — pero la lección queda: *una protección que funciona por accidente se ve igual
111que una que funciona por diseño*.
112
113## 2. Nunca `git add -A` — y acotar el COMMIT, no sólo el `add`
114
115Sólo rutas explícitas. Un `add -A` arrastra al commit los ficheros a medias de otro agente. Commits
116granulares, en español, directo sobre `main`, y `git push` tras cada unidad de trabajo (el `origin`
117empuja a gitea **y** al espejo privado de GitHub; ver `scripts/espejo-setup.sh`).
118
119✅ **ARREGLADO el 2026-09-22 — y la causa NO era la que decía acá.** Lo de abajo se deja porque el
120síntoma es el mismo y alguien lo va a volver a buscar, pero el diagnóstico estaba mal: el git del
121corpus **SÍ trae** `git-remote-https` (está en su artefacto, medido: `ls …-git/usr/libexec/git-core |
122grep -c remote-http` ⇒ **2**). Lo que pasaba es que git busca sus treinta ayudantes en un directorio
123**horneado al build** —`git --exec-path` ⇒ `/usr/libexec/git-core`, la ruta FHS—, y corrido desde el
124store mira el libexec de la IMAGEN de la jaula, que tiene **0**. Faltaba `GIT_EXEC_PATH`, nada más.
125Puesto en el generador (`scripts/servidor/jaula-herramientas.sh`) y en el envoltorio
126`/work/sergio/bin/git`; con eso el mismo binario clona por https y empuja al espejo. Si el síntoma
127vuelve, mirá `git --exec-path` ANTES de creer que faltan los helpers.
128
129⚠ **El síntoma histórico, para que se reconozca** (medido 2026-09-18): el `PATH` del lab pone primero
130el git del CORPUS —`/work/sergio/bin/git`, 2.54.0, musl— y su envoltorio no le decía dónde están sus
131ayudantes. El push terminaba así, con gitea ya actualizado y GitHub intacto:
132
133```
134To ssh://git.gioser.net:2345/sergio/takana.git
135   317ce48c..fd065720  main -> main            ← gitea OK
136git: 'remote-https' is not a git command.      ← y el espejo se queda atrás
137fatal: remote helper 'https' aborted session
138```
139
140Sale con estado ≠ 0, pero **la primera línea dice «main -> main» y se lee como éxito**. El git de la
141imagen sí los tiene (`/usr/bin/git`, 2.55.0), así que el espejo se empuja con él:
142
143```sh
144/usr/bin/git push https://github.com/sergiovelasquezzeballos/takana.git main
145```
146
147Corolario: el espejo deriva solo mientras se trabaje desde adentro — el 2026-09-18 estaba una hora
148atrás y nadie se había enterado. Comprobar con `git log --oneline -1` de los dos lados, no con la
149salida del push.
150
151⚠ **«Rutas explícitas» en el `add` NO ALCANZA, y esto es medido, no teórico** (2026-09-06): el
152commit `ff0b556` se llevó dentro un rename y un borrado de otro agente **habiendo usado
153`git add recipes/firefox.toml` y `git commit` sin `-a`**. La causa es que **el índice es estado
154COMPARTIDO**: `git commit` commitea el índice ENTERO, no lo que vos acabás de añadir, así que
155cualquier cosa que el otro agente dejó en `git add` viaja en tu commit. Comprobado en un repo de
156juguete, en los dos sentidos:
157
158```sh
159git add mio.txt && git commit -m …          # ⇒ arrastra ajeno.txt (estaba staged por el otro)
160git commit -m … -- mio.txt                  # ⇒ SÓLO mio.txt; lo del otro queda staged e intacto
161```
162
163**Entonces: `git commit -m "…" -- <rutas>`.** El `--` acota el commit por pathspec y es lo único que
164aísla de verdad. Vale también para `git commit -F -`. Corolario: no dar por bueno el alcance sin
165mirarlo — `git show --stat` sobre el commit recién hecho cuesta un segundo y es la única forma de
166enterarse el mismo día en vez de por el otro agente.
167
168⚠ **Pero el `--` NO protege la ruta que vos nombrás, y la intuición es la contraria** (medido
1692026-09-15, repo de juguete): con un fichero en estado `MM` —stageado *y* modificado por otro—,
170`git commit -- f.txt` commitea **el ÁRBOL, no lo stageado**, y además deja el índice en la versión
171del árbol, o sea que **lo que el otro agente tenía stageado en ESA ruta desaparece**.
172
173```sh
174# f.txt: v2 en el índice (del otro), v3 en el árbol (mío)
175git commit -m … -- f.txt     # ⇒ commitea v3 · el índice queda en v3 · v2 se perdió
176```
177
178El pathspec aísla de lo que el otro dejó en **otras** rutas — que es lo del párrafo de arriba y
179sigue siendo cierto. Corolario: **un fichero que aparece `MM` y que vos no tocaste no se commitea
180ni con pathspec — se avisa.** Pasó con el `Cargo.lock` de tawasuyu (SDD 26 §7.quinquies), donde el
181árbol del otro agente ya traía la reparación que hacía falta.
182
183## 2 ter. `git pull --rebase` puede DESCARTAR tu commit sin decir nada
184
185⚠ **Medido acá el 2026-09-16, y cuesta una hora de trabajo si no sabés mirar el reflog.**
186
187Secuencia real: commit propio en `main` (10 ficheros nuevos) → `git push` → **gitea lo rechaza**
188porque otro agente empujó → `git pull --rebase` → el push vuelve a fallar y **el commit ya no está**,
189ni en el log ni en el ÁRBOL: los diez ficheros habían desaparecido del disco.
190
191El reflog lo explica y es lo único que lo explica:
192
193```
19411a9ead6 HEAD@{0}: pull --rebase (finish): returning to refs/heads/main
19511a9ead6 HEAD@{1}: pull --rebase (start): checkout 11a9ead6
196f3d7be6c HEAD@{2}: commit: los 9 daemons propios entran al catálogo…     ← el mío
197```
198
199**Entre `start` y `finish` no hay ni un `pick`**: el rebase no encontró NADA que reaplicar, y con
200`-q` eso se ve igual que un pull limpio.
201
202⚠ **PASÓ DOS VECES EL MISMO DÍA** (2026-09-16 y 2026-09-17), con el mismo reflog: `commit` propio,
203`(start): checkout <otro>`, `(finish): returning to refs/heads/main` — y ni un `pick` en el medio.
204
205**Y el comando NO es el culpable: se reprodujo en un repo de juguete y ahí funciona bien.** Dos
206clones, el otro empuja, yo commiteo local, `git pull --rebase -q origin main` ⇒ **mi commit se
207reaplica y mi fichero sigue ahí**. O sea que lo que falla no es `pull --rebase`: es que este árbol
208lo comparten varios agentes y **alguien mueve `refs/heads/main` mientras el rebase está en vuelo**.
209La receta práctica no cambia; el diagnóstico sí, y evita ir a buscar el bug al lugar equivocado.
210
211⚠ **La CAUSA exacta no está determinada, y conviene decirlo así en vez de inventarla.** Lo medido es el
212reflog de arriba y que los diez ficheros desaparecieron también del ÁRBOL, que es lo que un
213`reset --hard` o un `checkout` concurrente de otro agente sí explicaría. Lo que NO es evidencia —y
214se escribió como si lo fuera— es que los commits que trajo el `pull` cuelguen del commit anterior al
215mío: eso es lo normal cuando el otro lado empujó desde una máquina que había hecho `fetch` antes, y
216no dice nada de lo que pasó acá. **Que el árbol perdiera los ficheros es el dato fuerte; el resto,
217hipótesis.**
218
219**Qué hacer:**
220
221```sh
222git reflog -15                      # tu commit está acá, con su sha
223git cherry-pick <sha>               # vuelve entero, ficheros incluidos
224```
225
226Y la comprobación que evita el susto: **`git log --oneline -1` después de cada `pull --rebase`.** Si
227tu commit no es el primero, no se rebasó — se perdió de la rama, y el reflog lo tiene.
228
229⚠ **El espejo de GitHub queda DIVERGENTE en ese escenario**, y es normal: el `push` doble puede
230triunfar en un remoto y fallar en el otro (pasó: gitea rechazó, GitHub aceptó). Tras recuperar el
231commit, el espejo tiene un sha huérfano con el mismo contenido. Se repara empujando **sólo la URL de
232GitHub** con `--force-with-lease=main:<sha-huérfano>` — con lease, que verifica que el remoto sigue
233donde creés antes de pisarlo. Comprobar antes que el contenido no se pierde:
234`git diff --stat <huérfano> HEAD`.
235
236## 3. Antes de dar un artefacto por presente, mirá que tenga contenido
237
238Un directorio **vacío** en el store no es un artefacto: es un nombre. `Store::has` ya lo rechaza y
239`respaldo-storagebox.sh --listar` los separa a `work/respaldo-vacios.txt`, pero la regla general
240sigue valiendo para cualquier código nuevo — **un ausente falla ruidosamente; un vacío llega hasta
241el final diciendo que todo fue bien**.
242
243## 3 bis. En el HUB, el scratch de build no cae donde parece — y el disco bueno estaba sin usar
244
245⚠ **Medido el 2026-09-21.** `work_root` no es una ruta elegida: sale de `dirname(store)/work`
246(`crates/takana-build/src/config.rs`). Con el store en `/store`, el padre es `/` ⇒ **todo el scratch
247de build cae en el overlay de la jaula**. En esta caja eso son 69 G compartidos, y el disco de
248verdad —`/dev/sdc`, 255 G— estaba **al 9 %, sin usar**.
249
250Dos cosas lo empeoran, y ninguna se ve desde `df`:
251
252- **El overlay es capa DESECHABLE.** `/work` no tiene montaje propio: si la jaula se rehace, los
253  árboles de fuentes se van con ella. La caché de build vivía en algo que se tira.
254- **Los árboles se nombran por RECETA, no por commit.** `boveda` y `shuma-pregunta` salen del mismo
255  `b80f7567…` de tawasuyu y ocupaban **2,7 G cada una** — 5,4 G de los 6,8 G totales eran el mismo
256  contenido dos veces. Cada receta nueva de ese monorepo cuesta otros 2,7 G. Es deliberado (árbol
257  por receta = el aislamiento que evita el destrozo del ADR 0012); **no deduplicar de paso.**
258
259✅ **ARREGLADO EN EL CÓDIGO el 2026-10-15, y ya no hay nada que recordar.** Lo de abajo se deja
260porque explica el síntoma, pero **los enlaces NO hacen falta y no hay que rehacerlos.**
261
262La causa era que `defaults_for_store_with_lab` derivaba `work_root = dirname(store)/work` para que
263el scratch cayera en el filesystem DEL STORE —el `seal` es un `rename`, atómico sólo dentro del
264mismo fs—, y esa derivación **sólo lo consigue si el store no es un punto de montaje**. Acá
265`/store` es su propio montaje (`sdc`), su padre es `/`, y `/work` es `sda4`: la derivación hacía
266exactamente lo contrario de lo que pretendía, en silencio. Ahora, **si el store es punto de
267montaje, el scratch va dentro del store en `.work`** — mismo fs por construcción (`store_es_punto_de_montaje`
268+ `work_root_para` en `config.rs`). El worker no se mueve: ahí store y padre comparten fs.
269
270⇒ En esta caja: `/store es montaje=true ⇒ work_root=/store/.work`. Sin variables y sin enlaces.
271
272⚠ **Lo que SÍ hay que recordar es la lección, porque se pagó dos veces:** el 21-sep esto se parcheó
273con symlinks (`ln -sfn /work/sergio/work/sources /work/sources`), **se perdieron al rehacer la
274jaula**, y el 2026-10-15 `/work` se llenó al 100 % con 15 G de scratch que debía estar en el disco
275grande. *Un arreglo que hay que recordar no es un arreglo* — y vale igual para un `export
276TAKANA_WORK` en una shell o en un script: el día que alguien construya desde otro sitio, el
277problema vuelve sin avisar. El override `TAKANA_WORK` sigue existiendo para casos deliberados.
278
279**El WORKER no tiene este problema:** un único disco de 196 G con todo encima (`/`, `/opt/takana`,
280su store), así que ahí `work_root` y el store comparten filesystem y el `seal` —que es un `rename`,
281atómico sólo dentro del mismo fs— cumple su premisa. **En el hub esa premisa estuvo rota** hasta el
2822026-10-15 —`/work` en `sda4` y `/store` en `sdc`, discos distintos— y la restauró el arreglo de
283arriba: el scratch pasa a `/store/.work`, en el filesystem del store. Ya no hace falta mover el
284store a ningún sitio.
285
286## 4. La superficie de la CLI va en INGLÉS; los mensajes, en castellano
287
288Subcomandos, flags y nombres de opciones: **inglés** (`build`, `hash`, `hydrate`, `mirror push`,
289`kernel closure`, `qorpa pull`). Lo que el usuario LEE —ayuda, errores, logs— va en castellano, y
290los nombres propios del proyecto son quechua (`takana`, `qorpa`, `harkaq`, `yupana`, `arje`).
291
292**La única excepción, y es cerrada:** el ADR 0016 admite **alias de marca** en castellano sobre un
293verbo canónico —hoy uno solo, `forja` → `build`— para que la invocación de la hoja de marca
294funcione. El alias **no reemplaza nada**: el canónico sigue siendo el inglés y es el que usan los
295scripts, el cron y los runbooks. Un alias es superficie de marca; el contrato es el canónico.
296Agregar otro alias es una decisión de ADR, no algo que se hace de paso.
297
298**Por qué.** Un verbo de CLI es contrato: entra en scripts, cron y runbooks, y renombrarlo después
299rompe llamadores que nadie recuerda. Media superficie en cada idioma obliga a adivinar en cada
300comando nuevo, y ya pasó: el ADR 0015 nació con `traer/crear/correr` y hubo que corregirlo.
301
302⚠ **Deuda conocida, NO barrida todavía:** varios scripts de `scripts/` exponen flags en castellano
303(`--crear`, `--traer`, `--listar`, `--sha`). El barrido es su propia unidad de trabajo — tocarlos de
304paso rompe cron y la granja. Código NUEVO nace en inglés desde hoy.

CLAUDE.md

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

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:

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.

✅ ACTUALIZADO 2026-09-22 — el ADR 0012 está DECIDIDO e IMPLEMENTADO, y la carrera ya no existe. takana build toma un lock exclusivo POR RECETA en el fetch y lo sostiene hasta el seal (fetch::GuardaArbol), con re-chequeo de la tienda al obtenerlo: el que espera sale por cache hit. No serializa la granja —dos recetas distintas no comparten su árbol propio, y un consumidor recibe el ARTEFACTO de su dep, no su árbol— ni puede deadlockear, porque las deps se construyen ANTES de tomar el lock propio (los locks no se anidan). Verificado con dos builds concurrentes y con un control que reproduce la firma del apagón (.zwrap/cc AUSENTE, 0/400 fuentes sin lock; OK con él).

⇒ El flock de abajo SIGUE HACIENDO FALTA, pero por otra razón: serializa contra farm-worker-loop.sh y campana-deuda.sh, que es control de RECURSOS (RAM, disco), no de corrupción. Y JOBS=1 ya no es una necesidad de corrección: subirlo es una decisión de recursos — el worker son 6 cores / 16 G y esta clase de máquina ya provocó un global_oom.

⚠ 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:

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.

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).

✅ ARREGLADO el 2026-09-22 — y la causa NO era la que decía acá. Lo de abajo se deja porque el síntoma es el mismo y alguien lo va a volver a buscar, pero el diagnóstico estaba mal: el git del corpus SÍ trae git-remote-https (está en su artefacto, medido: ls …-git/usr/libexec/git-core | grep -c remote-http ⇒ 2). Lo que pasaba es que git busca sus treinta ayudantes en un directorio horneado al build —git --exec-path ⇒ /usr/libexec/git-core, la ruta FHS—, y corrido desde el store mira el libexec de la IMAGEN de la jaula, que tiene 0. Faltaba GIT_EXEC_PATH, nada más. Puesto en el generador (scripts/servidor/jaula-herramientas.sh) y en el envoltorio /work/sergio/bin/git; con eso el mismo binario clona por https y empuja al espejo. Si el síntoma vuelve, mirá git --exec-path ANTES de creer que faltan los helpers.

⚠ El síntoma histórico, para que se reconozca (medido 2026-09-18): el PATH del lab pone primero el git del CORPUS —/work/sergio/bin/git, 2.54.0, musl— y su envoltorio no le decía dónde están sus ayudantes. El push terminaba 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:

/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:

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.

# 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:

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:

✅ ARREGLADO EN EL CÓDIGO el 2026-10-15, y ya no hay nada que recordar. Lo de abajo se deja porque explica el síntoma, pero los enlaces NO hacen falta y no hay que rehacerlos.

La causa era que defaults_for_store_with_lab derivaba work_root = dirname(store)/work para que el scratch cayera en el filesystem DEL STORE —el seal es un rename, atómico sólo dentro del mismo fs—, y esa derivación sólo lo consigue si el store no es un punto de montaje. Acá /store es su propio montaje (sdc), su padre es /, y /work es sda4: la derivación hacía exactamente lo contrario de lo que pretendía, en silencio. Ahora, si el store es punto de montaje, el scratch va dentro del store en .work — mismo fs por construcción (store_es_punto_de_montaje

⇒ En esta caja: /store es montaje=true ⇒ work_root=/store/.work. Sin variables y sin enlaces.

⚠ Lo que SÍ hay que recordar es la lección, porque se pagó dos veces: el 21-sep esto se parcheó con symlinks (ln -sfn /work/sergio/work/sources /work/sources), se perdieron al rehacer la jaula, y el 2026-10-15 /work se llenó al 100 % con 15 G de scratch que debía estar en el disco grande. Un arreglo que hay que recordar no es un arreglo — y vale igual para un export TAKANA_WORK en una shell o en un script: el día que alguien construya desde otro sitio, el problema vuelve sin avisar. El override TAKANA_WORK sigue existiendo para casos deliberados.

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 estuvo rota hasta el 2026-10-15 —/work en sda4 y /store en sdc, discos distintos— y la restauró el arreglo de arriba: el scratch pasa a /store/.work, en el filesystem del store. Ya no hace falta mover el store a ningún sitio.

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.