---
title: Limitaciones conocidas
icon: phosphor-duotone:warning
summary: Los vacíos honestos - qué está probado contra una aplicación real en ejecución y qué no.
description: Los vacíos honestos - qué está probado contra una aplicación real en ejecución y qué no.
tags: [reference, limitations]
---

# Limitaciones conocidas

BxAgents está en desarrollo activo. Esta página rastrea los vacíos honestos - qué está probado contra una aplicación real en ejecución, qué todavía solo corre contra el proveedor `"mock"` de bx-ai, y las peculiaridades reales ascendentes con las que este proyecto se topó.

## Las pruebas corren solo contra el proveedor `mock`

Cada spec de vía rápida (pipeline de build, generadores, verbos de CLI) y la [suite de integración de ColdBox](#real-coldbox-integration-testing) ejercitan el proveedor `"mock"` incorporado de bx-ai - nunca una llamada de red real a un LLM. Esto es deliberado (rápido, gratis, CI determinista), pero significa que ninguna prueba automatizada actualmente demuestra que un proveedor real (OpenAI, Anthropic, etc.) realmente funciona correctamente de extremo a extremo. Haz al menos una ejecución manual de `chat`/`serve` contra un proveedor real y una clave de API desechable antes de depender de esto en producción.

## Real ColdBox integration testing

`./gradlew testColdBoxIntegration` arranca un proceso **real** de `boxlang-miniserver` contra el propio `Application.bx`/`Bootstrap` de una app generada, y hace un request HTTP genuino a través de una ruta registrada con `toAi()`. Esta es la prueba más sólida de la suite - detectó tres bugs reales en `ColdBoxAppGenerator.bx` durante el desarrollo (un `extends` faltante en un `Binder` de WireBox, un orden incorrecto de argumentos del constructor de `Bootstrap`, y una llamada simple a `getInstance()` que no existe en `Binder`).

### La condición de carrera del primer request de `toAi()`

El **primer** request HTTP a la ruta `toAi()` de una app recién arrancada puede fallar transitoriamente con "Function [getInstance] not found" - una condición de carrera genuina de inyección perezosa de ColdBox/WireBox en el propio delegado `getInstance` del Router, no un bug de BxAgents. Tiene éxito de forma confiable en cada request posterior a que algo más ya haya forzado a WireBox a construir el singleton `GeneratedAgent` una vez (un health check, una sesión de `chat`, otra ruta). **Envía un request de calentamiento** antes de depender de una ruta `toAi()` recién desplegada bajo carga.

## Tres verificaciones de integración - cerradas por una ruta distinta a la planeada originalmente

`tests/specs/integration/RuntimeStartupSmokeSpec.bx` todavía tiene tres `xit()`, ya que este archivo se ejecuta vía el corredor de CLI (`runTests.bxs`/`testBx`), y el modo CLI de BoxLang nunca da un scope `cgi` - que el RoutingService de ColdBox necesita incluso para cargar el router en el arranque. Eso es un hecho estructural sobre el modo CLI, no un vacío: las tres verificaciones ahora están probadas de verdad en otro lugar, en `tests/specs/integration/coldbox/ColdBoxRuntimeSpec.bx` (que se ejecuta dentro de un request HTTP real servido por un proceso real de `boxlang-miniserver`):

- Un request HTTP enrutado por ColdBox real llegando al agente generado de extremo a extremo - probado por `ColdBoxRuntimeSpec.bx` más la propia aserción `POST /api/chat/invoke` de `runColdBoxIntegrationTests.bxs`.
- `schedules/*` realmente registrándose con un `Scheduler` de ColdBox **en vivo** - probado vía `SchedulerService.getSchedulers()["appScheduler@coldbox"].hasTask(...)` contra un arranque real, sin esperar a que se dispare un cron real (la granularidad más gruesa soportada es 1 minuto, lo que gravaría cada ejecución de CI por una prueba adicional marginal una vez que la tarea se confirma en vivo y registrada).
- `chat` y una ruta HTTP servida (`serve`) nunca divergiendo - corregido de su redacción original (`chat` deliberadamente nunca arranca WireBox en absoluto, así que nunca puede compartir el *objeto* singleton de WireBox por diseño) a lo que realmente importa: instanciar `GeneratedAgentFactory` fuera de WireBox produce un agente conductualmente equivalente al propio singleton de WireBox.

## Corregido: las pruebas de CLI de proceso de SO real detectaron un bug real de resolución de módulos

`ModuleCliProcessTest.java` genera procesos hijo genuinos de `java -jar <boxlang-jar> module:bxagents <verb> ...` contra una copia de la estructura real del módulo instalable (`build/modules/bxagents`), apuntado a un `modulesDirectory` real - exactamente como una instalación real de BoxLang carga este módulo. Cada otro spec de CLI llama a `ModuleConfig.main()`/al `run()` de cada verbo en proceso en su lugar, lo cual es más rápido pero nunca prueba que el módulo se resuelva correctamente una vez genuinamente instalado. Esta prueba detectó a todo verbo de CLI fallando con "class not located" en el momento en que se ejecutó a través de un proceso de módulo genuinamente instalado, porque las referencias cruzadas internas usaban una ruta con puntos simple `bxagents.models....` que solo se resolvía gracias al propio `boxlang.json` de dev/test de este repositorio (un mapeo `/bxagents` declarado a mano) - un módulo genuinamente instalado nunca obtiene ese mapeo. Corregido cambiando las referencias internas de cada clase anidada a la forma de sufijo relativo al módulo `Class@bxagents` (ver el docblock de `init()` de `BuildPipeline.bx` para la explicación completa); la salida de estructura de módulo de `build.gradle` se movió de `build/module` a `build/modules/bxagents` (el nombre de la carpeta debe ser igual al nombre del módulo para que el descubrimiento de `modulesDirectory` lo encuentre en absoluto), y el `boxlang.json` de dev/test también lo carga ahora como un módulo real.

## Corregido: un conjunto de bugs reales encontrados al construir el framework de pruebas

Construir el matcher `toHaveCalledTool` de `BaseAgentSpec` y el arnés de integración de ColdBox sacó a la luz - y corrigió - una serie de bugs reales, no detectados previamente. Registrados aquí como un changelog de lecciones aprendidas, no como vacíos abiertos:

- **Los agentes nunca recibían sus propias tools.** La llamada `aiAgent()` generada por `ColdBoxAppGenerator` nunca pasaba en absoluto un argumento `tools:` - confirmado contra el código fuente real de bx-ai (`AiAgent.bx` nunca referencia `aiToolRegistry()` internamente). Ningún agente que BxAgents haya construido jamás, en ningún contexto, realmente recibió sus tools declaradas; pasó desapercibido porque ninguna prueba afirmaba sobre una invocación real de tool, solo sobre una respuesta no vacía. Corregido en `ColdBoxAppGenerator.renderAgentFactory()`: cada `GeneratedAgentFactory.bx` generado ahora carga su propio directorio `tools/` vía `ToolRegistryLoader.bx` (una ruta absoluta, no una relativa - la resolución relativa de `aiToolRegistry().scan("tools")` falla silenciosamente desde un contexto cargado por `DynamicClassLoader` como `chat`), luego pasa `tools: aiToolRegistry().getAll()` a cada llamada de `aiAgent()`.
- **`var` no puede usarse en el nivel superior de un script `.bxs`.** Declara en el scope `local`, que solo existe dentro de una función, así que un `var x = ...` de nivel superior lanza `Scope [local] is not available in this context` en tiempo de ejecución, no en tiempo de análisis - sobrevive la revisión y solo se dispara en el camino de código que lo alcanza. Nunca escribas `var` fuera de una función en un `.bxs`, y envuelve la salida de diagnóstico en su propio try/catch para que nunca pueda enmascarar el fallo que está describiendo.
- **El ajuste de aplicación para un datasource por defecto es `this.datasource`, NO `this.defaultDatasource`.** Este último se acepta silenciosamente y no hace nada - `queryExecute()` todavía falla con `No default datasource defined...`, nombrando el datasource registrado que estás intentando seleccionar, lo cual se lee como si el mecanismo de selección estuviera roto en lugar de que el ajuste esté mal escrito.
- **El `moduleSettings.qb.defaultOptions` de qb no llegaba a `QueryBuilder` en un arranque real de ColdBox**, aunque el propio cableado de `onLoad()` de qb se lee como correcto - cada query fallaba con "no default datasource" a pesar de que el datasource estaba registrado. `ChatDb.query()` ahora llama a `.mergeDefaultOptions( { datasource : static.DATASOURCE } )` en cada builder explícitamente en lugar de depender de ese cableado.
- **Una app generada ahora también declara `this.defaultDatasource`**, así que cualquier cosa que ejecute una query sin nombrar uno (ColdBox mismo, otro módulo) alcanza el único datasource en lugar de fallar. `ColdBoxAppGeneratorSpec` cubre tanto el caso con web UI como el caso sin web UI.
- **El `models/ChatDb.bx` generado no compilaba, y ningún spec unitario lo notó.** Un bug de comillas de literal de cadena de BoxLang (`?: ""` emitido como `?: "`, abriendo una cadena sin terminar) se enviaba en cada proyecto de web UI generado; cada caso de `WebUiGeneratorSpec` solo hacía grep del texto generado, nunca intentaba compilarlo. Ahora hay un caso que afirma que cada línea generada tiene un número par de comillas dobles - la lección más amplia siendo que un spec de generador que solo hace grep de su propia salida no prueba que la salida sea código válido.
- **`GET /chat/api/health` prueba mucho menos de lo que afirmaba un comentario obsoleto.** La acción `health()` generada renderiza un literal 200 y no toca nada más - no dice nada sobre WireBox, `ChatDb` o SQLite. Un comentario en `runColdBoxIntegrationTests.bxs` solía afirmar lo contrario; corregido en el lugar.
- **El `JSONReporter` de TestBox producía un StackOverflow en specs que sostenían singletons de framework en vivo** (objetos reales de `controller`/WireBox/qb reflexionados recursivamente durante la serialización) - una suite que pasaba se reportaba como una ejecución fallida. `tests/runner-coldbox.bxm` ahora ensambla su propio reporte vía un helper `toScalar()` en su lugar, y registra los conteos de aprobado/fallido antes de que se construya el reporte.
- **En una máscara de tiempo de BoxLang, `nn` son NANOSEGUNDOS, no minutos - usa `mm`.** (Nótese la trampa inversa en una máscara de *fecha*, donde `mm` es el mes.)
- **Dos acciones de `handlers/ChatUi.bx` tenían un agujero de autorización a pesar de derivar identidad correctamente en otras partes.** `/pending` y `/resume` se direccionaban solo por `threadId`, así que un visitante con el `threadId` de otra persona podía leer sus tool calls pendientes y responder sus aprobaciones - derivar identidad del lado del servidor da atribución, no control de acceso. Ambas ahora comparan al llamador contra el `userId` que el agente checkpointeó en las opciones de la ejecución.
- **La comprobación de ruta de `ProjectValidator` no detectaba el traversal `..`**, atrapando solo un separador inicial - `../../var/lib/chat.db` pasaba limpiamente. Ahora se rechaza en ambos casos, coincidiendo segmentos de ruta completos para que nombres legítimos como `..hidden` sigan pasando.
- **`chr()` no existe en BoxLang - el BIF es `char()`.** Costó varios ciclos de CI porque el fallo es uno de tiempo de ejecución que sobrevive la revisión (`tests/runner-coldbox.bxm` lo llamaba en su primer marcador de progreso, así que cada request devolvía 500 al entrar).
- **La instrumentación que un reintento puede sobrescribir no registra nada.** Un archivo de progreso se truncaba en cada reintento, ocultando la evidencia real y convirtiendo un simple 500 en un opaco "HTTP 408, never responded"; los reintentos mismos también eran activamente dañinos, apilando ejecuciones completas de pruebas concurrentes sobre un pool de trabajadores pequeño. Corregido: un solo request (la sonda de health check antes de él ya prueba que el servidor está arriba), registro de progreso de solo-adición etiquetado por request.
- **`request` es un nombre de scope incorporado reservado.** Una variable de bucle llamada `request` lo sombrea silenciosamente sin ningún error en absoluto - corregido renombrando a `recordedRequest` en los matchers de `BaseAgentSpec.bx`.

## La búsqueda del miniserver de `serve` es solo por PATH

`serve` busca `boxlang-miniserver` solo en `PATH` (`MiniServerLauncher.findExecutable()`). No hay respaldo a una ruta configurada o a un binario incluido - si no está instalado y en `PATH`, `serve` falla con un error claro y accionable, pero no hay forma alternativa de apuntarlo a uno. `invoke --server` reutiliza `serve` internamente, así que hereda la misma búsqueda solo por PATH - su propia prueba de `InvokeSpec.bx` para el round-trip HTTP real comprueba `MiniServerLauncher.findExecutable()` primero y se omite (en lugar de fallar) cuando no hay ningún binario real en `PATH`, el mismo idioma que `RuntimeStartupSmokeSpec.bx` ya usa para su propia comprobación de presencia del jar. Haz al menos una ejecución manual de `bxAgents invoke --message=... --server` en una máquina con `boxlang-miniserver` instalado antes de depender del camino HTTP real en producción - el mismo enfoque honesto ya usado en otro lugar de esta página para vacíos que esta suite no puede cerrar por sí misma en cada entorno.

## El home de runtime de BoxLang delimitado alcanza a `serve`/`invoke --server` incondicionalmente; los verbos en proceso solo si tu instalación de BoxLang carga `.env`

`serve` delimita el propio home de runtime de BoxLang del miniserver a `.build/runtime` vía `serverHome` (un campo real y confirmado de `MiniServerConfig` - el propio texto de ayuda de CLI de `MiniServer` de `boxlang-web`: `-s, --serverHome <PATH>  BoxLang server home directory (default: ~/.boxlang)`), así que la caché de clases compiladas de cada proyecto y cualquier override de configuración están aislados por proyecto en lugar de compartidos globalmente en `~/.boxlang`. `invoke --server` hereda esto ya que reutiliza `serve` internamente. Esta parte es incondicional - nosotros mismos escribimos la configuración de lanzamiento para ese proceso.

`chat`, `build`, `test`, e `invoke` en proceso por defecto son diferentes: se ejecutan dentro del proceso `bxAgents` ya iniciado, cuyo propio runtime de BoxLang - y por lo tanto su home - se resolvió antes de que cualquiera de nuestro código de BoxLang, incluyendo el propio `main()` de `ModuleConfig.bx`, tuviera oportunidad de ejecutarse (el motor tiene que existir para interpretar ese código en absoluto). `BoxRuntime` es un singleton a nivel de JVM; su home está fijado en la primera inicialización, así que nada que una clase de verbo haga desde dentro de ese proceso ya en ejecución puede cambiarlo retroactivamente.

SÍ hay una palanca real para esto, sin embargo: `BoxRunner` (el propio punto de entrada de CLI de núcleo de BoxLang, no solo el miniserver) lee una variable de entorno `BOXLANG_HOME` genuina antes de que `BoxRuntime` se inicialice - confirmado directamente contra el jar de runtime real (no solo la documentación): ejecutarlo con `BOXLANG_HOME=<path>` configurado como una variable de entorno de SO real (las rutas relativas se resuelven contra el CWD, igual que una ruta absoluta) puebla la estructura de home completa en esa ruta en lugar de `~/.boxlang`, cada vez. `new` genera un `.env` que declara `BOXLANG_HOME=.build/runtime` - la misma ruta que usa `serve` - exactamente por esta razón (reflejando la propia convención `.env`/`BOXLANG_HOME` de `ortus-boxlang/bx-ai-intro`), en la **raíz del proyecto** - es decir, el directorio desde el que un usuario realmente ejecuta `bxAgents <verb>` para los verbos en proceso, que es el lugar correcto para que un cargador de `.env` basado en CWD lo encuentre.

**Lo que confirmamos, y lo que no pudimos.** `boxlang-miniserver` (lo que lanza `serve`) sí tiene carga automática de `.env` real e incorporada - confirmado descompilando y luego realmente ejecutando `ortus.boxlang.web.MiniServer` directamente: con ningún `envFile` configurado, resuelve `.env` en relación a el **webRoot** del servidor (no la raíz del proyecto) y, si se encuentra, lo carga como Java `Properties`, aplicando cada clave vía `System.setProperty()` - los valores reales cargados de esta manera SÍ son visibles para el código de BoxLang vía `getSystemSetting()`. El jar de runtime de núcleo crudo (`BoxRunner`, usado por `chat`/`build`/`test`/`invoke` por defecto) no tiene lógica equivalente en ninguna parte de él (confirmado haciendo grep en cada clase del jar en busca de `.env`) - así que esos verbos solo captan `.env` si la propia CLI `boxlang` que tienes instalada (un lanzador nativo provisto por BVM, no este jar crudo) hace su propia carga de `.env` antes de que la JVM arranque, de la manera en que `ortus-boxlang/bx-ai-intro` depende de ello. Eso es plausible y consistente con el uso en el mundo real de ese proyecto, pero no es algo que este sandbox tenga el binario real para verificar directamente.

Una peculiaridad específica y confirmada incluso donde sí ocurre la carga de `.env`: **`BOXLANG_HOME` mismo no tiene efecto a menos que sea una variable de entorno de SO real** - no una propiedad de sistema de la JVM, y no un flag `-D`. Verificado dos veces directamente contra los jars reales: (1) ejecutar `boxlang-miniserver` contra un webRoot cuyo `.env` declaraba `BOXLANG_HOME=customhome` cargó el archivo (confirmado vía su propia línea de log "Loaded environment variables from:" y vía `getSystemSetting()` devolviendo correctamente otros valores de `.env`), sin embargo el servidor aún registraba `Logs Directory: /root/.boxlang/logs` - el home por defecto, no `customhome`; (2) lanzar `BoxRunner` directamente con `-DBOXLANG_HOME=<path>` (una propiedad de sistema de la JVM, sin `.env` involucrado) tampoco tuvo efecto en el home resuelto, aunque `getSystemSetting( "BOXLANG_HOME" )` devolvía felizmente el valor del flag desde código BoxLang. Así que la resolución de `BOXLANG_HOME` específicamente solo lee la variable de entorno de SO verdadera - a diferencia de la mayoría de los ajustes, que `getSystemSetting()` devuelva un valor para ella no significa que el home del runtime realmente se haya movido. Si tu CLI de `boxlang` carga `.env` de la misma manera en que lo hace internamente MiniServer (`System.setProperty` después de que la JVM ya ha arrancado) en lugar de exportar una variable de entorno real antes de lanzar la JVM, `BOXLANG_HOME` en `.env` no alcanzará el home del runtime aunque `.env` mismo se cargue con éxito - todo lo demás en él seguirá funcionando. Si tu instalación no te da esto en absoluto, cárgalo tú mismo antes de ejecutar comandos (por ejemplo, `set -a; source .env; set +a`) para obtener el mismo aislamiento que `serve` ya obtiene incondicionalmente.

## El paso de conveniencia `box install` de `new` no está ejercitado por una prueba automatizada

`new` ejecuta `box install` dentro de la carpeta `tests/` generada por defecto, así que `bxAgents test` funciona de inmediato (ver [Referencia de CLI](cli-reference.md)). Esta es una operación de red real (CommandBox resuelve `testbox` contra ForgeBox) - confirmada de tomar ~25 segundos y fallar con un error de certificado en este sandbox de desarrollo específicamente, la misma restricción de ForgeBox-inalcanzable ya señalada en otro lugar del propio tooling de este proyecto. `NewSpec.bx` solo ejercita el camino rápido y determinista `--skipInstall` (y las invocaciones de `new` de `ModuleCliProcessTest.java` de proceso de SO / `ModuleConfigCliSpec.bx` en proceso también pasan `--skipInstall` por la misma razón) - el propio camino de intento de instalación por defecto se verifica solo por pruebas manuales, no por CI.

## `chat` necesita una TTY real

`chat` usa el propio `MiniConsole` de BoxLang, que invoca `stty` para configurar el modo raw de terminal - solo puede ejecutarse contra una terminal interactiva genuina. No funciona canalizado, redirigido, ni desde un proceso no interactivo (un job de CI, un script). No hay modo de respaldo no interactivo.

## Corregido: `chat` e `invoke` por defecto (sin `--server`) solían fallar para un `Agent.bx` basado en clase

Anteriormente, tanto `chat` como `invoke` por defecto lanzaban `The requested class [agent.classes.agentClass] has not been located in any class resolver.` antes de siquiera alcanzar al agente. Causa raíz: ambos verbos cargan el `GeneratedAgentFactory.bx` generado en proceso vía `DynamicClassLoader.instantiate()` (sin contenedor ColdBox involucrado), y la factory generada solía instanciar un `Agent.bx` basado en clase vía una búsqueda de ruta con puntos **relativa**, que solo se resuelve una vez que algo ha registrado un mapeo que hace resoluble la raíz de la app - y nada lo hacía fuera de un arranque real de ColdBox. Registrar uno a mitad de script tampoco lo corrige (confirmado empíricamente).

**Corrección:** `ColdBoxAppGenerator.copyAgentClass()` ahora devuelve la propia ruta de archivo absoluta de la clase copiada, y `renderClassBasedAgentStatement()` la instancia vía `DynamicClassLoader.instantiate( absolutePath, context )` - la misma primitiva que `chat`/`invoke` ya usan para cargar `GeneratedAgentFactory.bx` mismo - en lugar de un `new "..."()` relativo. Confirmado contra `examples/class-based-agent/`: `chat`, `invoke` por defecto, `invoke --server`, y `serve` todos construyen y ejecutan correctamente el mismo agente.

## Ninguna prueba de humo de instalación del `executable` de box.json

`box.json` declara `"boxlang": { "executable": "bxAgents" }` así que una instalación real de módulo produce un comando `bxAgents` nativo (ver [Instalación](getting-started/installation.md)). Este cableado en sí no está ejercitado por una prueba automatizada - depende del propio comportamiento documentado del instalador de módulos de BoxLang para generar wrappers ejecutables, verificado leyendo su código fuente, no por una prueba de instalar-y-ejecutar en el propio CI de este repositorio.

## `schedules/Scheduler.bx` no se valida en tiempo de build

Ya que es código real, escrito a mano, de ColdBox pasado sin tocar (ver [schedules/](conventions/schedules.md)), `build` no puede comprobarlo de manera significativa de la forma en que solía comprobarse la configuración `{ cron, action }` - un error de sintaxis, un typo en una llamada `getInstance( "..." )`, o referenciar un nombre de agente que no existe, todo pasa `build` limpiamente y solo sale a la superficie cuando la app generada realmente arranca (`serve`), igual que cualquier otra clase real de BoxLang cuyo contenido este proyecto no posee. `build` sí atrapa un error más estrecho y adyacente: dos agentes (raíz o subagente, a cualquier profundidad) declarando el mismo `name` - eso es una colisión de binding de `config/WireBox.bx` que este proyecto genera él mismo, así que se comprueba en tiempo de validación igual que todo lo demás que genera.

## Corregido: un bug real en el verbo `test` - resolver "el jar actual de BoxLang" era ambiguo

`TestRunnerService.bx` genera un proceso hijo para ejecutar los `tests/specs` de un proyecto, y necesita saber con qué jar lanzarlo. El truco original de "de qué jar se cargó esta clase" se rompió una vez que `boxlang-miniserver-*.jar` (un fat jar que empaqueta su propia copia de `BoxRuntime`) también estaba en el classpath - el classloader podía resolver al jar del miniserver en su lugar, lanzando silenciosamente `MiniServer.main` (que sale con 1 inmediatamente) en lugar de `BoxRunner.main`. Reproducible solo a través de la suite completa `testBx`, no de forma aislada, lo cual es lo que hizo fácil pasarlo por alto.

Corregido resolviendo el jar desde `java.class.path` en su lugar - escaneando en busca de una entrada `boxlang-*.jar` que **no** contenga `miniserver`, recayendo en el viejo truco de codeSource solo si no se encuentra tal entrada.

## Los destinos `ssh`/`docker`/`digitalocean` de `deploy` no están ejercitados por una prueba automatizada de proceso real

`SshTargetSpec.bx`/`DockerTargetSpec.bx` afirman sobre el comando `scp`/`ssh`/`docker` exacto que construye cada destino (un array de argumentos real de `ProcessBuilder`) sin invocar jamás el binario real - el mismo enfoque de "capturar, no ejecutar" usado en otra parte de esta suite para cualquier cosa que necesite un binario que podría no estar en `PATH` en CI (ver los saltos de `assumeTrue` de `MiniServerLauncherTest`). La lógica de copia de `local` SÍ está ejercitada de verdad (`LocalTargetSpec.bx`), incluyendo una prueba de regresión para un bug latente real que este refactor corrigió: el `Deploy.bx` original elegía el `.bxa` "más nuevo" vía un ordenamiento léxico de nombre de archivo, que silenciosamente elige mal una vez que un proyecto alcanza versiones de dos dígitos (`v9.0.0` ordena después de `v10.0.0`) - corregido ordenando por el tiempo de modificación de archivo real (`DistArtifactLocator`).

`DigitalOceanTargetSpec.bx` de forma similar solo hace pruebas unitarias de la lógica pura de `buildAppSpec()`/`findExistingAppId()` - las llamadas reales `GET/POST /v2/apps` nunca se ejercitan en CI, ya que eso necesita una cuenta real de DigitalOcean y un token de API. Haz al menos un `deploy --name=<ssh-entry>` manual contra una VM desechable real, y un `deploy --name=<digitalocean-entry>` contra una cuenta real de DO con una app desechable, antes de depender de ninguno de los dos en producción - el mismo enfoque honesto ya usado arriba para el vacío de pruebas solo-con-proveedor-mock.

## Los destinos `ftp`/`sftp` de `deploy`: el manejo de conexión real está probado, una carga exitosa real no

A diferencia de `ssh`/`docker` (que invocan binarios externos y así solo pueden tener su *construcción de comando* probada sin un servidor real), `ftp`/`sftp` llaman al componente `bx:ftp` real del módulo [`bx-ftp`](https://github.com/ortus-boxlang/bx-ftp) **en proceso** - no hay opción de "capturar el comando, no ejecutar". `BaseFtpTargetSpec.bx` en su lugar hace un intento de conexión genuino contra `127.0.0.1` en un puerto en el que nada escucha, y afirma que el error real de conexión rechazada se captura y se relanza como un `BxAgents.DeployFailed` claro (confirmado vía el propio código fuente de bx-ftp que cada acción lanza en caso de fallo en lugar de devolver un `succeeded: false` suave). Esto prueba el camino real de conectar/envolver-error/limpiar de extremo a extremo - lo que no puede probar es una carga exitosa real.

Ese vacío es específicamente porque **este sandbox de desarrollo no tiene salida TCP cruda alguna** - solo HTTPS a través del proxy del entorno (confirmado directamente: tanto `curl ftp://test.rebex.net` como una conexión cruda `/dev/tcp` a un host FTP público colgaron y expiraron por tiempo de espera, y `docker info` no muestra ningún daemon corriendo, así que los propios servidores de prueba FTP/SFTP de Docker que trae bx-ftp tampoco pudieron iniciarse aquí). Esta es una restricción del sandbox, no una limitación de código - haz al menos un `deploy --name=<ftp-entry>` y un `deploy --name=<sftp-entry>` manuales contra un servidor real alcanzable (o los propios servidores de prueba `docker-compose up` de bx-ftp, desde una máquina que tenga acceso a Docker/red) antes de depender de ninguno de los dos en producción, el mismo enfoque honesto ya usado arriba para `ssh`/`digitalocean`.

## `build` no revierte un `.build/app` parcialmente escrito ante un fallo a mitad de generación

`BuildPipeline.build()` elimina y recrea `.build/app` de antemano, luego ejecuta los generadores de la Fase 5 en secuencia. Cada entrada que la Fase 3 (`ProjectValidator`) puede comprobar se comprueba antes de que nada de eso ocurra, así que un fallo genuino a mitad de generación debería ser raro en la práctica - pero si aun así ocurre uno (por ejemplo, una entrada de `models/`/`schedules/`/`mcp/` que falla al cargar, o un problema de entorno/sistema de archivos), `.build/app` queda en disco en un estado parcialmente escrito en lugar de restaurarse a su contenido previo o limpiarse. Un `build` exitoso posterior lo sobrescribe limpiamente, así que esto no es pegajoso, pero cualquier cosa que inspeccione `.build/app` entre un build fallido y el siguiente (un paso de CI, un reintento manual de `package`) puede ver una app rota, medio generada. Todavía no hay un paso de rollback/directorio-temporal-luego-intercambio.

## Se observó un `StackOverflowError` intermitente en `testBx`, no relacionado con el propio código de este proyecto

Mientras se investigaba este hito, `./gradlew testBx` ocasionalmente (no en cada ejecución) estrellaba toda la JVM con un `StackOverflowError` dentro de la propia serialización JSON genérica de objetos del motor de BoxLang (`DynamicObjectSerializer`/`BoxStructSerializer` llamándose mutuamente, alternando, hasta que se agota la pila - confirmado que sigue ocurriendo con `-Xss16m`, así que es un ciclo genuino, no meramente una estructura profunda pero finita). Aislado vía `git stash` (el fallo se reprodujo idénticamente sin ninguno de los cambios de esta sesión aplicados, contra el mismo commit exacto ya en `development`) y vía bisección de `tests/specs/**` en cada subdirectorio tanto individualmente como combinados (cada subconjunto, e incluso cada subconjunto-menos-uno, corrió limpio) - solo la única ejecución completa lo reproduce ocasionalmente, y volver a ejecutar la misma suite completa inmediatamente después de un fallo a veces pasa limpio. Esto apunta a una condición de carrera dependiente del tiempo (interactuando plausiblemente con la propia tarea `everySecond()` en segundo plano real de `ExampleScheduler`, que imprime desde su propio pool de hilos concurrentemente con cualquier spec que esté ejecutándose en ese momento) más que a un bug en algún spec particular o en el propio código generado de este proyecto. Si `testBx` falla con un `StackOverflowError` y ninguna otra explicación, reinténtalo antes de asumir una regresión real - actualmente no es reproducible a voluntad, así que no existe ninguna prueba de regresión automatizada para ello, y llevarlo hasta una única causa raíz estaba fuera del alcance de esta ronda.

## Gateways de estilo push (Telegram) probados solo contra un seam mockeado de API/scheduler - ninguna integración de plataforma en vivo corre en CI

`TelegramGatewaySpec.bx` ejercita la propia lógica de `TelegramGateway` (normalización entrante, fragmentación saliente en el límite de 4096 caracteres, construcción de teclado inline HITL, registro/eliminación de tarea de scheduler) enteramente contra un seam de prueba inyectable `apiCaller`/`setScheduler()` - nunca una llamada real a la API de bots de Telegram, y nunca un arranque real de scheduler de ColdBox. Esto prueba que el propio código del gateway es correcto, no que realmente funciona de extremo a extremo contra la API real de Telegram o un scheduler real en ejecución. Haz al menos un `bxAgents serve` manual contra un proyecto con un bot de Telegram real respaldado por `botTokenEnvVar` antes de depender de él en producción - el mismo enfoque honesto ya usado arriba para los vacíos de pruebas solo-con-proveedor-mock/sin-conexión-en-vivo en otras partes de este archivo. La misma advertencia se aplicará a cada futuro gateway de estilo push (Slack, Discord, Email, WhatsApp) construido de la misma manera.

`SlackGatewaySpec.bx` lleva el mismo vacío, un nivel más profundo: la conexión websocket persistente de `SlackGateway` se prueba vía un seam inyectable `setSocketOpener()` (un objeto falso que sustituye al `java.net.http.WebSocket` real), así que cada aserción de manejo de frame/lógica de reconexión en el spec corre con cero E/S de red real. Lo que sí se verificó genuinamente de forma directa (no mockeado): una prueba de humo independiente instanció el `SlackSocketListener(gateway)` real (que `implements="java:java.net.http.WebSocket$Listener"` directamente - BoxLang lo compila como un implementador de JVM genuino, sin necesidad de proxy) y llamó al `HttpClient.newWebSocketBuilder().buildAsync(...)` real contra una dirección inalcanzable, confirmando que la propia interoperabilidad de BoxLang a Java funciona correctamente hasta la frontera de red (falló con un simple `java.net.ConnectException`, no un error de casting/interoperabilidad) - pero ninguna prueba aquí ha completado jamás un handshake real de Socket Mode contra los servidores reales de Slack. Haz al menos un `bxAgents serve` manual contra un proyecto con credenciales reales de app de Slack respaldadas por `botTokenEnvVar`/`appTokenEnvVar` antes de depender de él en producción.

`DiscordGatewaySpec.bx` lleva el vacío idéntico, por la razón idéntica: la lógica de manejo de frame/latido/reconexión de `DiscordGateway` se ejercita enteramente contra seams inyectables `setApiCaller()`/`setSocketOpener()`, cero E/S de red real. La misma disciplina de prueba de humo independiente se aplicó aquí también - `gateway.onConnect()` conducido contra una llamada real de `HttpClient.newWebSocketBuilder().buildAsync(...)` a una dirección inalcanzable falló con un simple `java.net.ConnectException`, no un error de casting/interoperabilidad, confirmando que la cadena de interoperabilidad funciona. Lo que NO se verificó: un handshake real de Gateway (`Hello` → `Identify` → `READY`) contra los servidores reales de Discord, temporización real de latido bajo la propia tolerancia de Discord, o que el valor por defecto de `intents` (`GUILDS`+`GUILD_MESSAGES`+`DIRECT_MESSAGES`+`MESSAGE_CONTENT` = `37377`) sea realmente suficiente para recibir contenido de mensaje una vez que `MESSAGE_CONTENT` está habilitado/aprobado para un bot real en el Discord Developer Portal. Haz al menos un `bxAgents serve` manual contra un proyecto con un bot real de Discord respaldado por `botTokenEnvVar` (con `MESSAGE_CONTENT` habilitado) antes de depender de él en producción.

`EmailGatewaySpec.bx` lleva una versión mayor del mismo vacío. El IMAP entrante se prueba enteramente vía un seam inyectable `setImapPoller()` (structs de mensaje normalizados enlatados, sin buzón real), y el saliente se prueba enteramente vía un seam inyectable `setMailService()` (`FakeMailService`/`FakeMail`, sustituyendo a `MailService@cbmailservices` - `EmailGateway` nunca pasa por WireBox real en absoluto en estos specs, ya que no hay arranque real de ColdBox). Lo que sí se verificó genuinamente de forma directa esta sesión (no mockeado, no asumido): la superficie real de API `jakarta.mail` de la que depende `fetchInboundMessages()` (`Session.getDefaultInstance()`, `Flags`/`Flags.Flag`/`FlagTerm`, `Store.getStore("imaps")`, `Folder.READ_WRITE`, `MimeMultipart`, `InternetAddress`) contra los jars reales de `jakarta.mail-api`/Angus Mail (descargados de forma independiente para esto - no están vendidos en el propio classpath de prueba de este repositorio), confirmando que cada nombre de clase/método usado realmente existe y se resuelve; un `Store.connect()` real contra una dirección inalcanzable, conducido a través del propio `EmailGateway.pollInbox()` (no un helper omitido), falló con un simple error de tiempo de espera de conexión, no un error de interoperabilidad/casting, confirmando que la cadena de interoperabilidad alcanza correctamente la frontera de red real, la misma disciplina que las pruebas de humo de websocket de Slack/Discord. Lo que explícitamente NO se verificó, y es un vacío estrictamente mayor que el de los gateways de plataforma de chat: ningún handshake IMAP real contra un buzón real, ningún módulo real `cbmailservices`/`bx-mail` instalado en ninguna parte de este repositorio o su arnés de pruebas (ninguno está vendido de la forma en que lo están `bx-ai`/TestBox), así que el camino de resolución de WireBox (`MailService@cbmailservices` realmente existiendo, el `bx:mail` de `BXMail` realmente enviando) nunca se ha ejercitado en absoluto, mockeado o real, en este código base. Haz al menos un `bxAgents serve` manual contra un proyecto con credenciales IMAP reales Y una instalación real de `cbmailservices`/`bx-mail` (confirma que `box install` tuvo éxito y que `moduleSettings.cbmailservices` se resuelve) antes de depender de este gateway en producción - este es el menos verificado de los cuatro gateways de estilo push enviados hasta ahora.

`WhatsAppCloudGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: el camino de verificación de firma se ejercita con firmas HMAC-SHA256 genuinamente computadas (`javax.crypto.Mac`/`SecretKeySpec`, verificadas de forma cruzada e independiente esta sesión tanto contra `openssl dgst -hmac` como contra el propio módulo `hmac` de Python antes de confiar en el cómputo de BoxLang - se atrapó una discrepancia real de vector de referencia que resultó ser un typo en el valor esperado copiado a mano, no un bug, pero solo la verificación cruzada lo atrapó), el handshake de verificación, el despacho/deduplicación de webhook, el envío saliente, y el renderizado de botón/lista interactivo se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada HTTP saliente a la API de Graph siendo stubbeada (`setApiCaller()`). Lo que NO se verificó: las propias llamadas de contexto de request de ColdBox del `handlers/WhatsAppCloud.bx` generado (`event.getHTTPContent()`/`event.getHTTPHeader()`/`event.renderData()`, el acceso a parámetros de query de clave con puntos fusionada en el scope URL de `rc` para el handshake GET) contra un arranque real de ColdBox - estos SÍ son los idiomas documentados y estándar de handler REST de ColdBox (confirmados contra la propia receta de documentación "Building REST APIs" de ColdBox, no adivinados), un punto de partida significativamente más confiable que la suposición de clave `aiGatewayRegistry()` no documentada que resultó estar equivocada en otra parte de este archivo - pero "documentado" no es lo mismo que "probado que funciona en este contexto generado" - se difirió en lugar de arriesgar ese radio de impacto para lo que es un vacío de menor confianza-de-estar-realmente-equivocado que el bug de clave de registro. Haz al menos un `bxAgents serve` real + una prueba real de webhook de Meta (o `curl`) contra `/webhooks/whatsapp-cloud` antes de depender de esta ruta en producción. Tampoco se ha hecho jamás una llamada real a la API de Graph - la capa HTTP de `deliver()`/`requestHumanInteraction()` se ejercita solo vía el seam de prueba `apiCaller`.

`TeamsGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: la verificación de JWT se ejercita contra un par de claves RSA de 2048 bits genuinamente generado (`java.security.KeyPairGenerator`) y JWTs de prueba firmados a mano construidos enteramente dentro del spec (sin fixtures precomputadas, sin dependencia externa de `openssl` en tiempo de prueba) - una firma válida se acepta y despacha, mientras que una firma manipulada, un `aud` incorrecto, un `iss` incorrecto, y un `exp` expirado se confirman de forma independiente cada uno como rechazados con 401. El camino de actividad de invocación (clic de botón de Adaptive Card), el despacho de mensajes, el filtrado exclusivo de scope personal, el enhebrado vía `replyToId`, la fragmentación, y el renderizado de Adaptive Card se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada REST de Connector saliente siendo stubbeada (`setApiCaller()`) y las obtenciones de JWKS/token OAuth2 siendo stubbeadas (`setJwksFetcher()`/`setTokenFetcher()`). Lo que NO se verificó: las propias llamadas de contexto de request de ColdBox del `handlers/Teams.bx` generado contra un arranque real de ColdBox (la misma categoría de vacío que el propio handler de WhatsApp Cloud, y diferido por la razón idéntica - ver esa entrada arriba); nunca se ha hecho una obtención real de token OAuth2 ni una llamada REST de Connector real contra los endpoints reales de Microsoft; y la compensación de JWKS-cacheado-por-la-vida-de-la-instancia (ver `docs/conventions/gateways/teams.md`) significa que un escenario real de rotación de claves tampoco se ha ejercitado jamás. Haz al menos un `bxAgents serve` real + un registro real de app de Teams (App ID/contraseña del portal Azure/Bot Framework) + un cliente real de Teams enviando un DM antes de depender de este gateway en producción.

`TwilioGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: el camino de verificación `X-Twilio-Signature` HMAC-SHA1/base64 se ejercita con una firma genuinamente computada construida en línea en el spec, verificada de forma cruzada e independiente esta sesión contra los propios módulos `hmac`/`hashlib` de Python antes de confiar en la implementación de BoxLang (la misma disciplina que la propia verificación cruzada HMAC-SHA256 de WhatsApp Cloud) - una firma de referencia real se computó en Python para una combinación conocida de token de autenticación/URL/parámetros y se confirmó que coincide exactamente con la salida de BoxLang. El análisis de cuerpo con codificación de formulario (incluyendo un `+` literal viajando correctamente de ida y vuelta a través de la codificación de porcentaje `%2B`), el override de `publicUrl` para despliegues de proxy inverso/túnel, el modelo dual de TwiML-ack-luego-respuesta-REST-asíncrona, la fragmentación de salida, y la correlación de respuesta HITL indexada por número de teléfono se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada HTTP saliente a la API de Messages siendo stubbeada (`setApiCaller()`). Lo que NO se verificó: la propia llamada `event.getUrl()` del `handlers/Twilio.bx` generado contra un arranque real de ColdBox (la misma categoría de vacío que los propios handlers de WhatsApp Cloud/Teams, diferido por la razón idéntica) - `event.getUrl()` es un método documentado de Routable/Request Context de ColdBox (confirmado vía el MCP de documentación de ColdBox), no una suposición, pero "documentado" no es "probado en este contexto generado". Tampoco se ha hecho jamás una llamada real a la API de Messages de Twilio. Haz al menos un `bxAgents serve` real + una prueba real de webhook de número de teléfono de Twilio antes de depender de esta ruta en producción - y nótese que la correlación HITL indexada por número de teléfono (ver `docs/conventions/gateways/twilio.md`) tiene una limitación real y documentada: una segunda solicitud HITL al mismo número de teléfono antes de que se responda la primera sobrescribiría la entrada `pendingApprovals` de la primera, huerfanándola silenciosamente. Tampoco hay lista blanca/limitación de tasa construida para SMS entrante - a diferencia de Eve, que documenta (pero no aplica en código) que su propia configuración `allowFrom` es "obligatoria", este puerto no tiene ninguna compuerta equivalente en absoluto; cualquier número de teléfono puede enviar mensajes a un número desplegado de Twilio y alcanzar al agente.

`GitHubGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada, y la lógica central de este gateway se condujo además a través de una prueba de humo independiente de BoxLang real durante el desarrollo (no solo el spec permanente) - así fue como se atrapó un bug genuino antes de que jamás llegara a la suite de pruebas: la lógica de subcadena del helper de extracción de mención llamaba a `left( body, 0 )` cada vez que la `@mention` ocurría justo al comienzo de un comentario (un caso muy común), y el `left()` de BoxLang lanza `"Count cannot be zero"` en lugar de devolver una cadena vacía para un conteo de cero - confirmado disparándolo con un cuerpo de comentario real, luego corregido ramificando explícitamente alrededor del caso de longitud cero en lugar de asumir que `left()`/`mid()` toleran eso. La verificación de `X-Hub-Signature-256`, la compuerta de regex-lookahead de `@mention` (confirmada vía una prueba de humo dedicada de que un bot llamado `mybot` NO se dispara en `@mybot2`), las protecciones contra bucle de bot, la deduplicación de id de entrega, la identidad de conversación de issue-vs-hilo-de-review, `deliver()`, y la correlación HITL de `@mention`-a-respuesta se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada REST de GitHub saliente siendo stubbeada (`setApiCaller()`). Lo que NO se verificó: el `handlers/GitHub.bx` generado contra un arranque real de ColdBox (la misma categoría de vacío que el propio handler de cada otro gateway de webhook en este proyecto, diferido por la razón idéntica). Nunca se ha hecho una llamada real a la API de GitHub, y nunca se ha usado una GitHub App/PAT real contra un repositorio real - haz al menos un `bxAgents serve` real + un webhook real de GitHub configurado contra un repositorio de prueba antes de depender de este gateway en producción.

`SignalGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: el análisis JSON-RPC/SSE de `handleSseEvent()` (manejo de línea en blanco/JSON inválido, filtrado de mensaje de grupo, enhebrado de cita, respaldo a `sourceUuid` cuando falta un nombre para mostrar), la forma de envío y fragmentación de `deliver()`, y la correlación/coincidencia de decisión HITL se conducen todos a través de los métodos públicos reales del gateway con solo las llamadas de E/S salientes `rpcCaller`/`connector` siendo stubbeadas, la misma disciplina de prueba de seam que cada otro gateway en este proyecto. Dos hallazgos de nivel BoxLang salieron a la superficie durante el desarrollo, ambos resueltos y vale la pena registrarlos como peligros generales en lugar de bugs específicos del gateway: (1) un script de prueba de humo desechable que nombró una variable local `request` estaba interactuando silenciosamente con el propio scope reservado `request` de BoxLang en lugar de crear una variable simple, produciendo errores engañosos de "método no encontrado"/"discrepancia de tipo de argumento" desde `HttpClient.send()` que se veían exactamente como una limitación genuina de interoperabilidad Java pero desaparecían por completo una vez que se renombró la variable - `SignalGateway.bx` mismo nunca tuvo un bug; (2) un `try/catch` colocado directamente en el nivel superior de un script de prueba de humo `.bxs` independiente (no dentro de una función) disparó un `java.lang.VerifyError: Inconsistent stackmap frames`, una limitación real de verificación de bytecode del compilador de scripts de nivel superior de BoxLang, rastreada vía el stack trace hasta la propia clase generada del script de prueba, no `SignalGateway.bx` - corregido envolviendo el try/catch dentro de una función nombrada en su lugar. Lo que NO se verificó, y es el vacío más grande entre todos los gateways de estilo push enviados hasta ahora: nunca ha habido un daemon real de `signal-cli` disponible en este entorno, así que todo el ciclo de vida de conexión SSE asíncrona - abrir el stream vía `HttpClient.sendAsync()`+`BodyHandlers.ofLines()`, el bucle de retroceso exponencial de reconexión contra una conexión genuinamente inestable, el watchdog de inactividad de 30s/120s forzando una reconexión, y un round-trip JSON-RPC en vivo - nunca se ha ejercitado de extremo a extremo, solo probado por humo a nivel de plomería de interoperabilidad (una prueba independiente alcanzó un `java.net.ConnectException` real contra una dirección inalcanzable, probando que la cadena es sólida, no que funciona contra un daemon en vivo). Haz al menos un `bxAgents serve` manual contra un proyecto con un daemon real y en ejecución de `signal-cli` y una cuenta real vinculada de Signal antes de depender de este gateway en producción - esta es una arquitectura de transporte genuinamente nueva en este código base (el único gateway basado en SSE entre Telegram/Slack/Discord/Email/Signal), no solo una nueva plataforma sobre una forma de transporte ya probada.

## WhatsApp Personal (puente no oficial de cuenta personal) - investigado, no construido

El plan original (igualando la propia arquitectura de Hermes Agent) pedía un `WhatsAppPersonalGateway` construido generando un subproceso de Node.js ejecutando `@whiskeysockets/baileys` (el cliente de protocolo multi-dispositivo de WhatsApp Web que el propio Hermes usa - licenciado MIT, su `bridge.js` completo se leyó directamente del código fuente real de Hermes, no resumido). Ese enfoque se dejó de lado a mitad de sesión por instrucción directa de preferir una integración nativa BoxLang/JVM sobre un puente de subproceso, y de usar una biblioteca Java nativa solo si es de código abierto y ni GPL ni LGPL.

Esa búsqueda encontró **Cobalt** (`com.github.auties00:cobalt`, antes WhatsappWeb4j) - una implementación Java real, licenciada MIT, mantenida activamente (900+ estrellas) del protocolo de "cliente vinculado" multi-dispositivo de WhatsApp, con una API fluida documentada (`WhatsAppClient.builder().linkedApi().webClient()...`, `addNewMessageListener()`, `sendMessage()`) que coercionaría limpiamente desde un closure de BoxLang vía la propia coerción SAM-de-Java documentada de BoxLang (confirmado vía el MCP de documentación de BoxLang - no se necesita `createDynamicProxy()` para un listener de método único abstracto). Dos bloqueos reales salieron a la superficie durante la verificación, no adivinados:

1. **La primera lectura de pom.xml (rama `master` de Cobalt, una reescritura multi-módulo en progreso) requiere Java 25** - dos versiones mayores por delante de la propia línea base documentada de BoxLang (Java 21+, confirmado vía el MCP de documentación de BoxLang y coincidiendo con el propio JDK `21.0.10` de este proyecto). Al volver a comprobar contra el artefacto realmente publicado en Maven Central (`cobalt:0.0.10`, lo real que una `<dependency>` resolvería hoy, no la reescritura no publicada) se mostró `<java.version>21</java.version>` - así que el hallazgo de Java-25 fue una falsa alarma causada por leer la rama equivocada, no un bloqueo real. Vale la pena registrarlo como precaución: el `pom.xml` de rama por defecto de un repositorio de GitHub no es necesariamente lo que está en Maven Central.
2. **El `cobalt:0.0.10` real y publicado trae `com.aspose:aspose-words` como una dependencia dura de tiempo de compilación** (usada internamente para generar miniaturas de vista previa de enlaces a partir de documentos Word) - Aspose.Words for Java es de licencia comercial/propietaria, no de código abierto, así que empaquetarlo violaría la misma restricción de licencia que Cobalt mismo fue elegido para satisfacer. El grafo de dependencias completo (~15 jars: zxing, qr-terminal, curve25519, protobuf-base, jackson o fastjson2 dependiendo de la versión, libphonenumber, dd-plist, apk-parser, link-preview, jaffree, ez-vcard, slf4j, más Aspose) todos necesitarían descargarse y empaquetarse manualmente bajo la carpeta `libs/` de este módulo - los módulos de BoxLang no tienen resolución de dependencias de estilo Maven propia (confirmado vía el MCP de documentación de BoxLang: los jars de terceros se empaquetan directamente en la carpeta `libs/` de un módulo, cargados por un classloader por módulo - no hay clave `javaLibraries` en `box.json` que resolvería un árbol de dependencias automáticamente).

Dada la contaminación de licencia vía Aspose y el esfuerzo de ensamblaje manual de fat-jar sin herramientas de resolución de dependencias disponibles para verificar que el resultado realmente carga, **WhatsApp Personal fue descartado del alcance** en lugar de enviarse como un gateway real o un stub. `validGatewayTypes` de `ProjectValidator` y `TYPE_CLASS_MAP` de `GatewayGenerator` no incluyen una entrada `whatsapp-personal` - un proyecto que intenta declarar uno obtiene el error de validación existente de "tipo de gateway desconocido", igual que cualquier otro tipo no soportado, en lugar de un stub medio construido engañoso. Revisitar esto es razonable si Cobalt alguna vez elimina la dependencia de Aspose (es una funcionalidad estrecha - miniaturas de vista previa de enlaces de documentos Word - no central para la mensajería), o si una sesión futura decide que el enfoque de puente de subproceso Node/Baileys (rechazado en esta ronda por preferencia de arquitectura, no por un bloqueo técnico) es preferible después de todo.

## `GatewaySession` es de todo el proyecto y solo de agente raíz (v1)

Un proyecto con al menos una entrada de gateway de estilo push obtiene exactamente un `GatewaySession` generado, agrupando cada gateway de estilo push y siempre vinculado al agente raíz del proyecto - coincidiendo con el precedente existente de que la exposición HTTP `exposes: "agent"` también es siempre solo-de-agente-raíz (ver [gateways/](conventions/gateways/index.md#3-push-style-gateways-type-telegram--slack--discord--email--whatsapp-cloud--teams--twilio--github--signal-and-friends)).

**Esto es la arquitectura, no una carencia.** Un proyecto tiene un único agente raíz, y a los [`subagents/`](conventions/subagents.md) se les delega *desde ese raíz* - el raíz decide qué va a quién. Por eso todo punto de entrada (gateways, la ruta HTTP del agente, la web UI, los schedules) llega al raíz por diseño. Vincular un gateway directamente a un subagente esquivaría precisamente al agente que debe orquestarlo, y por eso no existe una clave `targetAgent` por gateway: para lograr "Telegram habla con soporte, Slack habla con investigación", esa lógica de enrutado va en el raíz, que delega, en lugar de partir la puerta de entrada.

## Corregido: `GatewaySessionBootstrap.bx` buscaba gateways por la clave incorrecta de `aiGatewayRegistry()`

Un bug real, previamente enviado: el interceptor generado buscaba gateways por el propio nombre de archivo de la entrada `gateways/*` descubierta (por ejemplo, `"telegramChannel"`), pero el `GatewayRegistry.register()` de bx-ai siempre indexa por el propio `getName()` fijo de la clase del gateway (por ejemplo, `"telegram"`) - nunca nada suministrado por el llamador. Esto significaba que la construcción de `GatewaySession` lanzaba una excepción en el arranque de ColdBox para cada proyecto generado con un gateway de estilo push; se envió sin detectarse porque la única cobertura previa afirmaba sobre el texto del archivo generado, nunca contra un registro en vivo. Corregido en `GatewayGenerator.generate()`: el interceptor ahora busca gateways por su cadena de TIPO, deduplicada, con una prueba de regresión permanente (`GatewayGeneratorSpec.bx`) que registra un gateway real y prueba que la clave emitida lo resuelve vía un `aiGatewayRegistry()` en vivo.

**Una consecuencia real y permanente que esta corrección expone (sigue siendo cierta hoy, no es un bug):** porque el registro está indexado por tipo, no por entrada, dos entradas `gateways/*` del mismo tipo de estilo push colisionan en el mismo slot de registro a nivel de todo el proyecto - por ejemplo, dos entradas `type: "telegram"` (dos tokens de bot diferentes) silenciosamente harían que el segundo registro sobrescriba al primero, y `GatewaySession` solo vería alguna vez uno de ellos. No hay un alias/override de nombre de registro por entrada hoy. Una instancia por tipo de estilo push, por proyecto, es el verdadero techo de v1.

## Corregido: el snapshot publicado de bx-ai solía retrasarse respecto a su propia rama `development`

Mientras la API `aiGatewayRegistry()`/`GatewaySession`/`BaseGateway` de bx-ai y su `IAiMemory.summarize()` delimitado por userId/conversationId (necesario para `/compact`) todavía no se habían publicado, este proyecto fijó `bx-ai@3.4.0-snapshot` - un artefacto de ForgeBox continuamente republicado que repetidamente se retrasaba respecto al HEAD real de la rama `development` de bx-ai por alguna cantidad de tiempo. Tres veces separadas, un `downloadModules` reciente obtuvo un snapshot anterior a la API que el propio código generado de este proyecto ya llamaba, rompiendo el arranque de gateway (`Function 'aiGatewayRegistry' not found`), toda la capa de gateway (faltando `BaseGateway.bx`), o `/compact` (resumiendo silenciosamente el scope equivocado). CI lo evitaba clonando y construyendo la rama `development` de bx-ai directamente y superponiéndola sobre lo que fuera que `downloadModules` obtuviera.

bx-ai desde entonces ha cortado un release real y estable `3.4.0` que lo lleva todo - confirmado directamente contra el artefacto publicado en ForgeBox (`BaseGateway.bx`, `GatewaySession.bx`, los BIFs `aiGatewayRegistry`/`aiGatewaySession`, `MockService.bx`, y `summarize( config, userId, conversationId )` están todos presentes). `build.gradle` ahora fija esa versión estable en lugar del snapshot, y el paso de superposición de CI se elimina - `downloadModules` solo ya es suficiente.

## La interfaz web de chat v1 (`exposes: "webui"`) - qué es real, y qué solo pudo comprobarse contra documentación, no contra un servidor en vivo

`WebUiGeneratorSpec.bx` y una prueba de extremo a extremo de `BuildPipelineSpec.bx` ambas conducen a las clases reales `WebUiGenerator`/`BuildPipeline` contra fixtures reales: el shell estático `<path>/index.html` se confirma que se escribe y se plantilla correctamente (los placeholders `__API_BASE__`/`__APP_TITLE__` sustituidos, nunca dejados en la salida), el opcional `interceptors/WebUiAuthGate.bx` se confirma que se genera solo cuando `apiKeyEnvVar` está configurado, que protege exactamente `<path>/api/*` y nunca el shell `<path>` desnudo mismo, y que se registra correctamente en la lista `interceptors:[...]` de `config/ColdBox.bx` de extremo a extremo a través del `BuildPipeline` real. El interceptor generado también se confirmó esta sesión que compila e instancia limpiamente vía una prueba de humo independiente (cargado a través de la misma primitiva `DynamicClassLoader` que el propio pipeline de build usa).

Lo que NO se verificó - y no pudo, en este entorno de desarrollo: una prueba real de navegador de un `bxAgents serve` real de la página realmente cargando, transmitiendo una respuesta, y la compuerta `X-API-Key` realmente rechazando/aceptando requests sobre HTTP real. El propio arnés `runColdBoxIntegrationTests.bxs`/`tests/coldbox` de este proyecto (el mismo que probó que la ruta `/invoke` de `toAi()` funciona de extremo a extremo para `http-gateway-agent`) requiere que `tests/coldbox` esté presente vía un `box install` real dentro de `tests/` - el acceso a red de CommandBox y ForgeBox ambos estuvieron indisponibles en el sandbox de esta sesión, así que ese arnés no pudo ejercitarse para la interfaz web (ni reconfirmarse para nada más) esta sesión. Dos consecuencias directas:

- La forma exacta de respuesta JSON de `/invoke` usada por las propias páginas/documentación generadas de este proyecto (`{"input": "..."}` de entrada, una respuesta conteniendo `"success": true`) está confirmada empíricamente **solo** vía la propia aserción previa, ya pasando, de `runColdBoxIntegrationTests.bxs` (una comprobación de subcadena, no una aserción de forma completa) - no reverificada esta sesión.
- El formato de cable SSE de `/stream` que analiza el propio JS de la interfaz web (líneas `data: {"token":"..."}`, terminadas por `data: [DONE]`) se toma directamente de la propia documentación oficial "AI Routing" de ColdBox, no reconfirmada de forma independiente contra un servidor en vivo esta sesión - a diferencia de casi cualquier otra afirmación de formato de cable en la documentación de este proyecto, que fueron verificadas cruzadamente contra código real en ejecución dondequiera que fue posible (ver, por ejemplo, las verificaciones cruzadas independientes de Python/openssl de los esquemas de firma HMAC-SHA256/SHA1 en otras partes de este archivo).

Haz al menos un `bxAgents serve` real + una prueba real de navegador (mensaje enviado, respuesta transmitida, la compuerta `X-API-Key` realmente devuelve 401 a un request que carece de la clave) antes de depender de esta funcionalidad en producción - el mismo consejo permanente ya dado para el vacío de no-probado-contra-un-arranque-real de cada handler de webhook generado en otras partes de este archivo.

## El almacén SQLite de la interfaz web - verificado a nivel de biblioteca, no a través de un arranque real de ColdBox

La pila qb + bx-sqlite subyacente a `models/ChatDb.bx` se verificó directamente esta sesión contra los jars reales, no inferida de la documentación: los propios fuentes `.cfc` de qb 13.1.0 compilan y corren nativamente en BoxLang 1.16 sin `bx-compat-cfml`; `SQLiteGrammar` + `SchemaBuilder` realmente crean las tablas e índices v1 contra un archivo SQLite real; un segundo pase de migración es un no-op limpio; `QueryBuilder` hace round-trip de inserciones, lecturas filtradas/ordenadas y eliminaciones; y la clave primaria compuesta de `preferences` realmente rechaza un `(userId, prefKey)` duplicado. Se encontraron dos restricciones de esta manera en lugar de asumirse - qb requiere un datasource **nombrado** (su propio `appendSqlComments()` tipa ese argumento como `string`, así que un struct inline lanza antes de que corra cualquier SQL) - y `SchemaBuilder@qb` se mapea solo con su argumento `grammar`, nunca recibiendo `moduleSettings.qb.defaultOptions` - y el código generado está moldeado en torno a ambos.

Las tres suposiciones de cableado de las que esto depende - que `getInstance( "ChatDb" )` se resuelve a través del propio WireBox de la app generada, que `SchemaBuilder@qb`/`QueryBuilder@qb` se resuelven una vez que qb está instalado como un módulo real de ColdBox (qb es una dependencia de `box.json`, no vendida aquí - el mismo vacío honesto que ya lleva `cbmailservices`), y que `this.datasources` en el `Application.bx` generado se capta como se espera - ahora se ejercitan de extremo a extremo para el camino de preferencias compartidas: `runColdBoxIntegrationTests.bxs` escribe y lee una preferencia a través de `POST`/`GET /chat/api/preferences` desde fuera del proceso (ver abajo), lo cual solo funciona si las tres se cumplen. Lo que **no** se ejercita todavía de la misma manera es el CRUD de conversación, la protección entre usuarios, y el pase de migración - esos están probados a nivel de biblioteca y vía `WebUiRuntimeSpec.bx` leyendo objetos reales de WireBox dentro de un arranque, pero no conducidos desde un segundo llamador HTTP independiente de la forma en que ahora lo está el camino de preferencias. Haz un `bxAgents serve` contra un proyecto webui con `qb` y `bx-sqlite` genuinamente instalados antes de confiar en el almacén en producción; el modo de fallo si alguna de las tres suposiciones de cableado está mal es ruidoso en el arranque (un mapeo de WireBox no resoluble o un driver JDBC desconocido), no silencioso.

## La interfaz web sobre HTTP real: probada para `/health` y las rutas de preferencia de espacio de trabajo compartido

`runColdBoxIntegrationTests.bxs` obtiene `GET /chat/api/health` desde fuera del servidor, contra la app fixture generada arrancada por un `boxlang-miniserver` real, y falla el build si no es un 200. En CI devuelve:

```
+ App probe GET /chat/api/health -> status=200
  body: {"success":true,"status":"ok"}
```

Ese request es la prueba base de extremo a extremo para el lado del servidor de la interfaz web: el enrutamiento de ColdBox alcanzando `handlers/ChatUi.bx`, WireBox resolviendo el handler y `ChatDb`, `this.datasources` dándole a bx-sqlite un archivo SQLite usable, y qb siendo un módulo de ColdBox genuinamente activado - nada de lo cual puede establecer ninguna aserción de texto fuente. El mismo script corredor va más allá y conduce dos rutas más sobre HTTP real contra ese mismo arranque: `POST /chat/api/preferences/set` y `GET /chat/api/preferences`, llamadas como dos requests HTTP separados y sin cookies, específicamente para probar el comportamiento de "un solo espacio de trabajo compartido" de la interfaz web sin cuentas - que una preferencia escrita por un llamador se lee de vuelta por un segundo llamador completamente independiente, a través de `ChatDb`/SQLite, no simplemente reflejada desde una sesión. `WebUiRuntimeSpec.bx` luego cubre el resto del comportamiento del almacén (migraciones, CRUD de conversación con scope, la protección entre usuarios) leyendo objetos reales de WireBox dentro de ese mismo arranque.

**Lo que todavía no está cubierto sobre HTTP real:** la lista/historial de conversación y la transmisión SSE. Conducir esas rutas desde `WebUiRuntimeSpec` no es posible tal como está estructurado, porque ese spec se ejecuta *dentro* de un request servido por el mismo miniserver único, así que una llamada de loopback competiría por el pool de trabajadores que el propio corredor ocupa. Cubrirlas necesita ya sea un segundo proceso de servidor o un pool de hilos de miniserver más grande, y es un seguimiento natural en lugar de algo que fingir.

Nótese que este párrafo previamente citó `runner-coldbox.bxm never responded: HTTP 408` como evidencia de esa inanición. **Esa atribución era incorrecta**, y vale la pena registrarlo porque engañó a este proyecto durante varios ciclos de CI: el 408 no tenía nada que ver con llamadas de loopback. La página del corredor llamaba a `chr( 10 )` - un BIF que no existe - así que devolvía 500 al entrar en cada request, y el bucle de reintento del orquestador se abrió paso a través de 90 segundos de esos 500 antes de reportar el tiempo de espera final. Ambos están corregidos. La preocupación de inanición-de-loopback arriba es una razón estructural genuina para no conducir más rutas desde dentro de ese spec, pero sigue siendo una expectativa bien fundada en lugar de algo que realmente se haya observado fallando aquí.

## El propio front-end de la interfaz web: conducido en un navegador real, pero contra una API mockeada

El JavaScript de la página enviada se ejercitó de verdad esta sesión, no solo se afirmó como texto fuente: el `index.html` generado se cargó en Chromium sin cabeza y se condujo de extremo a extremo con cada ruta `<path>/api/*` interceptada y respondida con payloads realistas. Confirmado funcionando de esa manera - la barra lateral de conversación renderizando desde `GET /conversations` y cambiando/renombrando/eliminando a través de sus propias rutas; `GET /info` moldeando la barra de herramientas (Compact apareciendo solo cuando `capabilities.compact` es true, el nombre del modelo aterrizando en el encabezado); el tema llegando desde `preferences` del lado del servidor y aplicándose; `/history` rehidratando el transcript; un turno de SSE real transmitiendo contenido, razonamiento y chips de tool-call desde el envelope de bx-ai; renderizado de markdown; y **New chat** creando una conversación del lado del servidor y abriéndola. Cero errores de JavaScript, cero placeholders `__TOKEN__` sin reemplazar, y el layout de pantalla estrecha se capturó en 390px.

Lo que eso NO prueba: la API era un mock de Playwright, no el `handlers/ChatUi.bx` generado corriendo bajo un arranque real de ColdBox contra un almacén SQLite real. Las formas de request y respuesta se tomaron del propio código fuente del handler generado, así que una desviación entre los dos no sería atrapada por esto. Todo en la entrada de limitaciones conocidas de arriba sobre el almacén se aplica aquí también - un `bxAgents serve` manual contra un proyecto webui con `qb` y `bx-sqlite` genuinamente instalados sigue siendo la puerta honesta antes del uso en producción.

## Los encabezados de *grupo* de la navegación del sitio siguen en inglés en un idioma traducido

Cada entrada de navegación respaldada por una página real toma su etiqueta del `title` del frontmatter de esa página, así que un árbol traducido obtiene etiquetas de navegación traducidas sin esfuerzo - `Instalación`, `Inicio rápido`, `Introducción a BxAgents` y demás se muestran correctamente bajo `/es/`.

Los cuatro nodos de agrupación sintéticos de `docs/nav.json` (`Getting Started`, `Conventions`, `Guides`, `Reference`, más el subgrupo `Example Agents`) no tienen ninguna página detrás, así que no hay de dónde leer un título traducido. bx-sites v1 entrega además a cada árbol de idioma el `navConfig` del árbol *por defecto* tal cual - por diseño, para que la navegación de un idioma siempre refleje la estructura de su árbol por defecto - lo que significa que tampoco hay un `nav.json` por idioma con el que sobrescribirlos. Por tanto se muestran en inglés en todos los idiomas.

Medido en un build real: la barra lateral de `/es/` muestra `["Getting Started", "Instalación", "Inicio rápido", "Introducción a BxAgents", "Conventions", "Agent.bx", ...]`. Etiquetas de página traducidas, encabezados de grupo no.

Cerrar esto requiere un cambio en bx-sites (una sobrescritura de navegación por idioma, o títulos de grupo traducibles), no un cambio aquí.
