f415dd56f5
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>
199 lines
8.3 KiB
Bash
199 lines
8.3 KiB
Bash
#!/usr/bin/env bash
|
|
# kill_fleet_agent — cierre limpio y dirigido de UN ejecutor de la flota tmux.
|
|
#
|
|
# Dado un sessionId (o prefijo) o un PID, mata el proceso claude del ejecutor con
|
|
# SIGTERM (cierre limpio) y cierra su window tmux en el socket del perfil
|
|
# FleetView. Es la pieza que usa el orquestador para liberar el slot idle de cada
|
|
# ejecutor en cuanto verifica que su DoD-contrato esta `met`: sin esto, los
|
|
# ejecutores terminados se acumulan en reposo en la flota.
|
|
#
|
|
# Guards de seguridad (NO destruye a quien no debe):
|
|
# - NO mata a un agente con role=orchestrator (leido de su goal.json). Matar un
|
|
# orquestador por error decapitaria la flota.
|
|
# - NO se mata a si mismo (la sesion que invoca la funcion): resuelve el PID de
|
|
# claude actual subiendo por los ancestros de /proc y rechaza el target si
|
|
# coincide. Es el equivalente dirigido de la regla "nunca pkill claude".
|
|
#
|
|
# Funcion IMPURA: manda SIGTERM a un proceso y cierra una window tmux. Por
|
|
# defecto EJECUTA (es el caso de uso del bot: cerrar un ejecutor ya verificado,
|
|
# recuperable luego con `claude --resume <sessionId>`). Usa --dry-run para ver el
|
|
# plan sin tocar nada.
|
|
#
|
|
# Overrides de entorno (testabilidad, no para uso normal):
|
|
# FN_FLEET_SESSIONS_DIR directorio de los sessions JSON. Default ~/.claude/sessions
|
|
# FN_FLEET_GOALS_DIR directorio de los goal JSON. Default ~/.claude/goals
|
|
# FN_FLEET_SELF_PID fuerza el PID propio (salta la deteccion por /proc)
|
|
set -euo pipefail
|
|
IFS=$' \t\n'
|
|
|
|
kill_fleet_agent() {
|
|
local target="" socket="" dry=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--socket) shift; socket="${1:-}" ;;
|
|
--dry-run) dry=1 ;;
|
|
-h|--help)
|
|
cat <<'USAGE'
|
|
Uso: kill_fleet_agent <sessionId|PID> [--socket <s>] [--dry-run]
|
|
|
|
Cierra UN ejecutor de la flota: SIGTERM al proceso claude + kill-window de su
|
|
window tmux. Resuelve el target por sessionId (exacto o por prefijo) o por PID.
|
|
|
|
Guards: NO mata a un role=orchestrator ni a la sesion que invoca la funcion.
|
|
|
|
Opciones:
|
|
--socket <s> Socket tmux del perfil FleetView donde vive la window.
|
|
Default: $FLEET_SOCKET, o "fleet" si no esta seteada.
|
|
--dry-run Imprime el plan (PID, sessionId, role, window, accion) y NO
|
|
mata ni cierra nada.
|
|
-h, --help Esta ayuda.
|
|
|
|
Salida: exit 0 ok (o dry-run); 2 uso incorrecto / target no resuelto; 3 guard
|
|
(intento de matar a un orquestador o a la sesion actual).
|
|
|
|
Ejemplos:
|
|
kill_fleet_agent 32945650-a4e1-472b-90c9-5b38ef60a463 # por sessionId
|
|
kill_fleet_agent 32945650 --socket fleet2 # por prefijo de sessionId
|
|
kill_fleet_agent 48213 --dry-run # por PID, solo ver el plan
|
|
USAGE
|
|
return 0 ;;
|
|
--*)
|
|
echo "kill_fleet_agent: opcion desconocida '$1' (usa -h)" >&2
|
|
return 2 ;;
|
|
*)
|
|
if [[ -z "$target" ]]; then
|
|
target="$1"
|
|
else
|
|
echo "kill_fleet_agent: argumento extra '$1' (target ya es '$target')" >&2
|
|
return 2
|
|
fi ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[[ -z "$target" ]] && {
|
|
echo "kill_fleet_agent: falta el target (sessionId o PID). Usa -h." >&2
|
|
return 2
|
|
}
|
|
|
|
local sessions_dir="${FN_FLEET_SESSIONS_DIR:-$HOME/.claude/sessions}"
|
|
local goals_dir="${FN_FLEET_GOALS_DIR:-$HOME/.claude/goals}"
|
|
[[ -z "$socket" ]] && socket="${FLEET_SOCKET:-fleet}"
|
|
|
|
command -v jq >/dev/null 2>&1 || {
|
|
echo "kill_fleet_agent: jq no esta instalado (necesario para leer los JSON)" >&2
|
|
return 1
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Resolver (PID, sessionId) a partir del target.
|
|
# -----------------------------------------------------------------------
|
|
local pid="" sid=""
|
|
if [[ "$target" =~ ^[0-9]+$ ]]; then
|
|
# target = PID. El sessionId sale de sessions/<pid>.json (si existe).
|
|
pid="$target"
|
|
local sfile="$sessions_dir/$pid.json"
|
|
if [[ -f "$sfile" ]]; then
|
|
sid="$(jq -r '.sessionId // ""' "$sfile" 2>/dev/null || true)"
|
|
fi
|
|
else
|
|
# target = sessionId (exacto o prefijo). Buscar en sessions/*.json el JSON
|
|
# cuyo .sessionId case; el nombre del archivo (<pid>.json) da el PID.
|
|
local f base candidate_sid
|
|
for f in "$sessions_dir"/*.json; do
|
|
[[ -f "$f" ]] || continue
|
|
candidate_sid="$(jq -r '.sessionId // ""' "$f" 2>/dev/null || true)"
|
|
[[ -z "$candidate_sid" ]] && continue
|
|
if [[ "$candidate_sid" == "$target" || "$candidate_sid" == "$target"* ]]; then
|
|
base="$(basename "$f" .json)"
|
|
pid="$base"
|
|
sid="$candidate_sid"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
[[ -z "$pid" ]] && {
|
|
echo "kill_fleet_agent: no se pudo resolver el target '$target' a un PID (sessions en $sessions_dir)" >&2
|
|
return 2
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Guard 1 — anti-self: no matar a la sesion que invoca la funcion.
|
|
# -----------------------------------------------------------------------
|
|
local self_pid="${FN_FLEET_SELF_PID:-}"
|
|
if [[ -z "$self_pid" ]]; then
|
|
local walk="$$" guard=0 comm
|
|
while [[ -n "$walk" && "$walk" != "0" && "$walk" != "1" ]]; do
|
|
comm="$(cat "/proc/$walk/comm" 2>/dev/null || true)"
|
|
if [[ "$comm" == "claude" ]]; then
|
|
self_pid="$walk"
|
|
break
|
|
fi
|
|
walk="$(awk '{print $4}' "/proc/$walk/stat" 2>/dev/null || true)"
|
|
guard=$((guard + 1))
|
|
[[ "$guard" -gt 64 ]] && break
|
|
done
|
|
fi
|
|
if [[ -n "$self_pid" && "$pid" == "$self_pid" ]]; then
|
|
echo "kill_fleet_agent: REHUSADO — el target (PID $pid) es la sesion actual. No me suicido." >&2
|
|
return 3
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Guard 2 — anti-orquestador: no matar a un role=orchestrator.
|
|
# -----------------------------------------------------------------------
|
|
local role=""
|
|
if [[ -n "$sid" ]]; then
|
|
local gfile="$goals_dir/$sid.json"
|
|
[[ -f "$gfile" ]] && role="$(jq -r '.role // ""' "$gfile" 2>/dev/null || true)"
|
|
fi
|
|
if [[ "$role" == "orchestrator" ]]; then
|
|
echo "kill_fleet_agent: REHUSADO — el target (sessionId ${sid:-?}, PID $pid) tiene role=orchestrator. No se mata al orquestador." >&2
|
|
return 3
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Resolver la window tmux del PID en el socket (pane_pid == claude por el
|
|
# `exec claude` de spawn_fleet_agent). Best-effort: vacio si no hay socket.
|
|
# -----------------------------------------------------------------------
|
|
local window=""
|
|
if command -v tmux >/dev/null 2>&1; then
|
|
window="$(tmux -L "$socket" list-panes -a -F '#{pane_pid} #{window_id}' 2>/dev/null \
|
|
| awk -v p="$pid" '$1==p {print $2; exit}' || true)"
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Plan (se imprime siempre).
|
|
# -----------------------------------------------------------------------
|
|
echo "kill_fleet_agent — target: $target PID: $pid sessionId: ${sid:-?} role: ${role:-executor} socket: $socket window: ${window:-(no resuelta)}"
|
|
|
|
if [[ "$dry" -eq 1 ]]; then
|
|
echo "DRY-RUN: no se ha matado el proceso ni cerrado la window."
|
|
return 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Ejecutar: SIGTERM al claude (cierre limpio) + kill-window (idempotente).
|
|
# -----------------------------------------------------------------------
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
kill "$pid" 2>/dev/null || true
|
|
echo "kill_fleet_agent: SIGTERM enviado a claude PID $pid."
|
|
else
|
|
echo "kill_fleet_agent: PID $pid ya no esta vivo (nada que matar)."
|
|
fi
|
|
|
|
if [[ -n "$window" ]] && command -v tmux >/dev/null 2>&1; then
|
|
tmux -L "$socket" kill-window -t "$window" 2>/dev/null || true
|
|
echo "kill_fleet_agent: window $window cerrada en el socket $socket."
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
kill_fleet_agent "$@"
|
|
fi
|