diff --git a/.claude/hooks/goal_phase_worker.sh b/.claude/hooks/goal_phase_worker.sh index 6d835b0..c0270f4 100755 --- a/.claude/hooks/goal_phase_worker.sh +++ b/.claude/hooks/goal_phase_worker.sh @@ -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: +# 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: [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" diff --git a/.claude/hooks/goal_tracker.sh b/.claude/hooks/goal_tracker.sh index 2e077ce..d7fc7ec 100755 --- a/.claude/hooks/goal_tracker.sh +++ b/.claude/hooks/goal_tracker.sh @@ -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: \". 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: \"; 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: \" (lo captura este hook directo de su prompt). Si describe una tarea clara sin ese prefijo, crea {\"goal\":\"\",\"phase\":\"planificando\"} leyendo su prompt. Sin objetivo, ignora." fi diff --git a/.claude/statusline.sh b/.claude/statusline.sh index 2209c2a..fcdb32d 100755 --- a/.claude/statusline.sh +++ b/.claude/statusline.sh @@ -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%%|*}"