feat(statusline): historial de estados + clasificacion al escribir el usuario
- statusline.sh: muestra los ultimos 7 estados previos como emojis atenuados (DIM) separados por │, entre el objetivo y la fase actual. El historial se guarda en el goal JSON (campo .history), colapsando estados consecutivos repetidos, hasta 12 entradas. - goal_phase_worker.sh: dos modos. 'stop' (tras la respuesta del asistente, con filtro de trabajo real) y 'prompt' (tras el prompt del usuario, clasifica la intencion para feedback inmediato). Nuevo veredicto 'sin_cambio' para preguntas/charla que no implican cambio de actividad; ante la duda, no toca. Ambos modos mantienen el historial. - goal_tracker.sh: en cada prompt con objetivo activo lanza el worker en modo prompt (background) ademas del Stop hook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
#!/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.
|
||||
# Worker del sistema objetivo+fase. Clasifica la fase de la tarea con ask_llm
|
||||
# (haiku, API directa; nunca `claude -p`, ver regla llm_invocation.md) y la
|
||||
# escribe en el goal JSON manteniendo un historial de estados.
|
||||
#
|
||||
# Args: <session_id> <transcript_path> <goal_json_file>
|
||||
# Dos modos:
|
||||
# stop (default): se lanza tras una respuesta del asistente. Lee el ultimo
|
||||
# turno del transcript y SOLO reevalua si hubo trabajo real (tool_use);
|
||||
# en turnos de charla pura no toca nada.
|
||||
# prompt : se lanza tras un prompt del usuario, para feedback inmediato. Usa la
|
||||
# intencion del prompt. Si el prompt es charla/pregunta y no implica
|
||||
# avanzar la tarea, el modelo responde sin_cambio y no se toca nada.
|
||||
#
|
||||
# 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.
|
||||
# Args: <session_id> <transcript_path> <goal_json> [mode] [prompt_text]
|
||||
|
||||
SID="$1"
|
||||
TRANSCRIPT="$2"
|
||||
F="$3"
|
||||
MODE="${4:-stop}"
|
||||
PROMPT_ARG="$5"
|
||||
|
||||
PY="$HOME/fn_registry/python/.venv/bin/python3"
|
||||
ASK="$HOME/fn_registry/python/functions/core/ask_llm.py"
|
||||
@@ -17,52 +25,55 @@ 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")
|
||||
LAST=""
|
||||
|
||||
# Charla pura (sin trabajo en el turno): no tocar la fase.
|
||||
[ "$HAS_WORK" = "0" ] && exit 0
|
||||
if [ "$MODE" = "prompt" ]; then
|
||||
# Modo prompt: clasifica la intencion del mensaje recien enviado.
|
||||
USER_MSG="$PROMPT_ARG"
|
||||
[ -z "$USER_MSG" ] && exit 0
|
||||
LAST="(el usuario acaba de enviar este mensaje; aun no hay respuesta del asistente)"
|
||||
else
|
||||
# Modo stop: una sola pasada de abajo a arriba sobre el turno actual.
|
||||
# Captura la ultima respuesta de texto del asistente + detecta trabajo real.
|
||||
[ -f "$TRANSCRIPT" ] || exit 0
|
||||
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
|
||||
fi
|
||||
|
||||
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."
|
||||
SYS="Eres un clasificador de fase de tarea. Recibes el objetivo, la ULTIMA PETICION DEL USUARIO (marca la intencion) y la ULTIMA RESPUESTA DEL ASISTENTE. Clasifica la actividad actual con UNA sola palabra de esta lista exacta, sin nada mas: investigando planificando haciendo testeando puliendo iterando pendiente_revision bloqueado hecho sin_cambio. Definiciones: investigando=explorando o leyendo ACTIVAMENTE codigo/archivos del proyecto como paso para avanzar la tarea (no una simple pregunta conceptual); planificando=disenando el enfoque sin ejecutar todavia; haciendo=implementando cambios; testeando=corriendo tests, probando o validando tecnicamente (si piden '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; sin_cambio=la peticion NO implica un cambio de actividad: preguntas, peticiones de explicacion o aclaracion conceptual, comentarios, opiniones o dudas que no ordenan avanzar, modificar o probar la tarea. La peticion del usuario pesa: refleja que se pidio hacer. Usa 'hecho' SOLO si el trabajo esta completo y confirmado. Ante la duda entre una fase concreta y sin_cambio, prefiere sin_cambio."
|
||||
|
||||
PROMPT="OBJETIVO DE LA TAREA: ${GOAL}
|
||||
|
||||
@@ -72,13 +83,14 @@ ${USER_MSG}
|
||||
ULTIMA RESPUESTA DEL ASISTENTE:
|
||||
${LAST}
|
||||
|
||||
Responde la fase actual (una sola palabra de la lista):"
|
||||
Responde la fase actual (una sola palabra de la lista, o sin_cambio):"
|
||||
|
||||
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
|
||||
*sin_cambio*|*sincambio*|*ninguna*|*charla*) exit 0 ;;
|
||||
*pendiente*revis*|*revis*) PHASE=pendiente_revision ;;
|
||||
*investig*) PHASE=investigando ;;
|
||||
*planific*) PHASE=planificando ;;
|
||||
@@ -91,9 +103,17 @@ case "$RAW" in
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# Escribir la fase preservando el resto del JSON.
|
||||
# Escribir la fase + mantener el historial (append solo si cambia respecto al
|
||||
# ultimo, para no llenar de repetidos; se conservan los ultimos 12 estados).
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq --arg p "$PHASE" '.phase=$p' "$F" > "$TMP" 2>/dev/null; then
|
||||
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"
|
||||
|
||||
@@ -43,11 +43,15 @@ if [ -n "$GOAL_LINE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2) Informativo: estado actual para el modelo.
|
||||
# 2) Informativo + clasificacion inmediata desde el prompt.
|
||||
if [ -f "$F" ]; then
|
||||
G=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
P=$(jq -r '.phase // ""' "$F" 2>/dev/null)
|
||||
echo "GOAL-TRACKER: file=$F | goal=\"$G\" phase=\"$P\". La fase la mantiene el Stop hook automaticamente — NO escribas la fase. El usuario fija el objetivo escribiendo \"objetivo: <texto>\". Si redefine la tarea en lenguaje natural sin ese prefijo, actualiza \"goal\" en ese JSON leyendo su prompt."
|
||||
# Clasifica la fase ya, desde la intencion del prompt (modo prompt, background).
|
||||
# El Stop hook la reevaluara despues con el resultado de la respuesta.
|
||||
TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null)
|
||||
nohup bash "$HOME/.claude/hooks/goal_phase_worker.sh" "$SID" "$TRANSCRIPT" "$F" prompt "$PROMPT" >/dev/null 2>&1 &
|
||||
echo "GOAL-TRACKER: file=$F | goal=\"$G\" phase=\"$P\". La fase la mantienen los hooks automaticamente (tu prompt + mi respuesta) — NO escribas la fase. El usuario fija el objetivo escribiendo \"objetivo: <texto>\"; si redefine la tarea en lenguaje natural, actualiza \"goal\" en ese JSON."
|
||||
else
|
||||
echo "GOAL-TRACKER: file=$F (sin objetivo aun). El usuario fija el objetivo de la terminal escribiendo \"objetivo: <texto>\" (lo captura este hook directo de su prompt). Si describe una tarea clara sin ese prefijo, crea {\"goal\":\"<su objetivo>\",\"phase\":\"planificando\"} leyendo su prompt. Sin objetivo, ignora."
|
||||
fi
|
||||
|
||||
@@ -296,6 +296,22 @@ if [ -n "$SESSION_ID" ] && [ -f "$GOAL_FILE" ]; then
|
||||
LEFT="${GC}${LEFT_PLAIN}${RESET}"
|
||||
|
||||
LINE0="${LEFT}"
|
||||
|
||||
# Historial: emojis de los ultimos 7 estados PREVIOS (sin el actual, que
|
||||
# se muestra completo a la derecha), atenuados y separados por │.
|
||||
PREV=$(jq -r '(.history // []) | .[0:-1] | .[-7:] | .[]' "$GOAL_FILE" 2>/dev/null)
|
||||
if [ -n "$PREV" ]; then
|
||||
HJOIN=""
|
||||
while IFS= read -r slug; do
|
||||
[ -z "$slug" ] && continue
|
||||
HS=$(phase_style "$slug")
|
||||
HIC="${HS%%|*}"
|
||||
if [ -z "$HJOIN" ]; then HJOIN="$HIC"; else HJOIN="${HJOIN} │ ${HIC}"; fi
|
||||
done <<< "$PREV"
|
||||
[ -n "$HJOIN" ] && LINE0="${LINE0} ${GRAY}│${RESET} ${DIM}${HJOIN}${RESET}"
|
||||
fi
|
||||
|
||||
# Fase actual (completa, con color e icono).
|
||||
if [ -n "$PHASE" ]; then
|
||||
PS=$(phase_style "$PHASE")
|
||||
PICON="${PS%%|*}"
|
||||
|
||||
Reference in New Issue
Block a user