Por qué no conviene que los agentes de Claude Code informen de su propio estado
Un hook Stop, un registro de eventos y presupuestos en dólares para gestionar sesiones de Claude Code como un organigrama, más cuatro sorpresas del harness que 74 tests en verde no detectaron.
Si ejecutas más de una sesión de Claude Code a la vez, tarde o temprano probarás lo obvio: pedirle a una sesión que gestione a las demás. Funciona unos cinco minutos. Tras intentarlo en varios proyectos con el mismo fallo cada vez, lo útil fue dejar de tratarlo como un problema de prompts y abordarlo como un problema de arquitectura. Esto es lo que falló, lo que se construyó y seis comportamientos del harness que ningún test unitario habría detectado, cuatro de los cuales solo aparecieron al ejecutar una sesión real.
Tres fallos, no uno
El síntoma que todos notan es el silencio: un subordinado nunca informa, ni siquiera cuando se le dice que avise al terminar. Sin embargo, es el más leve de tres problemas distintos.
Los subordinados no informan. La instrucción vive en el contexto del modelo, y el contexto se compacta, se agota o sencillamente queda eclipsado por la tarea. Cuando el trabajo termina, cuatrocientas líneas de salida de tests ya han desplazado el recordatorio de informar. No es un problema de disciplina. Es un problema de jerarquía de memoria, y redactarlo con más amabilidad no lo arregla.
Los supervisores no pueden observar. Es fácil pasarlo por alto y se debe a cómo funciona Claude Code. Una sesión solo se ejecuta mientras hay un turno de usuario en curso, y entre turnos no hace absolutamente nada: no sondea, no espera, no piensa. Por eso, un subordinado que sí envía un informe no llega a nadie. El mensaje queda en una sesión dormida hasta que una persona escribe algo.
Nadie detecta los fallos. Imagina dos sesiones editando el mismo archivo y sobrescribiéndose en silencio, una sesión que agota su contexto a mitad de la tarea y muere, o una que gira en bucle. Vistas desde fuera, todas parecen simplemente una sesión lenta.
Ese tercer problema es el caro, y el arreglo ingenuo de pedir a las sesiones que informen ni lo roza.
Informar es algo que le ocurre al modelo
Lo que lo cambió todo fue un principio: un informe no puede depender de que el modelo decida enviarlo. Tiene que dispararse alrededor del modelo, no desde él.
Si un informe depende de que el modelo elija presentarlo, solo es tan fiable como el contexto que le quede al modelo. Si se le quita de las manos, pasa a ser tan fiable como el ciclo de vida del proceso.
Claude Code tiene un mecanismo adecuado. Un hook Stop se ejecuta cada vez que una sesión termina un turno, y quien lo invoca es el harness, no el modelo. El modelo puede haberlo olvidado, puede haberse quedado sin contexto, incluso puede haberse caído, y el hook se ejecuta igualmente:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/stop-report.mjs"],
"timeout": 15
}
]
}
]
}
}
Esa es la única idea que sostiene todo. Lo demás es andamiaje a su alrededor.
El diseño
La herramienta resultante ejecuta las sesiones de Claude Code como una organización. Un Executive habla contigo, los Managers se encargan de líneas de trabajo y el Staff hace el trabajo. Los roles salen de la posición, no de etiquetas: un miembro del Staff que recluta su propio equipo actúa como Manager de esa rama.
you
▲
results ───────┼─────── objective
▼
┌───────────────────┐
│ Executive │
└─────────┬─────────┘
┌─────────────┴─────────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Manager │ │ Manager │
└─────┬─────┘ └─────┬─────┘
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Staff│ │Staff│ │Staff│ │Staff│
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
└─ peers ─┘ └─ peers ─┘
Cada miembro depende exactamente de un supervisor, y la escalada sube de un nivel en un nivel. Los pares resuelven entre ellos el trabajo que se solapa y solo recurren a un Manager cuando no pueden.
Tres decisiones merecen explicación, porque cada una podría haber sido distinta.
El estado es un registro de eventos de solo anexado. Hasta doce procesos del sistema operativo escriben a la vez en un mismo directorio. Se evitan los locks: cada cambio de estado es una línea añadida y el sistema operativo garantiza que ese anexado es atómico. Tampoco hay un organigrama almacenado, porque se calcula plegando el registro cada vez que alguien lo lee. Matar una sesión no puede corromper nada. En el peor caso deja un registro working obsoleto, y detectarlo es tarea de la detección de bloqueos.
Los supervisores persiguen. Cada supervisor ordena a sus subordinados directos en cada turno y sube por una escalera fija: dar un toque, leer la transcripción, reasignar. El peldaño intermedio importa más de lo que parece. Una ejecución larga de tests y un bucle infinito se ven idénticos desde fuera, y solo leer la salida real permite distinguirlos. Es una decisión para un modelo, no para un timeout.
Los presupuestos están encadenados y se cuentan en dólares. Un colega objetó que con una suscripción los dólares parecen abstractos y lo que realmente se percibe son los tokens. Aun así ganaron los dólares, por dos razones. --max-budget-usd es el único límite que el harness hace cumplir de verdad, mientras que un techo de tokens es solo un número en un archivo JSON que nada obliga a respetar. Además, los tokens no son comparables entre niveles de modelo: un millón de tokens puede costar unas diez veces más en el modelo más potente que en el más barato. Un presupuesto en tokens no puede limitar una jerarquía cuyo propósito es mezclar niveles, ya que un manager podría mantenerse holgadamente dentro de él y multiplicar la factura por diez. Los tokens se registran como indicador. La restricción son los dólares.
La secuencia de una asignación, de principio a fin, incluye una rama alternativa en medio, y esa es la parte que sostiene todo: el paso 5 existe para que el sistema nunca tenga que depender del paso 4.
El estado que un reloj no ve
Detectar bloqueos parece un simple problema de timeouts, y en gran parte lo es. Pero hay un estado que es a la vez el más urgente del sistema e invisible para cualquier cosa que mida el silencio.
Pensemos en un miembro que informa blocked. Acaba de comunicarse, así que el tiempo desde el último contacto es cero y cualquier comprobación de inactividad lo considera sano. En realidad ha dejado de trabajar y espera una decisión que solo su supervisor puede tomar. Ninguna comprobación lo marcará, porque anunció con educación que estaba atascado.
La prueba de humo lo destapó pronto, antes que los bugs más difíciles. Un subordinado envió blocked: beyond_capability y la vista de estado del supervisor lo mostraba como OK. Una vez que lo ves, el arreglo es pequeño. Verlo requirió una ejecución real.
Y luego se ejecutó de verdad
Para entonces había 74 tests unitarios, todos en verde, con la CI pasando en Linux y Windows en dos versiones de Node. El diseño se sostenía sobre el papel y en la batería de pruebas, y la confianza era alta.
Entonces una única sesión real en segundo plano recibió un trabajo trivial: crear un archivo con la palabra «hello». Falló cuatro veces seguidas por cuatro motivos sin relación entre sí. Ninguno era un error de programación. En cada caso el harness se comportaba de forma distinta a lo supuesto, y en cada caso fallaba en silencio.
1. En segundo plano se ignoran los ID de sesión
La identidad se había diseñado en torno a una idea elegante: el padre asigna de antemano el ID de sesión del hijo, de modo que el organigrama queda escrito antes de que el hijo exista y nunca hay un momento en que un subordinado se ejecute sin registrar.
En la práctica, claude --bg --session-id <uuid> ignora el UUID que le pasas y la sesión en segundo plano se inventa el suyo. El resultado tuvo algo de ironía. El hook Stop buscó en el organigrama el ID de sesión del harness, no encontró a nadie y salió sin registrar nada, así que la salvaguarda que existía para garantizar un informe no hizo nada.
La solución fue pasar al hijo el ID elegido por el padre mediante una variable de entorno y tratarlo como el válido, usando el ID del harness solo si la variable no existe.
2. Nadie puede responder a una solicitud de permiso
El subordinado siguió sus instrucciones a la perfección. Leyó la asignación, entendió el protocolo y ejecutó su primer comando, la comprobación de la bandeja de entrada, tal como se le había dicho.
Luego se quedó congelado para siempre en una solicitud de permiso. El modo de permisos acceptEdits aprueba las ediciones de archivos pero deja sin aprobar los comandos de shell, y a una sesión en segundo plano no hay ninguna persona conectada que pulse «sí». El supervisor no puede distinguirlo de un bloqueo, aunque el subordinado lo esté haciendo todo bien mientras espera una aprobación que no puede llegar.
El arreglo fue preaprobar la CLI al lanzar la sesión con --allowedTools. Hay que indicar las dos herramientas de shell, porque la que use el modelo depende de la plataforma: PowerShell en Windows y Bash en el resto.
3. --allowedTools es variádico y se traga el prompt
Con los permisos añadidos, el siguiente lanzamiento arrancó e informó al instante de que estaba inactivo y esperando un prompt.
--allowedTools <tools...> es variádico, así que sigue consumiendo argumentos más allá de un flag booleano. Este comando:
claude --allowedTools "Bash(node:*)" --bg "<the actual prompt>"
absorbe el prompt en la lista de herramientas y lanza una sesión sin instrucciones, que espera pacientemente una entrada que nunca llega. La opción se detiene limpiamente en un flag que recibe su propio valor, así que esta forma funciona:
claude --allowedTools "Bash(node:*)" --model haiku --bg "<the actual prompt>"
Ahora las reglas de permisos se emiten primero, justo antes de --session-id, para que la corrección no dependa del orden de los argumentos en otras partes del comando.
4. Para editar, primero quiere un worktree
En el cuarto intento la sesión anunció que necesitaba entrar en un worktree antes de editar. Intentó crear uno, falló porque el repositorio de pruebas no tenía commits (no se pudo resolver la rama base «HEAD») y después intentó un git commit que necesitaba una aprobación que nadie podía dar.
En parte es culpa del entorno de pruebas. Aun así, conviene conocer el hecho de fondo: las sesiones en segundo plano tienden a aislarse en un worktree para escribir, y ese aislamiento se parece más a un comportamiento por defecto que a una opción.
Otros dos, encontrados tanteando
--append-system-prompt-file existe pero no está documentado. No aparece en claude --help, pero al probarlo devuelve un error de «falta el argumento» en lugar de uno de opción desconocida. Importa porque las instrucciones de un rol ocupan unas treinta líneas con comillas y saltos de línea, y pasarlas en línea falla de tres maneras en Windows: cmd.exe no admite saltos de línea en un argumento, el escapado de comillas varía según la shell y las líneas de comandos tienen un tope cercano a 8191 caracteres. Escribir las instrucciones en un archivo y pasar su ruta evita los tres problemas.
--plugin-dir no llega a las sesiones lanzadas. Ejecutar un plugin desde un clon solo afecta a esa sesión, así que las sesiones que lanza arrancan sin hooks ni skills de rol e ignoran un protocolo que nunca recibieron. Un plugin instalado no tiene este problema, de modo que lo recomendable es instalarlo como es debido, y la configuración incluye una opción explícita de reenvío para el caso del clon.
Una batería en verde seguía sin bastar
Otro caso muestra por qué las pruebas reales por sí solas tampoco son la respuesta. La integración continua detectó un defecto que las ejecuciones locales nunca revelaron. Los eventos del registro se ordenaban por marca de tiempo en milisegundos, y los empates se resolvían con un ID de evento aleatorio. Las máquinas rápidas suelen escribir una ráfaga de eventos dentro del mismo milisegundo, así que toque, toque, informe podía plegarse como informe, toque, toque, y un miembro que acababa de informar seguiría contando como no avisado. La proyección no era determinista y podía leerse de forma distinta cada vez que se cargaba.
En local, en Windows, pasaban los 74 tests. En Linux, la CI falló dos veces.
La solución no eran marcas de tiempo más precisas. Como los anexados son atómicos, el orden de las líneas del archivo ya refleja el orden de escritura entre todos los procesos. Resolver los empates por la posición de la línea da un orden determinista y causalmente correcto.
Qué conviene llevarse
Nada de esto demuestra que el desarrollo asistido por IA sea poco fiable. El código fue la parte barata y la mayor parte estaba bien. Lo que muestra es de dónde debería salir la confianza. Una batería en verde demuestra que el sistema hace lo que modelaste. No puede decirte si tu modelo de la plataforma es correcto, y todos los fallos anteriores estaban justo en ese punto ciego, porque los tests compartían las mismas suposiciones erróneas que la implementación.
Lo que funcionó fue abaratar la refutación y repetirla una y otra vez. Cuatro ejecuciones reales, de unos minutos cada una y casi gratis con el modelo más pequeño, destaparon cuatro defectos de arquitectura que 74 tests habían dado por buenos.
Si construyes sobre una plataforma de agentes, reserva tiempo a propósito para ese ciclo. Los bugs interesantes no están en tu código. Están en la junta entre tu código y un harness cuyo comportamiento dedujiste en lugar de verificar. También vale la pena guardar esos datos del harness en un archivo de notas aparte de la especificación de diseño, ya que describen la plataforma y no tu proyecto.