feat(statusline): seguimiento de objetivo + fase por terminal
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>
This commit is contained in:
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Stop hook: tras cada respuesta del asistente, dispara (en background) la
|
||||
# clasificacion de la fase de la tarea. Lee la ultima respuesta del transcript,
|
||||
# la clasifica con ask_llm (haiku) y escribe el resultado en el goal JSON de la
|
||||
# sesion. El statusline lo pinta en el siguiente render.
|
||||
#
|
||||
# No bloquea el cierre del turno: el trabajo pesado va al worker en background.
|
||||
|
||||
INPUT=$(cat)
|
||||
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
|
||||
|
||||
[ -z "$SID" ] && exit 0
|
||||
|
||||
F="$HOME/.claude/goals/${SID}.json"
|
||||
# Solo si esta terminal tiene un objetivo fijado.
|
||||
[ -f "$F" ] || exit 0
|
||||
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
|
||||
[ -z "$GOAL" ] && exit 0
|
||||
|
||||
[ -z "$TRANSCRIPT" ] && exit 0
|
||||
[ -f "$TRANSCRIPT" ] || exit 0
|
||||
|
||||
# Lanzar clasificacion en background; el hook retorna de inmediato.
|
||||
nohup bash "$HOME/.claude/hooks/goal_phase_worker.sh" "$SID" "$TRANSCRIPT" "$F" >/dev/null 2>&1 &
|
||||
exit 0
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/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
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# UserPromptSubmit hook del sistema de objetivo+fase por terminal.
|
||||
#
|
||||
# Dos funciones:
|
||||
# 1) Si tu prompt empieza por "objetivo:" (o "meta:" / "goal:"), fija el objetivo
|
||||
# de esta terminal leyendolo DIRECTAMENTE de tu prompt, sin intervencion del
|
||||
# modelo. "objetivo: clear" (o -, none, borrar, quitar, reset) lo elimina.
|
||||
# 2) En cualquier otro caso, inyecta el estado actual (goal+phase) como contexto
|
||||
# para que el modelo sepa donde esta el archivo y que objetivo hay vigente.
|
||||
#
|
||||
# La FASE no la toca este hook: la mantiene el Stop hook (goal_phase_eval.sh).
|
||||
|
||||
INPUT=$(cat)
|
||||
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
[ -z "$SID" ] && exit 0
|
||||
|
||||
F="$HOME/.claude/goals/${SID}.json"
|
||||
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // ""' 2>/dev/null)
|
||||
|
||||
# 1) Fijar/limpiar el objetivo leyendo el prompt del usuario.
|
||||
GOAL_LINE=$(printf '%s' "$PROMPT" | grep -ioE '^[[:space:]]*(objetivo|meta|goal)[[:space:]]*:[[:space:]]*.+' | head -1)
|
||||
if [ -n "$GOAL_LINE" ]; then
|
||||
NEWGOAL=$(printf '%s' "$GOAL_LINE" | sed -E 's/^[^:]*:[[:space:]]*//; s/[[:space:]]+$//')
|
||||
case "$NEWGOAL" in
|
||||
-|clear|none|borrar|quitar|reset)
|
||||
rm -f "$F"
|
||||
echo "GOAL-TRACKER: objetivo de esta terminal borrado. El statusline deja de mostrar la linea de objetivo."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
if [ -f "$F" ]; then
|
||||
PH=$(jq -r '.phase // "planificando"' "$F" 2>/dev/null)
|
||||
else
|
||||
PH="planificando"
|
||||
fi
|
||||
TMP="${F}.tmp.$$"
|
||||
if jq -n --arg g "$NEWGOAL" --arg p "$PH" '{goal:$g, phase:$p}' > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$F"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
echo "GOAL-TRACKER: objetivo fijado desde tu prompt -> \"$NEWGOAL\" (phase=$PH). El statusline ya lo muestra; la fase la mantiene el Stop hook automaticamente."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2) Informativo: estado actual para el modelo.
|
||||
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."
|
||||
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
|
||||
exit 0
|
||||
@@ -13,6 +13,22 @@
|
||||
"Write(.git/**)"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "~/.claude/hooks/goal_tracker.sh" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "~/.claude/hooks/goal_phase_eval.sh" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "~/.claude/statusline.sh",
|
||||
|
||||
@@ -23,6 +23,11 @@ MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "Unknown"')
|
||||
CONTEXT_PCT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0' | xargs printf "%.0f")
|
||||
CONTEXT_TOTAL=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 200000')
|
||||
CURRENT_DIR=$(echo "$INPUT" | jq -r '.workspace.current_dir // "~"' | sed "s|$HOME|~|")
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
||||
|
||||
# Purga: borra goal files de sesiones muertas (no tocados en >7 dias). El worker
|
||||
# refresca el mtime en cada respuesta, asi que las sesiones vivas nunca caen.
|
||||
find "$HOME/.claude/goals" -maxdepth 1 -name '*.json' -mtime +7 -delete 2>/dev/null
|
||||
|
||||
# Tokens de entrada y salida (current_usage puede ser null antes del primer API call)
|
||||
INPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.input_tokens // 0')
|
||||
@@ -91,6 +96,34 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
GIT_STATUS=$(echo "$GIT_STATUS" | sed 's/ $//')
|
||||
fi
|
||||
|
||||
# Color estable por sesión (hash del session_id → paleta ANSI 256 legible).
|
||||
# Cada terminal mantiene su color toda su vida; distinto entre terminales.
|
||||
goal_color() {
|
||||
local sid="$1"
|
||||
local palette=(39 45 51 75 81 114 120 156 183 210 215 222 213 159 228)
|
||||
local h
|
||||
h=$(printf '%s' "$sid" | cksum | cut -d' ' -f1)
|
||||
local idx=$(( h % ${#palette[@]} ))
|
||||
printf '\033[1;38;5;%dm' "${palette[$idx]}"
|
||||
}
|
||||
|
||||
# Fase de trabajo → icono | color ANSI | etiqueta visible.
|
||||
# El slug (clave) lo escribe el agente del Stop hook; aqui se mapea a su estilo.
|
||||
phase_style() {
|
||||
case "$1" in
|
||||
investigando) printf '🔎|36|investigando' ;;
|
||||
planificando) printf '📋|34|planificando' ;;
|
||||
haciendo) printf '🔨|33|haciendo' ;;
|
||||
testeando) printf '🧪|35|testeando' ;;
|
||||
puliendo) printf '✨|95|puliendo detalles' ;;
|
||||
iterando) printf '🔁|94|iterando' ;;
|
||||
pendiente_revision) printf '👀|93|pendiente de revisión' ;;
|
||||
bloqueado) printf '⛔|31|bloqueado' ;;
|
||||
hecho) printf '✅|32|hecho' ;;
|
||||
*) printf "•|90|$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Función para crear barra de progreso
|
||||
progress_bar() {
|
||||
local pct=$1
|
||||
@@ -251,6 +284,30 @@ fi
|
||||
# 6. Directorio actual
|
||||
LINE2="${LINE2} ${GRAY}│${RESET} ${BLUE}${CURRENT_DIR}${RESET}"
|
||||
|
||||
# ===== LÍNEA 0: Objetivo (izq) + Fase (der) =====
|
||||
# Solo si la sesión tiene archivo de objetivo con goal no vacío.
|
||||
GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json"
|
||||
if [ -n "$SESSION_ID" ] && [ -f "$GOAL_FILE" ]; then
|
||||
GOAL=$(jq -r '.goal // ""' "$GOAL_FILE" 2>/dev/null)
|
||||
PHASE=$(jq -r '.phase // ""' "$GOAL_FILE" 2>/dev/null)
|
||||
if [ -n "$GOAL" ]; then
|
||||
GC=$(goal_color "$SESSION_ID")
|
||||
LEFT_PLAIN="🎯 ${GOAL}"
|
||||
LEFT="${GC}${LEFT_PLAIN}${RESET}"
|
||||
|
||||
LINE0="${LEFT}"
|
||||
if [ -n "$PHASE" ]; then
|
||||
PS=$(phase_style "$PHASE")
|
||||
PICON="${PS%%|*}"
|
||||
REST="${PS#*|}"
|
||||
PCOL="${REST%%|*}"
|
||||
PLABEL="${REST#*|}"
|
||||
LINE0="${LINE0} ${GRAY}│${RESET} \033[1;${PCOL}m${PICON} ${PLABEL}${RESET}"
|
||||
fi
|
||||
echo -e "$LINE0"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Imprimir resultado (2 líneas)
|
||||
echo -e "$LINE1"
|
||||
echo -e "$LINE2"
|
||||
|
||||
Reference in New Issue
Block a user