# Notas del carril verde

Lo medido, lo resuelto y lo que queda abierto.

Fechas: 15 de agosto de 2026 (instalación, en frío) y **16 de agosto (primer
arranque real)**. Lo de la primera fecha salía de leer código y mirar el disco;
lo de la segunda está ejecutado.

## EL SUELO DE 64k: el tope que invalidó la medida anterior (16 ago)

*(Esto va primero porque cambia la conclusión de la sección siguiente.)*

`arrancar.sh` midió bien: **24576 era el mayor contexto que cabía** en la GPU
con las 32 capas en la tarjeta, 3301 MiB, y con eso el modelo emitía `tool_calls`
válidos. Todo correcto y todo inútil, porque al abrir Hermes:

```
Model qwen3.5:4b-verde has a context window of 24,576 tokens, which is below
the minimum 64,000 required by Hermes Agent.
```

Es un **tope duro del arnés**, no un aviso: `agent/model_metadata.py:405`,
`MINIMUM_CONTEXT_LENGTH = 64_000`, comprobado en `agent/agent_init.py:2656`.
No se ve en frío — `hermes prompt-size` no lo menciona — y solo salta al
construir el agente.

### Las dos salidas que se descartaron, y por qué

1. **La puerta de atrás de `lmstudio`.** El mismo `if` se salta si
   `provider == "lmstudio"` y `context_length` está puesto a mano. No se usa: ese
   proveedor llama a `{server}/api/v1/models`, la API **nativa de LM Studio**,
   que ollama no tiene, y además intenta precargar el modelo por su cuenta
   (`ensure_lmstudio_model_loaded`). Fingir ser otro servidor rompería más
   adelante y de forma más confusa.
2. **Mentir en el `config.yaml`** poniendo 64000 con el modelo a 24576. Es la
   peor de las tres: Hermes creería tener 64k, **no comprimiría** hasta
   acercarse, y ollama iría tirando los tokens más viejos — que son el prompt de
   sistema y los esquemas de las 19 herramientas. El síntoma sería que el agente
   deja de llamar herramientas a mitad de sesión, **sin un solo error en el log**.

### Lo que se hizo: subir el contexto de verdad y pagar capas

Con `num_ctx 65536` fijo, medido el 16 de agosto:

| `num_gpu` | Resultado | En tarjeta | Total |
|---|---|---|---|
| 32 | `cudaMalloc failed` | — | — |
| 24 | `cudaMalloc failed` | — | — |
| **16** | **carga** | **2795 MiB** | 5784 MiB |
| 8 | carga | 1772 MiB | 5784 MiB |
| 0 | carga | 0 MiB | 5368 MiB |

O sea que 64k **sí cabe en esta máquina**, pero con **la mitad de la red en la
CPU** y unos 3 GB en RAM. Lo que eso cuesta en tiempo está más abajo, en
"Cuánto tarda de verdad".

La palanca para recuperar capas —`OLLAMA_FLASH_ATTENTION=1` +
`OLLAMA_KV_CACHE_TYPE=q8_0`, que parte el KV caché por la mitad— **sigue sin
aplicarse**: es del servicio de ollama, no del modelo, y afectaría también al
naranja. Esa decisión es del usuario.

**Lección de método**, que es la misma que ya aparece dos veces en este fichero:
se midió a fondo el límite de la *máquina* y no se comprobó el límite del
*software que iba encima*. Los dos son topes; solo uno se estaba mirando.

## Cuánto tarda de verdad (16 ago)

Medido con `hermes -z`, que es una pregunta y a callar, contra el modelo a
`num_ctx 65536` con 16 de 32 capas en GPU.

| Escalón | Vueltas | Tiempo |
|---|---|---|
| 1 · "hola, ¿quién eres?" — **en frío** | 0 herramientas | **2 min 38 s** |
| 2 · "¿cuánto espacio libre queda?" | 1 herramienta | **48,6 s** |
| 3 · "lee verde/README.md y resúmelo" | 4 (dos fallidas + recuperación) | **3 min 29 s** |
| 3 bis · igual, con `terminal.cwd` arreglado | 1 | **1 min 58 s** |
| 3 ter · igual, ya con `AGENTS.md` | 2 (una fallida + recuperación) | **4 min 54 s** |
| 4 · "cuál de los .md de verde/ es el más largo" | 4 encadenadas | **3 min 54 s** |
| 5 · "un cartel en arte ascii que ponga JARVIS" | 3 (una salió a la red) | **4 min 58 s** |
| 6 · "di en voz alta…" | 1 (`text_to_speech`) | **3 min 37 s** |
| *(pwd suelto, caché caliente, una llamada)* | 1 | **13,2 s** |

**Esa diferencia de tres veces no es ruido, y es lo más útil que se midió.** El
primer turno paga el prefill entero de los ~20.500 tokens del prompt fijo con
media red en la CPU. A partir de ahí, ollama **cachea el prefijo** y las vueltas
siguientes solo procesan lo nuevo — y eso que la 2 hace *dos* llamadas al modelo
(pedir la herramienta y redactar con su resultado) y aun así tarda un tercio.

O sea que la cifra que importa para decidir si esto se usa **no es la del primer
arranque**. Es la de régimen. Igual que la primera transcripción de whisper
tarda ~4 s por arrancar el servidor y luego se queda en ~930 ms.

El escalón 2 acertó, además: `663G` libres de `1,9T` al `63 %`, que es
exactamente lo que dice `df -h /`.

**Y el 3 enseñó algo que no se estaba buscando.** Falló dos veces seguidas —
partió mal la frase y pidió `cat ~/lee-verde/README.md`, que no
existe— y en vez de inventarse una respuesta cambió de estrategia: `search_files`
para localizar el fichero, y `read_file` con la ruta absoluta ya correcta. El
resumen final es fiel al README.

