#!/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. # # 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. # # 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" [ -x "$PY" ] || exit 0 [ -f "$ASK" ] || exit 0 [ -f "$F" ] || exit 0 GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null) [ -z "$GOAL" ] && exit 0 USER_MSG="" 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 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 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." 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, 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 ;; *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 + 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 | .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