diff --git a/.claude/hooks/goal_phase_active.sh b/.claude/hooks/goal_phase_active.sh new file mode 100755 index 0000000..5d61fdc --- /dev/null +++ b/.claude/hooks/goal_phase_active.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# PostToolUse hook: marca el estado ACTIVO de la tarea segun la herramienta que +# el asistente acaba de usar. Determinista, sin LLM, en tiempo real. Solo actua +# si la terminal tiene un objetivo fijado. +# +# El estado de REPOSO (al parar: hecho/pendiente_revision/bloqueado/en_pausa) lo +# pone el Stop hook (goal_phase_eval.sh + goal_phase_worker.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" +[ -f "$F" ] || exit 0 +GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null) +[ -z "$GOAL" ] && exit 0 + +TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) +[ -z "$TOOL" ] && exit 0 + +PHASE="" +case "$TOOL" in + Read|Grep|Glob|NotebookRead|WebFetch|WebSearch|ToolSearch) + PHASE=investigando ;; + Edit|Write|MultiEdit|NotebookEdit) + PHASE=haciendo ;; + Task|Agent|Workflow) + PHASE=haciendo ;; + Bash) + CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null | tr '[:upper:]' '[:lower:]') + case "$CMD" in + *pytest*|*"go test"*|*ctest*|*jest*|*vitest*|*"npm test"*|*"npm run test"*|*"cargo test"*|*unittest*|*" test "*|*"./fn run"*test*) + PHASE=testeando ;; + ls|ls\ *|cat\ *|*grep*|find\ *|head\ *|tail\ *|stat\ *|tree*|rg\ *|fd\ *|*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) + PHASE=investigando ;; + *) + PHASE=haciendo ;; + esac + ;; + mcp__registry__fn_search|mcp__registry__fn_show|mcp__registry__fn_code|mcp__registry__fn_uses|mcp__registry__fn_list_domains|mcp__registry__fn_doctor|mcp__registry__fn_proposal) + PHASE=investigando ;; + mcp__registry__fn_run) + PHASE=haciendo ;; + *) + # Herramientas que no representan un cambio de actividad (TodoWrite, + # AskUserQuestion, etc.): no tocar la fase. + exit 0 ;; +esac +[ -z "$PHASE" ] && exit 0 + +# 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 diff --git a/.claude/hooks/goal_phase_worker.sh b/.claude/hooks/goal_phase_worker.sh index c0270f4..5d09842 100755 --- a/.claude/hooks/goal_phase_worker.sh +++ b/.claude/hooks/goal_phase_worker.sh @@ -1,23 +1,19 @@ #!/bin/bash -# 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. +# 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. # -# 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. +# 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: [mode] [prompt_text] +# Args: 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" @@ -25,55 +21,58 @@ 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) -USER_MSG="" +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="" - -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 +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 - done < <(tac "$TRANSCRIPT") + 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 +# 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="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." +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 bloqueado en_pausa sin_cambio. hecho=el objetivo esta completo y verificado; pendiente_revision=el asistente espera que el humano revise un resultado o decida algo importante; bloqueado=no puede avanzar por un error o por falta de informacion del usuario; en_pausa=hizo un avance y espera la siguiente indicacion, sin estar terminado ni bloqueado ni a la espera de revision; sin_cambio=el turno no altera el estado de reposo actual (charla irrelevante). Usa 'hecho' SOLO si el trabajo esta completo y confirmado, nunca si queda algo pendiente." PROMPT="OBJETIVO DE LA TAREA: ${GOAL} @@ -83,28 +82,24 @@ ${USER_MSG} ULTIMA RESPUESTA DEL ASISTENTE: ${LAST} -Responde la fase actual (una sola palabra de la lista, o sin_cambio):" +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 -# 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 ;; - *test*) PHASE=testeando ;; - *puli*) PHASE=puliendo ;; - *iter*) PHASE=iterando ;; - *bloque*) PHASE=bloqueado ;; - *hecho*|*complet*|*termin*|*done*) PHASE=hecho ;; - *hacien*|*implement*) PHASE=haciendo ;; + *pendiente*revis*|*revis*) PHASE=pendiente_revision ;; + *bloque*) PHASE=bloqueado ;; + *hecho*|*complet*|*termin*|*done*) PHASE=hecho ;; + *pausa*|*pause*|*espera*|*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 el historial (append solo si cambia respecto al -# ultimo, para no llenar de repetidos; se conservan los ultimos 12 estados). +# 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 diff --git a/.claude/hooks/goal_tracker.sh b/.claude/hooks/goal_tracker.sh index d7fc7ec..1bd327a 100755 --- a/.claude/hooks/goal_tracker.sh +++ b/.claude/hooks/goal_tracker.sh @@ -43,15 +43,13 @@ if [ -n "$GOAL_LINE" ]; then exit 0 fi -# 2) Informativo + clasificacion inmediata desde el prompt. +# 2) Informativo: estado actual para el modelo. +# La fase la mantienen los hooks automaticamente: PostToolUse marca el estado +# ACTIVO segun las herramientas usadas; el Stop hook resuelve el REPOSO con haiku. if [ -f "$F" ]; then G=$(jq -r '.goal // ""' "$F" 2>/dev/null) P=$(jq -r '.phase // ""' "$F" 2>/dev/null) - # 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." + echo "GOAL-TRACKER: file=$F | goal=\"$G\" phase=\"$P\". La fase la mantienen los hooks automaticamente (PostToolUse = activo, Stop = reposo) β€” 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/settings.json b/.claude/settings.json index ff8f798..9647d39 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -27,6 +27,13 @@ { "type": "command", "command": "~/.claude/hooks/goal_phase_eval.sh" } ] } + ], + "PostToolUse": [ + { + "hooks": [ + { "type": "command", "command": "~/.claude/hooks/goal_phase_active.sh" } + ] + } ] }, "statusLine": { diff --git a/.claude/statusline.sh b/.claude/statusline.sh index fcdb32d..681f1d1 100755 --- a/.claude/statusline.sh +++ b/.claude/statusline.sh @@ -116,10 +116,11 @@ phase_style() { 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' ;; + en_pausa) printf '⏸️|90|en pausa' ;; hecho) printf 'βœ…|32|hecho' ;; + iterando) printf 'πŸ”|94|iterando' ;; *) printf "β€’|90|$1" ;; esac }