Eso es exactamente lo que se venía a probar de este carril: **recuperarse de una
herramienta que devuelve error**. El núcleo naranja, con sus acciones rápidas, o
acierta a la primera o no hay segunda. Las dos vueltas de más son las que
explican los 3 min 29 s.

### El punto flaco del 4B: las rutas relativas

Con `terminal.cwd` ya arreglado se repitió el escalón 3, y salió **peor**:
**1 min 58 s** y una sola llamada, `read_file` sobre
`COFRE/CODERS/JARVIS/README.md`. **Se comió el `verde/`.** Resumió el README de
la raíz —el del proyecto entero— con un "JARVES" de propina y la frase cortada a
medias.

La comparación entre las dos pasadas es lo interesante:

| | Ruta base | Qué hizo | Resultado |
|---|---|---|---|
| Con `cwd` en `$HOME` | mal | falló, buscó, se corrigió | **acertó** |
| Con `cwd` en el repo | bien | fue directo a una ruta plausible | **falló** |

O sea: **cuando la herramienta le devuelve un error, se recupera; cuando le
devuelve un fichero que existe pero no es el pedido, no tiene forma de notarlo**
y lo resume tan campante. Es el mismo patrón del error de `~/README.md`,
y la razón de que estos fallos no se vean: no hay error que mirar.

Lo que apunta a la mejora obvia si este carril sigue adelante: un `AGENTS.md` en
la raíz del repo describiendo la disposición de carpetas. Hermes ya carga
ficheros de contexto, así que el sitio está.

#### Escrito el `AGENTS.md`, el escalón 3 pasa — pero no por lo que parece

Se escribió (`<raíz>/AGENTS.md`: los tres carriles, el árbol de carpetas y la
regla de que las rutas relativas salen de la raíz del repo). Cuesta **2.490 B**
del prompt fijo, que sube a 84.740 B ≈ **21.185 tokens** de los 65.536: quedan
44.351 para conversar.

Tercera pasada del escalón 3, **4 min 54 s**, y el resumen es del fichero
correcto por fin. Recoge incluso el aviso de la fuga a internet que se añadió
hoy al README.

**Pero el modelo volvió a alucinar la ruta.** Su primera llamada fue

    read_file ~/COFRE/CODERS/JARVIS-verde/zapier-connection.py

que no existe y no se parece a nada de lo pedido. Recibió `File not found` y
**entonces** acertó con `JARVIS/verde/README.md`.

O sea que el `AGENTS.md` **no evitó el disparate; le dio con qué recuperarse**.
Es la tercera vez que sale el mismo patrón en esta sesión y ya no es anécdota:

| Lo que recibe la herramienta | Qué hace el 4B |
|---|---|
| Un **error** | cambia de estrategia y suele acertar |
| Un fichero **que existe pero no es** | lo resume tan contento |

La lección práctica para este carril: **más vale una ruta que falle que una que
acierte por accidente.** Y en la prosa sigue descuidado — aquí se inventó un
"98 % local", llamó API de terceros a `pyfiglet` (que es una librería local; la
API era `asciified.thelicato.io`) y le dio la vuelta a la frase del objetivo.

### El escalón 4, que era el que decidía: **pasa**

*"dime cuál de los .md de la carpeta verde/ es el más largo"* — **3 min 54 s**,
cuatro llamadas encadenadas, respuesta correcta:

| # | Llamada | Qué pasó |
|---|---|---|
| 1 | `search_files` con `pattern: "*.md"` | `rg: regex parse error` |
| 2 | `search_files` con `pattern: "\\.md$"` | **corrigió el patrón él solo** |
| 3 | `terminal`: `find … -name "*.md"` | los cinco ficheros |
| 4 | `terminal`: bucle con `wc -l` sobre cada uno | las cinco cifras |

Y concluyó: `verde/NOTAS.md` con 625 líneas. Es el correcto.

**Esto es lo que justificaba el carril entero.** No hay una herramienta que
conteste esa pregunta: hay que listar, medir y razonar sobre el resultado de la
vuelta anterior, y saber cuándo parar. El núcleo naranja no puede hacerlo — sus
150 acciones rápidas o aciertan a la primera o no hay segunda.

La letra pequeña, que no cambia el veredicto pero conviene tener escrita: al
enumerar los ficheros los ordenó mal (puso PLAN-ARRANQUE con 181 por encima de
ESCALERA con 208) y etiquetó el README de `verde/` como *"categoría raíz"*. O
sea que **el razonamiento de la cadena es bueno y la prosa de alrededor es
descuidada**. Para una orden hablada da igual; para un informe, no.

## EL BLOQUEO DEL ARRANQUE: no cabe la pregunta

*(15 ago, tarde. Esto es lo más importante del fichero.)*

Se preparó el primer arranque y apareció un tope que no era el que se
anticipaba. **No es que `qwen3.5:4b` no sepa llamar herramientas: es que no le
cabe la pregunta.** Medido con `hermes prompt-size`:

| | |
|---|---|
| Prompt de sistema de Hermes | 26.281 B |
| Esquemas de las 19 herramientas | 55.978 B |
| **Fijo, antes de decir nada** | **82.259 B ≈ 20.500 tokens** |
| `num_ctx` de `qwen3.5:4b-jarvis` | **4.096** |

Cinco veces por encima. Y hay un segundo tope escondido: **`num_predict 300`,
que el naranja hereda del modelo BASE** (no lo pone su Modelfile). 300 tokens
cortan el JSON de una llamada a herramienta por la mitad.

