# 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ó (`/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 `. 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).