#!/bin/bash # Worker del Stop hook: resuelve el estado de REPOSO de la tarea cuando el # asistente para y cede el control. Clasifica con ask_llm (haiku, API directa; # nunca `claude -p`, ver regla llm_invocation.md) y lo escribe en el goal JSON # manteniendo el historial. # # El estado ACTIVO (mientras se trabaja: investigando/haciendo/testeando) lo # marca el hook PostToolUse (goal_phase_active.sh), de forma determinista. Este # worker SOLO produce estados de reposo: hecho, pendiente_revision, bloqueado, # en_pausa. # # Args: SID="$1" TRANSCRIPT="$2" F="$3" PY="$HOME/fn_registry/python/.venv/bin/python3" ASK="$HOME/fn_registry/python/functions/core/ask_llm.py" [ -x "$PY" ] || exit 0 [ -f "$ASK" ] || exit 0 [ -f "$F" ] || exit 0 [ -f "$TRANSCRIPT" ] || exit 0 GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null) [ -z "$GOAL" ] && exit 0 CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null) DOD=$(jq -r '.dod // ""' "$F" 2>/dev/null) [ -z "$DOD" ] && DOD="(no definido)" is_active() { case "$1" in investigando|planificando|haciendo|testeando|puliendo) return 0 ;; *) return 1 ;; esac } # Una pasada de abajo a arriba sobre el turno actual: ultima respuesta de texto # del asistente + ultima peticion del usuario + si hubo trabajo (tool_use). LAST="" USER_MSG="" HAS_WORK=0 while IFS= read -r line; do t=$(printf '%s' "$line" | jq -r '.type // empty' 2>/dev/null) if [ "$t" = "assistant" ]; then if [ -z "$LAST" ]; then txt=$(printf '%s' "$line" | jq -r '(.message.content // [])[]? | select(.type=="text") | .text' 2>/dev/null) [ -n "$txt" ] && LAST="$txt" fi if printf '%s' "$line" | jq -e '(.message.content // [])[]? | select(.type=="tool_use")' >/dev/null 2>&1; then HAS_WORK=1 fi elif [ "$t" = "user" ]; then ctype=$(printf '%s' "$line" | jq -r '.message.content | type' 2>/dev/null) if [ "$ctype" = "string" ]; then USER_MSG=$(printf '%s' "$line" | jq -r '.message.content' 2>/dev/null) break fi if ! printf '%s' "$line" | jq -e '(.message.content // [])[]? | select(.type=="tool_result")' >/dev/null 2>&1; then USER_MSG=$(printf '%s' "$line" | jq -r '(.message.content // [])[]? | select(.type=="text") | .text' 2>/dev/null) break fi fi done < <(tac "$TRANSCRIPT") # Solo resolver reposo si hubo trabajo este turno o si veniamos de un estado # activo (paramos tras currar). Charla sobre un reposo previo: no tocar. if [ "$HAS_WORK" = "0" ] && ! is_active "$CUR"; then exit 0 fi LAST=$(printf '%s' "$LAST" | tail -c 4000) [ -z "$LAST" ] && exit 0 USER_MSG=$(printf '%s' "$USER_MSG" | tail -c 1500) SYS="El asistente acaba de PARAR y cede el control al usuario. Clasifica el estado de REPOSO en que queda la tarea. Responde UNA sola palabra, sin nada mas, de: hecho pendiente_revision preguntando bloqueado en_pausa sin_cambio. hecho=el objetivo esta completo y verificado; pendiente_revision=el asistente termino un trabajo y espera que el humano lo revise o apruebe (no hace una pregunta directa); preguntando=el asistente termina formulando una o varias PREGUNTAS concretas al usuario y necesita su respuesta o decision para continuar; bloqueado=no puede avanzar por un error o por falta de informacion/acceso; en_pausa=hizo un avance y espera la siguiente indicacion, sin estar terminado ni preguntar ni bloqueado; sin_cambio=el turno no altera el estado de reposo actual (charla irrelevante). Distingue: si la respuesta acaba con preguntas al usuario es 'preguntando'; si deja un resultado para que lo mire es 'pendiente_revision'. REGLA DEL DoD: se te da el DoD (definition of done) que define cuando la tarea esta TERMINADA. Marca 'hecho' UNICAMENTE si el resultado descrito por el asistente CUMPLE ese DoD de forma clara y verificada; compara punto por punto el resultado contra el DoD. Si el DoD no se cumple del todo, o no esta verificado, NO uses 'hecho': usa 'pendiente_revision' (dejas un resultado para que el humano lo revise) o 'en_pausa' (avance parcial). Si el DoD es '(no definido)', usa tu criterio: 'hecho' solo si el objetivo esta claramente completo y verificado." PROMPT="OBJETIVO DE LA TAREA: ${GOAL} DEFINITION OF DONE (DoD) — la tarea esta TERMINADA solo si esto se cumple: ${DOD} ULTIMA PETICION DEL USUARIO: ${USER_MSG} ULTIMA RESPUESTA DEL ASISTENTE: ${LAST} Compara el resultado contra el DoD y responde con una sola palabra de la lista permitida:" RAW=$("$PY" "$ASK" --model claude-haiku-4-5-20251001 --system "$SYS" "$PROMPT" 2>/dev/null | tr '[:upper:]' '[:lower:]') [ -z "$RAW" ] && exit 0 case "$RAW" in *sin_cambio*|*sincambio*|*ninguna*|*charla*) exit 0 ;; *pregunt*|*consulta*|*respuesta*) PHASE=preguntando ;; *pendiente*revis*|*revis*|*aprob*) PHASE=pendiente_revision ;; *bloque*) PHASE=bloqueado ;; *hecho*|*complet*|*termin*|*done*) PHASE=hecho ;; *pausa*|*pause*|*siguiente*) PHASE=en_pausa ;; # Si por error devuelve un estado activo al parar, lo tratamos como pausa. investigando|planificando|haciendo|testeando|puliendo) PHASE=en_pausa ;; *) exit 0 ;; esac # Escribir la fase + mantener historial (append solo si cambia respecto al # ultimo; se conservan los ultimos 12 estados). TMP="${F}.tmp.$$" if jq --arg p "$PHASE" ' .phase = $p | .history = ( ( .history // [] ) as $h | ( if ($h | length) > 0 and ($h[-1] == $p) then $h else ($h + [$p]) end ) | .[-12:] ) ' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F" else rm -f "$TMP" fi exit 0