Los dos están **bien puestos para el naranja** —respuestas habladas cortas, VRAM
para whisper— y son justo lo contrario de lo que pide un arnés agéntico.

Y encima, la trampa que avisaba la documentación: `/api/show` reporta
`qwen35.context_length = 262144`, el máximo del modelo, **no** los 4096
configurados. Hermes se habría creído que tenía 262k. El síntoma habría sido
respuestas truncadas y llamadas rotas, y la conclusión natural —"el 4B no vale
para esto"— **habría sido falsa**.

### La variante verde, ya creada

`verde/qwen3.5-4b-verde.Modelfile` → `ollama create qwen3.5:4b-verde`.
**`qwen3.5:4b-jarvis` no se tocó.**

| Parámetro | Valor | Por qué |
|---|---|---|
| `num_gpu` | 32 | todas las capas, con el naranja cerrado |
| `num_ctx` | 24576 | 32768 no cabe (ver abajo) |
| `num_predict` | 4096 | 300 cortaría una tool call |
| `temperature` | 0.2 | emitir JSON no es escribir prosa |

**Trampa del Modelfile:** ollama **no admite comentarios en la misma línea** que
un `PARAMETER`. Esto falla:

```
PARAMETER num_gpu 32          # todas las capas
→ Error: invalid int value [32          # todas las capas]
```

Los comentarios van en su propia línea.

### Lo medido de VRAM

| Configuración | Resultado |
|---|---|
| `num_ctx 32768`, 32 capas | **`cudaMalloc failed: out of memory`** |
| `num_ctx 24576`, 32 capas | **pendiente**: hay que medirlo con la tarjeta libre |

Escalera si 24576 tampoco entra, en orden de menos a más invasivo: `num_ctx
16384` (el suelo: por debajo no cabe el prompt fijo con holgura) → `num_gpu 24`
→ `OLLAMA_FLASH_ATTENTION=1` + `OLLAMA_KV_CACHE_TYPE=q8_0`.

**Cuidado con el último:** `OLLAMA_KV_CACHE_TYPE` es del **servicio**, no del
modelo. Se pone en el systemd de ollama y **afectaría también al naranja** la
próxima vez que arranque. Si se usa, hay que anotarlo aquí y en el Modelfile del
naranja.

### Desde dónde se lanza importa, y mucho

| Directorio de arranque | Prompt de sistema |
|---|---|
| `~/COFRE/CODERS/JARVIS` | 26.281 B |
| `~/.hermes/hermes-agent` | **55.261 B** |

La diferencia son 30 KB: el `AGENTS.md` de 81 KB del **propio repo de Hermes**,
que se cuela como fichero de contexto y encima sale truncado con un aviso. A
mano es fácil olvidarlo y sacar medidas que no comparan nada.

### Y el `cd` NO basta (16 ago)

Esto se descubrió subiendo la escalera y es peor que lo anterior, porque **no
deja ni un error**. `verde-launcher.sh` hacía `cd "$RAIZ"` antes de llamar a
Hermes, que parece suficiente y no lo es: **Hermes restaura el directorio de
trabajo apuntado en la sesión** y se salta aquel desde el que lo lanzas.

El síntoma: se le pide *"lee verde/README.md y resúmelo en una línea"* y
devuelve

> **Gitleaks es una herramienta SAST diseñada para detectar y prevenir secretos
> codificados a mano en repositorios git.**

Parece el modelo alucinando de manera espectacular. No lo es. En la base de
sesiones se ve la llamada real:

```
terminal  {"command": "cat ~/README.md | head -50"}
```

Resolvió `verde/README.md` contra `~`, ahí hay un `README.md` que es el
de Gitleaks, lo leyó entero y lo resumió **bien**. La herramienta funcionó, el
modelo entendió, el resumen es correcto — y la respuesta es inútil.

### Lo que NO lo arregla, probado

**`--in DIR` no vale en modo `-z`.** Parece la bandera exacta —entra en el
directorio *y* se salta la restauración, mientras que `--no-restore-cwd` solo
hace lo segundo— y con ella el fallo se repitió igual. El motivo está en
`hermes_cli/main.py:11324`: el modo de una sola pregunta sale por
`_run_and_exit_oneshot(...)` y **termina el proceso ahí**, sin llegar nunca a
`cmd_chat`, que es la única función donde se atiende `--in`. Se ignora en
silencio. En el TUI sí funciona, y por eso `verde-launcher.sh` la pasa.

**Un `cd` delante tampoco.** Comprobado del modo más directo posible:

```
pwd del shell : ~/COFRE/CODERS/JARVIS
pwd del agente : ~
```

Y no es que el modelo se lo invente: el `ls -d */` que ejecutó por su cuenta
devolvió `BIKEPARK/ COFRE/ ctf/ Downloads/…`, o sea el `$HOME` de verdad.

### Lo que sí lo arregla

`terminal.cwd` en `~/.hermes/config.yaml`, que de fábrica viene como `"."` y
que su propia documentación describe como *"el directorio actual donde ejecutas
hermes"*. **No lo es.**

```yaml
terminal:
  cwd: "~/COFRE/CODERS/JARVIS"
```

Verificado: con eso el prompt de sistema pasa a decir
`working directory: ~/COFRE/CODERS/JARVIS`. Está replicado en
`verde/config.yaml` y lo comprueba `pruebas/03_config.sh`.

**Ojo, que esto ATA el carril a este repositorio.** Es lo correcto para un banco
de pruebas que se compara contra el naranja, y es una limitación real si algún
día se quiere que el verde mire `~/BIKEPARK` o `~/oasis`. Con la raíz en `$HOME`
además se ahoga: un `search_files` de `*.md` devolvía sobre todo
`site-packages/` de los venv de `entrena/`.

