feat(infra): kill_fleet_agent — cierre dirigido de un ejecutor de la flota (auto-kill)

Cierra UN ejecutor por sessionId (exacto/prefijo) o PID: SIGTERM al proceso claude
(cierre limpio, recuperable con --resume) + kill-window de su window tmux. Lo usa
el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que
su DoD-contrato esta met. Guards: NO mata a un role=orchestrator (leido del
goal.json) ni a la sesion que invoca (PID propio por ancestros /proc). --dry-run
para inspeccionar sin tocar nada. Overrides FN_FLEET_* para tests. Tag grupo
orchestration. Tests: 17 asserts (golden por sessionId/PID/prefijo, guards
orchestrator/self rc=3, errores rc=2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 13:28:07 +02:00
parent b6ad1a3a15
commit f415dd56f5
3 changed files with 372 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
---
name: kill_fleet_agent
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: "kill_fleet_agent <sessionId|PID> [--socket <s>] [--dry-run]"
description: "Cierre limpio y dirigido de UN ejecutor de la flota tmux. Dado un sessionId (exacto o prefijo) o un PID, manda SIGTERM al proceso claude del ejecutor (cierre limpio, recuperable con claude --resume) y cierra su window tmux (kill-window) en el socket del perfil FleetView. Lo usa el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que su DoD-contrato esta met. Guards de seguridad: NUNCA mata a un agente con role=orchestrator (leido de su goal.json) ni a la sesion que invoca la funcion (resuelve su propio PID de claude por los ancestros de /proc). Por defecto EJECUTA; --dry-run imprime el plan sin tocar nada. Es el cierre dirigido a UN agente, frente a reboot_all_claudes que opera sobre toda la flota."
tags: [fleet, claude-fleet, orchestration, tmux, kill, infra]
uses_functions: []
uses_types: []
error_type: error_go_core
file_path: "bash/functions/infra/kill_fleet_agent.sh"
tested: true
tests:
- "golden: ejecutor por sessionId, PID y prefijo se resuelve y dry-run imprime el plan"
- "guard: matar un role=orchestrator devuelve rc=3 y se niega"
- "guard: matar la sesion actual (self) devuelve rc=3 y se niega"
- "error: target no resuelto rc=2; sin target rc=2"
test_file_path: "bash/functions/infra/kill_fleet_agent_test.sh"
params:
- name: target
desc: "Primer arg posicional: sessionId del ejecutor (exacto o prefijo) o su PID (todo digitos). Por PID se lee sessions/<pid>.json para el sessionId; por sessionId se busca en sessions/*.json el que case y su archivo da el PID."
- name: --socket
desc: "Socket tmux del perfil FleetView donde vive la window. Default: $FLEET_SOCKET, o 'fleet' si no esta seteada."
- name: --dry-run
desc: "Imprime el plan (PID, sessionId, role, window, accion) y NO mata el proceso ni cierra la window. Sin esto, ejecuta."
output: "Imprime una linea de plan con PID, sessionId, role, socket y window resueltos, seguida de la accion ejecutada (SIGTERM + kill-window) o, con --dry-run, de DRY-RUN. Exit 0 ok/dry-run; 2 uso incorrecto o target no resuelto a PID; 3 guard (target es un orchestrator o la sesion actual)."
---
# kill_fleet_agent
Cierra de forma dirigida UN ejecutor de la flota tmux: SIGTERM al proceso `claude` (cierre limpio, recuperable con `claude --resume <sessionId>`) más `kill-window` de su window en el socket del perfil FleetView. Es la pieza que el orquestador usa para **liberar el slot idle** de cada ejecutor en cuanto verifica que su DoD-contrato está `met` — sin esto, los ejecutores terminados se acumulan en reposo en la flota.
## Ejemplo
```bash
# Cerrar un ejecutor por sessionId (el orquestador lo llama tras verificar `met`):
./fn run kill_fleet_agent 32945650-a4e1-472b-90c9-5b38ef60a463 --socket "$FLEET_SOCKET"
# Por prefijo de sessionId, en el socket por defecto ($FLEET_SOCKET o "fleet"):
./fn run kill_fleet_agent 32945650
# Ver el plan sin matar nada (PID, sessionId, role, window, accion):
./fn run kill_fleet_agent 48213 --dry-run
```
## Cuando usarla
Úsala desde el modo orquestador justo después de que el verificador independiente devuelva `met` sobre un ejecutor: ciérralo para que no quede ocupando un slot idle en la flota. Resuelve el target por sessionId (exacto o prefijo) o por PID, comprueba los guards y manda SIGTERM + cierra la window. Es el cierre dirigido a **un** agente; para reiniciar/parar **toda** la flota usa `reboot_all_claudes` (con `--exclude-current`). Nunca uses `pkill`/`killall claude` (te matas a ti mismo, el orquestador).
## Gotchas
- **Impura y destructiva**: manda SIGTERM y cierra una window tmux. Por defecto EJECUTA (es el caso de uso del bot: cerrar un ejecutor ya verificado `met`); usa `--dry-run` para inspeccionar antes.
- **Guard anti-orquestador**: si el goal.json del target tiene `role=orchestrator`, rehúsa con exit 3. Evita decapitar la flota por error. El `role` se lee de `~/.claude/goals/<sessionId>.json` (lo escribe `mark_claude_role`).
- **Guard anti-self**: resuelve el PID de `claude` de la sesión actual subiendo por los ancestros de `/proc`; si el target coincide, rehúsa con exit 3 ("No me suicido"). Es el equivalente dirigido de la regla "nunca `pkill claude`".
- **Resolución de la window**: usa `tmux -L <socket> list-panes -a` y casa `pane_pid == PID`. Funciona porque `spawn_fleet_agent` arranca el ejecutor con `exec claude`, así el `pane_pid` ES el PID de claude. Si no hay socket/tmux, la window queda "(no resuelta)" y solo se manda el SIGTERM (best-effort, no falla).
- **SIGTERM, no SIGKILL**: cierre limpio para que Claude Code persista su sesión; el trabajo se puede retomar con `claude --resume <sessionId>`.
- **Requiere `jq`** para leer los JSON de sessions/goals.
- **Overrides de entorno solo para tests**: `FN_FLEET_SESSIONS_DIR`, `FN_FLEET_GOALS_DIR` y `FN_FLEET_SELF_PID` redirigen los directorios y fuerzan el PID propio; no usarlos en operación normal.
## Capability growth log
(v1.0.0 — sin cambios todavía.)