5efcedf9ba
Cada terminal muestra su objetivo (color estable por session_id) y la fase de
trabajo actual, para distinguir sesiones y saber cuando algo esta hecho.
- statusline.sh: linea 0 con objetivo (izq, color por sesion) + fase (der) con
el separador estandar; 9 fases (investigando, planificando, haciendo,
testeando, puliendo, iterando, pendiente_revision, bloqueado, hecho) con icono,
color y etiqueta. Purga de goal files de sesiones muertas (>7 dias).
- hooks/goal_tracker.sh (UserPromptSubmit): fija el objetivo leyendo el prompt
del usuario ("objetivo: ...", "objetivo: clear" lo borra); si no, informa el
estado actual al modelo.
- hooks/goal_phase_eval.sh (Stop): al terminar el turno lanza el worker en
background, sin bloquear.
- hooks/goal_phase_worker.sh: clasifica la fase con ask_llm (haiku, API directa,
nunca claude -p) usando la peticion del usuario + la ultima respuesta del
asistente. Solo reevalua si el turno tuvo trabajo real (tool_use); en charla
pura no toca la fase ni gasta llamada.
- settings.json: registra los hooks UserPromptSubmit y Stop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.6 KiB
Bash
Executable File
102 lines
4.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Worker del Stop hook (corre en background). Clasifica la fase de la tarea a
|
|
# partir de la ultima respuesta del asistente y la escribe en el goal JSON.
|
|
#
|
|
# Args: <session_id> <transcript_path> <goal_json_file>
|
|
#
|
|
# Usa ask_llm (grupo claude-direct, API directa, arranque 0) con haiku. NUNCA
|
|
# usa `claude -p` (lento, cold start). Ver regla llm_invocation.md del registry.
|
|
|
|
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
|
|
|
|
# Una sola pasada de abajo a arriba sobre el transcript del turno actual:
|
|
# - captura la ultima respuesta de texto del asistente (para clasificar)
|
|
# - detecta si en este turno hubo TRABAJO REAL (algun tool_use: edits, bash...)
|
|
# El recorrido se detiene al llegar al prompt humano que abrio el turno.
|
|
# Si el turno fue solo charla (sin tool_use), NO se reevalua la fase ni se gasta
|
|
# una llamada al modelo: la fase se queda como estaba.
|
|
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")
|
|
|
|
# Charla pura (sin trabajo en el turno): no tocar la fase.
|
|
[ "$HAS_WORK" = "0" ] && exit 0
|
|
|
|
LAST=$(printf '%s' "$LAST" | tail -c 4000)
|
|
[ -z "$LAST" ] && exit 0
|
|
# La peticion del usuario marca la INTENCION del turno (ej. "testealo" -> testeando).
|
|
USER_MSG=$(printf '%s' "$USER_MSG" | tail -c 1500)
|
|
|
|
SYS="Eres un clasificador de fase de tarea. Recibes el objetivo, la ULTIMA PETICION DEL USUARIO (marca la intencion del turno) y la ULTIMA RESPUESTA DEL ASISTENTE. Clasifica la actividad de ESTE turno con UNA sola palabra de esta lista exacta, sin nada mas: investigando planificando haciendo testeando puliendo iterando pendiente_revision bloqueado hecho. Definiciones: investigando=leyendo o explorando codigo para entender; planificando=disenando el enfoque sin ejecutar todavia; haciendo=implementando cambios; testeando=corriendo tests, probando o validando tecnicamente (si el usuario pide 'testea/prueba/valida', es testeando); puliendo=retoques finales sobre algo ya casi listo; iterando=ciclo continuo de ajustes pequenos; pendiente_revision=el asistente espera que el humano revise o decida algo importante; bloqueado=no puede avanzar por un error o falta de informacion; hecho=el objetivo esta claramente terminado y verificado. La peticion del usuario pesa: refleja que se pidio hacer en este turno. Usa 'hecho' SOLO si el trabajo esta completo y confirmado, nunca si queda algo pendiente."
|
|
|
|
PROMPT="OBJETIVO DE LA TAREA: ${GOAL}
|
|
|
|
ULTIMA PETICION DEL USUARIO:
|
|
${USER_MSG}
|
|
|
|
ULTIMA RESPUESTA DEL ASISTENTE:
|
|
${LAST}
|
|
|
|
Responde la fase actual (una sola palabra de la lista):"
|
|
|
|
RAW=$("$PY" "$ASK" --model claude-haiku-4-5-20251001 --system "$SYS" "$PROMPT" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
[ -z "$RAW" ] && exit 0
|
|
|
|
# Normalizar la respuesta a un slug canonico (tolerante a verbosidad/acentos).
|
|
case "$RAW" in
|
|
*pendiente*revis*|*revis*) PHASE=pendiente_revision ;;
|
|
*investig*) PHASE=investigando ;;
|
|
*planific*) PHASE=planificando ;;
|
|
*test*) PHASE=testeando ;;
|
|
*puli*) PHASE=puliendo ;;
|
|
*iter*) PHASE=iterando ;;
|
|
*bloque*) PHASE=bloqueado ;;
|
|
*hecho*|*complet*|*termin*|*done*) PHASE=hecho ;;
|
|
*hacien*|*implement*) PHASE=haciendo ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
|
|
# Escribir la fase preservando el resto del JSON.
|
|
TMP="${F}.tmp.$$"
|
|
if jq --arg p "$PHASE" '.phase=$p' "$F" > "$TMP" 2>/dev/null; then
|
|
mv "$TMP" "$F"
|
|
else
|
|
rm -f "$TMP"
|
|
fi
|
|
exit 0
|