**Cómo se diagnosticó, que es reutilizable:** `-z` imprime solo el texto final,
sin las llamadas a herramienta, así que desde fuera no se ve nada. Todo queda en
`~/.hermes/state.db`, tabla `messages`, columnas `tool_calls` y `content`:

```bash
python3 -c "
import sqlite3
c = sqlite3.connect('file:$HOME/.hermes/state.db?mode=ro', uri=True)
for r in c.execute('SELECT role, tool_name, tool_calls, content FROM messages ORDER BY rowid DESC LIMIT 10'):
    print(r)
"
```

Sin mirar ahí, este fallo se atribuye al modelo y se acaba cambiando de modelo
para nada.

## Por qué se paró: `dataset.py`

El arranque en GPU quedó **aplazado**, no descartado. La gráfica la tiene un
trabajo tuyo: `JARVIS/entrena/dataset.py`, que genera pares pregunta-respuesta
de COFRE con `qwen3.5:4b-jarvis`. Son **7-10 horas** (tu propio README lo dice) y
va por `100/2261 fragmentos`.

Detalle que confunde y conviene tener escrito: mientras ese script corra,
`ollama stop qwen3.5:4b-jarvis` **parece no funcionar**. Sí funciona — descarga
el modelo — pero la siguiente petición del script lo vuelve a cargar en
segundos. No es un fallo de ollama.

Comprobado que los `ollama stop` **no rompieron** la generación: siguió de 199 a
224 ejemplos en 75 s.

### Retomar cuando termine

```bash
# 1. confirmar que dataset.py acabó y la tarjeta está libre
pgrep -f 'entrena/dataset.py' || echo "terminado"
nvidia-smi --query-gpu=memory.used --format=csv,noheader     # ~140 MiB

# 2. medir si 24576 cabe
curl -s http://127.0.0.1:11434/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"qwen3.5:4b-verde","messages":[{"role":"user","content":"ok"}],"max_tokens":5}'
curl -s http://127.0.0.1:11434/api/ps | python3 -m json.tool | grep -E 'size_vram|context_length'

# 3. decirle a Hermes el contexto REAL (si no, cree que son 262144)
#    en ~/.hermes/config.yaml, bloque model:, en las lineas canonicas (~82/~95)
#      default: "qwen3.5:4b-verde"
#      context_length: 24576

# 4. LA PRUEBA DECISIVA, antes de abrir el TUI: ¿emite tool_calls?
curl -s http://127.0.0.1:11434/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model":"qwen3.5:4b-verde",
  "messages":[{"role":"user","content":"¿Cuánto espacio libre queda en el disco?"}],
  "tools":[{"type":"function","function":{"name":"ejecuta_comando",
    "description":"Ejecuta un comando de shell",
    "parameters":{"type":"object","properties":{"comando":{"type":"string"}},"required":["comando"]}}}]}' \
  | python3 -m json.tool
```

El paso 4 separa dos preguntas que juntas se contestan mal: *¿sabe el modelo
emitir una llamada?* y *¿funciona Hermes?*. Si sale `tool_calls` con JSON
válido, lo que falle después es de Hermes. Si contesta en prosa, abrir el TUI
solo daría un fallo más confuso.

Después, la escalera del plan: "hola" → una herramienta → ficheros → **varias
vueltas encadenadas** (el escalón que separa un chatbot de un agente) → skills.
La voz al final, y la escucha la última de todas: los 839 MiB de pico de whisper
contra una tarjeta que tendrá el modelo casi entero.

## Estado de la gráfica

| | VRAM | Quién |
|---|---|---|
| Antes de instalar | 878 MiB de 4096 | tres `whisper-server` tuyos, del naranja |
| Después de instalar | **127 MiB de 4096** | nadie; tus whisper acabaron solos |

Instalar **no cargó nada** en la tarjeta. Comprobado con
`nvidia-smi --query-compute-apps` y con `/api/ps` de Ollama, que devolvió
`{"models": []}` en todo momento.

De paso, un dato que puede interesarte del naranja: llegaste a tener **tres
`whisper-server` a la vez**, ~880 MiB cada uno, 2648 MiB en total sobre una
tarjeta de 4096. Es el escenario exacto del `CUDA error: out of memory` que
documenta el README. No hice nada al respecto —es tu desarrollo en marcha— pero
queda anotado.

## Verificación en frío, ejecutada

| Comprobación | Resultado |
|---|---|
| `hermes` en PATH | `~/.local/bin/hermes` |
| Tamaño de `~/.hermes` | 2,2 GB |
| Caché de Playwright | 622 MB |
| Credenciales con valor en `.env` | **0** |
| Tokens de mensajería | **0** (por eso el gateway nunca arrancó) |
| URLs de nube activas en `config.yaml` | **0** |
| Puertos fuera de loopback | **ninguno** |
| Modelos cargados en Ollama | ninguno |
| Puente de voz | **funciona**: 0,34 s para 3,2 s de audio, 22050 Hz mono |

Las 11 líneas con contenido del `.env` no son claves: son `TERMINAL_TIMEOUT`,
`BROWSER_SESSION_TIMEOUT`, banderas de depuración y similares.

## Trampa: claves YAML duplicadas en config.yaml

**Esto casi cuela y merece quedar escrito.** El `config.yaml` que genera el
instalador declara `provider` y `base_url` **dos veces** dentro del bloque
`model:`: una arriba, junto a `default`, y otra ~45 líneas más abajo. En YAML la
última repetición gana.

Al configurar Ollama arriba, el fichero *parecía* correcto leyéndolo, y al
parsearlo salía esto:

```
model.provider: auto
model.base_url: https://openrouter.ai/api/v1
```

Es decir: el carril habría salido **por OpenRouter** creyendo yo que iba a
localhost. Se arregló dejando los valores solo en las líneas de abajo, las
canónicas.

**La lección, que vale para todo este fichero:** con 1890 líneas y 121 activas,
no basta con leerlo. Hay que parsearlo:

```bash
python3 -c "import yaml;d=yaml.safe_load(open('$HOME/.hermes/config.yaml'));print(d['model'])"
```

## Trampa: la instalación de Chromium se cuelga

Le pasa a más gente (issue #35166 del repo). El zip se descarga entero y la
**extracción se queda parada**: 50 minutos de reloj con 1 segundo de CPU y cero
crecimiento en `~/.cache/ms-playwright/`. El `timeout` del instalador mata al
padre y deja procesos huérfanos reteniendo un `__dirlock`, así que el reintento
también falla.

Se resolvió a mano, y si vuelve a pasar tras un `hermes update`, esta es la
receta:

```bash
pkill -f oopDownloadBrowserMain; pkill -f 'playwright install'
rm -rf ~/.cache/ms-playwright/chromium-1208 ~/.cache/ms-playwright/__dirlock
mkdir -p ~/.cache/ms-playwright/chromium-1208
unzip -q /tmp/playwright-download-*/playwright-download-chromium-*.zip \
      -d ~/.cache/ms-playwright/chromium-1208/
touch ~/.cache/ms-playwright/chromium-1208/INSTALLATION_COMPLETE
```

El `INSTALLATION_COMPLETE` es lo que Playwright mira para dar el navegador por
bueno (`lib/server/registry/index.js:1345`). Sin él lo reinstala cada vez.

Quedaron los tres componentes, verificados con `playwright install --dry-run`:

| | Tamaño |
|---|---|
| `chromium-1208` | 364 MB |
| `chromium_headless_shell-1208` | 254 MB |
| `ffmpeg-1011` | 4,9 MB |

El binario responde: `Google Chrome for Testing 145.0.7632.6`.

## Resuelto: tu voz afinada SÍ se puede reutilizar

Era la pregunta abierta del plan, y salió el mejor de los tres desenlaces.

**Hermes tiene un proveedor de TTS por comando que no está documentado en su
web.** Está en `tools/tts_tool.py`, bloque *"Custom command providers"*:

```yaml
tts:
  provider: mi-voz
  providers:
    mi-voz:
      type: command
      command: "... {output_path} {input_path}"
      output_format: wav
```

Marcadores: `{input_path}`, `{text_path}`, `{output_path}`, `{format}`,
`{voice}`, `{model}`, `{speed}`. Ejecuta con `shell=True`.

Eso permite llamar al mismo `config/jarvis-piper.sh` que usa el naranja, así que
**el verde suena exactamente igual**: mismo binario Piper 1.2.0, mismo `.onnx`,
misma cadena de audio. Sin esto la comparación de voz entre carriles no habría
valido nada.

Dos detalles que obligaron a escribir `voz-verde.sh` en vez de meter el comando
directo en el YAML:

1. **El entorno se limpia.** `_run_command_tts` llama a
   `hermes_subprocess_env(inherit_credentials=False)`, así que
   `JARVIS_PIPER_VOZ` y `JARVIS_PIPER_TONO` no llegarían. Se ponen dentro del
   adaptador.
2. **Las interfaces no encajan.** `jarvis-piper.sh` quiere el texto como
   argumento; Hermes entrega un fichero. El adaptador hace de puente.

### Qué voz, y ojo con el README

`voz-verde.sh` usa **davefx a tono 1.0**, que es lo que tiene el núcleo hoy
(`nucleo/boca/voz.py`: `POR_DEFECTO = "davefx"`, `JARVIS_PIPER_TONO = "1.0"`).

El `README.md` del repo describe `sharvard` + 0,86 = 106 Hz como *"la puesta"*.
Eso era el JARVIS sobre Newelle. **El verde copia al núcleo, que es contra quien
se va a comparar, no al README.** Si algún día el núcleo vuelve a sharvard, hay
que cambiarlo aquí también.

### De paso: el Piper de Hermes es el GPL, no el tuyo

El proveedor `piper` de serie usa `OHF-Voice/piper1-gpl` (GPL-3.0). El tuyo es
el original `rhasspy/piper` 1.2.0, **MIT**, archivado en octubre de 2025. Los dos
son software libre; no son el mismo paquete. Como usamos el comando y no el
proveedor de serie, el verde habla con **tu** Piper MIT.

## El cerebro: lo que hay que vigilar

Configurado como proveedor `custom` contra `http://127.0.0.1:11434/v1` con
`qwen3.5:4b-jarvis`. Ollama ya habla OpenAI, no hace falta puente.

**Pero la propia guía de Hermes avisa de algo que hay que probar.** Su tabla de
modelos locales dice, literalmente, que para trabajo agéntico completo hace
falta `gemma4:31b`, y marca *sin* tool calling a todo lo de 9B para abajo:

| Modelo (su tabla) | Tool calling |
|---|---|
| `gemma4:31b` | Sí |
| `gemma2:27b` | No |
| `gemma2:9b` | No |
| `llama3.2:3b` | No |

Su tabla no lista qwen3.5, que **sí** hace tool calling — el commit
*"nucleo: el cerebro con tool calling nativo"* lo demuestra. Pero lo demuestra
**en tu arnés**, no en el de Hermes, que inyecta muchas más definiciones de
herramientas por vuelta. Así que:

- Es lo primero que hay que comprobar al arrancar: si el 4B llama herramientas
  bien dentro de Hermes o se atraganta.
- Si se atraganta, esta máquina tiene **62 GB de RAM**: un modelo grande cabe,
  aunque iría por CPU (~2-5 tok/s según su guía, 30-120 s por respuesta). Para
  eso está `HERMES_API_TIMEOUT=1800` documentado en `env.ejemplo`.
- `qwen3.5:9b` (6,14 GB) es el escalón intermedio, y solo con el naranja cerrado.

**Aviso de contexto:** si se define `num_ctx` propio en Ollama, hay que poner el
mismo en Hermes. Su `/api/show` reporta el contexto *máximo* del modelo, no el
configurado, así que Hermes puede creer que tiene más sitio del que hay.

## Antes de comparar los dos carriles: no midas peras y manzanas

El naranja tiene **150 acciones rápidas que se saltan el modelo**: "¿cuánto
espacio queda?" es un `df`, no una inferencia. Hermes **no tiene ese atajo** —
allí toda orden pasa por el cerebro, con las herramientas, las skills y la
memoria en contexto en cada vuelta.

El verde va a medir más lento, y **no será culpa del modelo**. Para comparar de
verdad hay que usar solo las frases que hoy *no* encajan en el catálogo y acaban
en el cerebro: son las únicas donde los dos hacen lo mismo. Las hay anotadas en
`config/jarvis-sin-accion.jsonl`.

## Pendiente: dependencias de sistema de Playwright

El instalador pidió `sudo` para las librerías de sistema de Chromium, se declinó
(no hay sudo sin contraseña) y **degradó solo**: instaló el binario en la caché
del usuario. Chromium arrancará si las librerías ya están; si se queja, esto es
lo que hay que correr una vez:

```bash
cd ~/.hermes/hermes-agent && sudo npx playwright install-deps chromium
```

## Qué se descargará el primer día que arranques

Nada de esto ha pasado todavía. Que no sorprenda:

| | Cuándo | Cuánto |
|---|---|---|
| Modelo de whisper `small` | primera transcripción | ~500 MB en disco, 839 MiB de pico en GPU |
| Modelo de Ollama | ninguno — `4b-jarvis` ya está | 0 |
| Voces de Piper | ninguna — usamos las tuyas | 0 |

`stt.local.unload_after_idle_seconds: 300` está puesto justo por los 839 MiB:
suelta whisper tras cinco minutos de silencio y le devuelve el hueco al modelo.
Cuesta una recarga en la frase siguiente.

## Se instaló también el driver de Computer Use

Va en la instalación completa. Es el que permitiría a un agente **controlar
ratón y teclado del escritorio**, no solo el navegador. Instalado no es lo mismo
que activo — hoy no hay nada que lo invoque — pero conviene saber que está antes
de dar permisos a algo. Si se quiere fuera: `--skip-computer-use`.

**Dato que sí importa en esta máquina:** ese driver no viene de Nous. El
instalador lo trae con un `curl | bash` contra **terceros**:

```
https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh
```

O sea que el envoltorio `instalar.sh` audita el instalador de Hermes, pero ese
instalador se descarga y ejecuta otro script de otro repo sin que nadie lo mire.
Con COFRE en esta máquina, merece una lectura antes del próximo `hermes update`.
Está en `~/.hermes/hermes-agent`, y el binario resultante también.

## Las 104 pruebas del naranja: NO se ejecutaron

El plan decía correr `pruebas/ejecutar.sh` para comprobar que el naranja seguía
dando el mismo número. **No se hizo**, y a propósito: esa suite abre la
aplicación y carga el modelo, que es exactamente lo que se pidió no hacer
mientras el naranja estaba en desarrollo.

Queda pendiente para cuando pares de desarrollar:

```bash
cd ~/COFRE/CODERS/JARVIS/pruebas && ./ejecutar.sh
```

En su lugar se comprobó lo que sí se podía en frío: que la gráfica quedó libre y
que el verde no escribió fuera de su sitio. Todo lo suyo vive en `verde/`, en
`~/.hermes/`, más tres ficheros de escritorio (`~/.local/share/icons/jarvis-verde.png`,
`~/.local/share/applications/jarvis-verde.desktop`) y la caché de Playwright.

Los 13 ficheros que `git status` da por modificados son **todos anteriores** a
esta sesión: el más reciente es `config/set_persona.py` a las 13:22 y esto
empezó a las 16:09. Y los ficheros nuevos que aparecen en `nucleo/` después de
esa hora —`arranca.sh`, `estado.py`, `ventana.sh`, `pruebas/prueba_cara.py`…—
son **tu desarrollo del naranja de esta tarde**: no existían cuando se listó esa
carpeta al empezar.

## La duda del 4B: resuelta, y a favor (16 ago)

Era la pregunta abierta del carril — la guía de Hermes marca *sin* tool calling
todo lo de 9B para abajo y recomienda `gemma4:31b` para trabajo agéntico.

**`qwen3.5:4b` llama herramientas bien dentro de Hermes.** Comprobado primero
contra el endpoint pelado (`pruebas/04_tool_calling.sh`, tres casos) y después
en el arnés completo, con las 19 herramientas en contexto:

| Caso | Resultado |
|---|---|
| Pide herramienta cuando hace falta | `terminal {"command": "df -h ."}` |
| Acierta la herramienta y sus **dos** argumentos | `escribe_fichero {"ruta": …, "contenido": …}` |
| **No** llama cuando no hace falta ("¿cuánto son dos más dos?") | contesta `4` en prosa |

El tercero importa tanto como los otros: un modelo que llama siempre es tan
inútil como uno que no llama nunca, y con una sola pregunta no se distinguen.

## El escalón 6: habla, y con la voz del naranja (16 ago)

`text_to_speech` → proveedor `jarvis-piper` → `voz-verde.sh` →
`config/jarvis-piper.sh`. Comparado el WAV que salió de Hermes con el que genera
la prueba `07_voz.sh` llamando al adaptador a pelo:

| | Hermes | Directo |
|---|---|---|
| Frecuencia | 22050 Hz | 22050 Hz |
| Canales | 1 | 1 |
| Bits | 16 | 16 |

Idénticos, que era el objetivo: mismo binario Piper 1.2.0 MIT, mismo `.onnx`,
mismo tono. **La comparación de voz entre los dos carriles vale**, que es para
lo que se montó el adaptador.

De propina, sin que nadie se lo pidiera contestó *"Perfecto, señor"* y *"Todos
mis sistemas están listos para sus órdenes"*. El registro de JARVIS sale del
`SOUL.md` de Hermes, no de la persona del naranja, y aun así coincide.

Queda el juicio que no es medible: si suena bien. El fichero está en
`~/.hermes/cache/audio/`.

## EL ESCALÓN 5 ROMPIÓ LA PREMISA: el agente se fue a internet solo (16 ago)

Esto es lo más serio de la sesión de estreno y hay que leerlo antes que nada.

Se le pidió *"hazme un cartel en arte ascii que ponga JARVIS"* — a propósito sin
nombrar ninguna skill, que es lo que medía el escalón. Lo que hizo:

| # | Llamada | Resultado |
|---|---|---|
| 1 | `skill_view {"name": "ascii-art"}` | **eligió la skill sola** ✓ |
| 2 | `python3 -m pyfiglet "JARVIS" --font slant` | `No module named pyfiglet` |
| 3 | `curl https://asciified.thelicato.io/api/v2/ascii?text=JARVIS` | **salió a internet** |

Nadie le dijo que podía. La skill menciona pyfiglet, pyfiglet no está instalado,
y el modelo resolvió el hueco **buscando el servicio equivalente en la web** y
mandándole el texto a un tercero.

### Por qué no lo cazaba nada

`pruebas/06_privacidad.sh` pasa, y seguirá pasando: comprueba **configuración**
—cero credenciales, cero URLs de nube en el YAML, nada escuchando fuera de
loopback— y esto no es configuración, es **conducta en marcha**. El agente tiene
una terminal y la terminal tiene `curl`. No hay ajuste que lo impidiera.

Es una diferencia de fondo con el naranja, y conviene que quede escrita: el
naranja ejecuta un catálogo cerrado de 150 acciones. El verde **decide qué
ejecutar**, y ese es justo su valor y justo su riesgo. No se puede tener lo uno
sin lo otro.

### Lo que se puede hacer, por orden de menos a más invasivo

1. **Instalar lo que las skills dan por hecho** (`pip install pyfiglet`). Quita
   el motivo, no la capacidad. Es parche, no solución.
2. **Apagar las skills que tiran de red**: `hermes skills disable <nombre>`.
3. **Recortar herramientas** en `platform_toolsets` del `config.yaml`, que ya
   estaba apuntado como palanca para el contexto y sirve para esto también.
4. **Cortar por debajo**: regla de cortafuegos para el proceso, o terminal en
   contenedor sin red. `hermes egress` **no vale** para esto — es iron-proxy,
   un proxy que INYECTA credenciales en las salidas, no un bloqueo.

**No he aplicado ninguna.** Cualquiera de ellas cambia lo que el carril sabe
hacer, y eso es una decisión del usuario, no una corrección técnica.

### Y el cartel salió mal

Aparte de todo lo anterior: el banner que `curl` devolvió era correcto, y al
copiarlo a su respuesta final el modelo **lo destrozó**. Mismo patrón que la
lista de ficheros del escalón 4: el 4B razona bien la cadena y es descuidado
reproduciendo texto literal largo. 4 min 58 s en total.

## El escalón 7 (la escucha) cabe, y por un motivo que sorprende (16 ago)

El plan lo dejaba para el final temiendo un `CUDA error: out of memory`: whisper
`small` pide **839 MiB en el pico** de la transcripción y la tarjeta son 4096.
Medido con el verde cargado y trabajando:

| | MiB |
|---|---|
| Tarjeta | 4096 |
| Ocupado por el modelo verde (65536 ctx, 16 capas) | 2902 |
| **Libre** | **1062** |
| Pico de whisper `small` | 839 |
| Margen | **223** |

**Cabe.** Y lo que sorprende es que el verde deja **más** sitio que el naranja:
2902 MiB contra los 2941 del reparto automático de ollama para
`qwen3.5:4b-jarvis`. Bajar 16 capas a la CPU libera tarjeta, así que el precio
que se paga en velocidad se cobra en VRAM.

Sigue siendo justo —223 MiB— y falta la prueba de verdad, que necesita
micrófono. Pero la aritmética que hacía temer el escalón ya no dice lo que
decía.

## Para probar cuando arranques

- **Tema del panel web.** `dashboard.theme` acepta `default`, `midnight`,
  `ember`, `mono`, `cyberpunk`, `rose`. No he podido ver sus paletas (el panel
  va empaquetado), pero `cyberpunk` o `mono` son los candidatos a pegar con el
  verde. El TUI no tiene tema: ahí el color lo pone `verde-launcher.sh` con
  secuencias OSC.
- **Los escalones 5, 6 y 7** de `ESCALERA.md`: una skill, la voz dentro de
  Hermes y la escucha por micrófono. Los tres piden criterio u oído, o el
  micrófono, así que no se automatizan.
- **Si merece la pena la palanca del KV caché.** `OLLAMA_FLASH_ATTENTION=1` +
  `OLLAMA_KV_CACHE_TYPE=q8_0` partiría el caché por la mitad y devolvería capas
  a la tarjeta, que es justo lo que hoy cuesta el tiempo de respuesta. **No se
  ha aplicado**: es del servicio de ollama y afectaría también al naranja.

## Noche del 16-17 ago: voz de entrada, RAG y frontal a nivel del naranja

Encargo: subir el verde al nivel del naranja en voz, RAG y frontal. Todo local,
nada a gitea (pendiente de revisión).

### La escucha, resuelta y en local (escalón 7 cerrado sin micro)

El TUI de Hermes no tiene micrófono, y su STT nativo (`stt:` del config) es para
notas de voz de **mensajería** — saldrían de la máquina. Así que se puentea:

- `verde/escucha_transcribe.py` — WAV → texto con **faster-whisper `small`**
  (el mismo que declara el config), en **CPU** a propósito: el 4B ocupa la
  tarjeta y el margen es de 223 MiB; whisper en CPU tarda ~12 s por frase y no
  arriesga OOM. No llama a ninguna API.
- `verde/escucha-verde.sh` — pulsar-para-hablar: graba (pw-record/arecord/parec)
  → transcribe → `hermes chat --reasoning none -q` → responde; `--habla` lo dice
  con la voz del verde, `--seguido` encadena turnos.
- `verde/pruebas/08_escucha.sh` — **8/8**. Bucle TTS→STT sin micro: genera voz
  con el `07`, la transcribe y comprueba que vuelven las palabras clave. Mide y
  verifica que el transcriptor no referencia APIs remotas.

Medido: "hola jarvis verde cuanto espacio libre queda en el disco" → *"¿Cuánto
espacio libre queda en el disco?"* (solo pierde "jarvis"→"harvis", nombre raro).

Latencia honesta: la ESCUCHA va bien (~12 s CPU). El cuello es el 4B (minutos
por turno). El puente sirve para PROBAR que el verde oye, no como diario.

### RAG del naranja, disponible en el verde

El agente ya puede consultar el mismo índice que el núcleo (14.949 entradas):

- `nucleo/saber/busca_cli.py` — CLI sobre `saber.busca` (texto o `--json`).
- Skill local `~/.hermes/skills/research/buscar-en-apuntes/` — le dice al agente
  cuándo y cómo buscar (herramienta de terminal, local). `hermes skills list`:
  `buscar-en-apuntes · research · local · enabled`.

### Skills de ECC (el ganador del hackathon de Claude)

Instaladas 4 por `hermes skills install` (escaneadas SAFE): `security-review`,
`git-workflow`, `python-testing`, `coding-standards`. Total 82 skills. El prompt
fijo apenas sube (29 KB, lejos de 64k). Probada `security-review` con el 4B:
checklist correcto, 4 min 30 s en frío.

## Noche: expansión del RAG + reentrenamiento (auto mode, local, sin gitea)

Contenido añadido al índice PERSONAL (nunca al público sin revisar licencias):
- **Strix** (usestrix/strix, agente IA de pentesting) clonado en CODERS; sus docs
  (README, docs/, AGENTS, benchmarks) indexados → 49 fragmentos.
- **6 awesome lists** más: sysadmin, cli-apps, security, pentest, shell, linux
  (catálogo pasa de 385 a 740 fragmentos). En awesome.py, publico=False.
- **Apps instaladas** de esta máquina: nucleo/saber/instalado.py lee los .desktop
  (146 apps) + apt-mark showmanual (22 fragmentos). Personal por definición.
- Índice personal: ~49 MB. Reindex raw hecho; contenido recuperable ya.

Reentrenamiento nocturno: nucleo/saber/enriquece/reentrena_noche.sh encadena
síntesis de ganchos del contenido nuevo → reindex → eval → refresco COFRE
(cosecha+sintetiza) → reindex final. Corre detached, log en datos/reentrena_noche.log.

Pendiente de revisar por la mañana: licencias de las 6 awesome nuevas antes de
subir nada; y decidir qué de todo esto va al índice público / gitea.

## Cierre de la noche (07:52) — números y decisiones

Dos rondas de reentrenamiento. Índice final: **64 MB, 19.578 entradas** (12.260
preguntas-gancho, 1.383 catálogo, 168 apps, 459 metodología incl. strix).

Retrieval (eval_set ampliado a 57 casos = 45 pentest + 12 amplitud):
- Ronda 1: hit@1 77%, hit@5 96%
- Ronda 2 (+osint/devops/docker/go/python): hit@1 **75%**, hit@5 **96%**
- Amplitud sola (apps/self-hosted/cli/linux/strix): hit@1 75%, hit@5 **100%**

**Intercambio medido:** pentest puro pasó de 82% (antes de la noche) a ~75% de
hit@1; hit@5 se mantiene 95-96%. Es el precio de la amplitud. Las listas gigantes
(awesome-go, 3197 items → 322 fragmentos; osint con algún artefacto de TOC) son
las que más ruido meten y menos aporta a consultas por voz.

**Decisiones para la mañana:**
1. Licencias de las 12 awesome lists antes de publicar nada (ahora todas
   publico=False = solo índice personal).
2. Si se prioriza precisión pentest: quitar awesome-go (y quizá awesome-python)
   recuperaría 1-2 pts de hit@1 sin perder casi cobertura útil.
3. Qué del contenido nuevo va al índice público / gitea (hoy: nada subido).
