Files
repo_Claude/.claude/hooks/goal_tracker.sh
T
integrador d4640a0660 fix(goal): poblar goal de ejecutores spawneados (statusline/FleetView sin objetivo)
spawn_fleet_agent pre-crea ~/.claude/goals/<sid>.json con solo
{role, parent_orchestrator} antes del primer prompt. goal_tracker.sh usaba
'el archivo existe' como proxy de 'objetivo definitivo', así que para esos
ejecutores nunca lanzaba goal_autogen: el goal quedaba vacío para siempre y el
statusline (LINE0) y FleetView mostraban '(sin objetivo)'.

Fix: 'definitivo' ahora exige .goal NO vacío (no solo que el archivo exista).
Cuando el archivo existe pero sin goal (ejecutor spawneado), se fija un goal
provisional PRESERVANDO role/parent_orchestrator y se lanza autogen, que lo pisa
con el definitivo. No regresiona el caso definitivo ni el primer-prompt sin
archivo (verificado con 3 casos).
2026-06-21 14:59:15 +02:00

145 lines
7.6 KiB
Bash
Executable File

#!/bin/bash
# UserPromptSubmit hook del sistema de objetivo+fase por terminal.
#
# Modelo:
# - El OBJETIVO (target) es el IDENTIFICATIVO de la terminal: se genera una vez
# (del primer prompt, o a mano con "objetivo: ...") y NUNCA cambia solo.
# - El DoD SI se ajusta con tus prompts para reflejar la condicion de terminado.
# - La FASE la mantienen los hooks de fase: PostToolUse (activo) y Stop (reposo).
#
# Comandos META (se ejecutan FUERA DE BANDA: el hook hace su efecto y BLOQUEA el
# prompt con decision=block, asi el agente NO lo recibe ni responde; solo ves una
# confirmacion breve):
# objetivo: <texto> fija/cambia el objetivo a mano (meta:/goal: equivalen).
# objetivo: clear lo borra (tambien -, none, borrar, quitar, reset).
# dod: <texto> fija un DoD a mano.
# dod: clear lo borra.
# pausa marca la fase en en_pausa (Ctrl-C no dispara hooks).
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)
PROMPT_TRIM=$(printf '%s' "$PROMPT" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
# Bloquea el prompt (no llega al agente) y muestra <reason> al usuario.
block() { jq -n --arg r "$1" '{decision:"block", reason:$r}'; exit 0; }
# --- objetivo: <texto> (manual; preserva el DoD si ya existia) ---
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"
block "🎯 Objetivo de esta terminal borrado." ;;
esac
if [ -f "$F" ]; then
PH=$(jq -r '.phase // "planificando"' "$F" 2>/dev/null)
DD=$(jq -r '.dod // ""' "$F" 2>/dev/null)
else
PH="planificando"; DD=""
fi
TMP="${F}.tmp.$$"
if jq -n --arg g "$NEWGOAL" --arg p "$PH" --arg d "$DD" \
'{goal:$g, phase:$p, prompts:[]} | if $d != "" then .dod=$d else . end' > "$TMP" 2>/dev/null; then
mv "$TMP" "$F"
else
rm -f "$TMP"
fi
block "🎯 Objetivo fijado: ${NEWGOAL}"
fi
# Nota: el rename de FleetView se hace ahora con alt+r DENTRO de la TUI (escribe
# el campo .rename del goal directamente). Ya no se captura /rename en este hook,
# asi el built-in /rename de Claude Code queda libre para renombrar la sesion.
# --- dod: <texto> ---
DOD_LINE=$(printf '%s' "$PROMPT" | grep -ioE '^[[:space:]]*dod[[:space:]]*:[[:space:]]*.+' | head -1)
if [ -n "$DOD_LINE" ]; then
NEWDOD=$(printf '%s' "$DOD_LINE" | sed -E 's/^[^:]*:[[:space:]]*//; s/[[:space:]]+$//')
[ -f "$F" ] || block "Fija primero un objetivo (\"objetivo: ...\") antes del DoD."
case "$NEWDOD" in
-|clear|none|borrar|quitar|reset)
TMP="${F}.tmp.$$"
jq 'del(.dod)' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F"
block "🏁 DoD borrado." ;;
esac
TMP="${F}.tmp.$$"
if jq --arg d "$NEWDOD" '.dod=$d' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F"; else rm -f "$TMP"; fi
block "🏁 DoD fijado: ${NEWDOD}"
fi
# --- pausa (marca manual; Ctrl-C no dispara hooks en Claude Code) ---
case "$PROMPT_TRIM" in
pausa|pause|pausar|"en pausa"|/pausa)
[ -f "$F" ] || block "No hay objetivo en esta terminal."
TMP="${F}.tmp.$$"
if jq '.phase="en_pausa" | .history=((.history // [])+["en_pausa"])[-12:]' "$F" > "$TMP" 2>/dev/null; then
mv "$TMP" "$F"
fi
block "⏸️ Fase marcada en pausa." ;;
esac
# --- prompt NORMAL: pasa al agente + estado ---
# Distinguimos dos situaciones por el flag .provisional del goal file:
# - no existe el archivo -> primer prompt: ponemos objetivo PROVISIONAL = tu
# texto + lanzamos autogen (haiku, UNA sola vez)
# que lo definira.
# - existe pero .provisional -> autogen aun no termino (o fallo): conservamos el
# provisional y relanzamos autogen (idempotente,
# self-healing).
# - existe y NO provisional -> objetivo definitivo: solo mostramos estado.
#
# NOTA (2026-06-21): el campo `.dod` movil YA NO se regenera con LLM en cada
# prompt. goal_refine.sh esta desactivado (era una request haiku por turno por
# sesion -> amplificaba el rate-limit compartido de la organizacion). El `.dod`
# movil no lo consume nadie; el criterio que clasifica la flota es `dod_contract`
# + `dod_status` (set_dod_contract.py, sin LLM). El DoD inicial lo fija autogen
# una vez; el usuario lo ajusta a mano con "dod: ...".
PROV="false"
GOAL_NOW=""
if [ -f "$F" ]; then
PROV=$(jq -r '.provisional // false' "$F" 2>/dev/null)
GOAL_NOW=$(jq -r '.goal // ""' "$F" 2>/dev/null)
fi
# "Objetivo definitivo" = archivo con goal NO vacio y no provisional. El check de
# goal no vacio es clave para los ejecutores lanzados por spawn_fleet_agent: su
# goal.json se PRE-CREA con solo {role, parent_orchestrator} (sin goal). Sin este
# guard, el hook tomaria ese archivo como objetivo definitivo y nunca lanzaria
# autogen, dejando el goal vacio para siempre (statusline y FleetView sin objetivo).
if [ -f "$F" ] && [ "$PROV" != "true" ] && [ -n "$GOAL_NOW" ]; then
G=$(jq -r '.goal // ""' "$F" 2>/dev/null)
P=$(jq -r '.phase // ""' "$F" 2>/dev/null)
D=$(jq -r '.dod // ""' "$F" 2>/dev/null)
echo "GOAL-TRACKER: file=$F | goal=\"$G\" dod=\"$D\" phase=\"$P\". El objetivo es fijo (identificativo de la terminal, NO lo cambies). El DoD inicial lo fija el autogen una vez (sin LLM por prompt); el usuario lo ajusta con \"dod: ...\" — NO lo regeneres tu. La fase la mantienen los hooks (PostToolUse=activo, Stop=reposo) — NO la escribas. Comandos meta del usuario (no los uses tu): objetivo:/dod:/pausa."
else
# Sin objetivo definitivo todavia. Mostramos de inmediato un objetivo PROVISIONAL
# igual a tu propio texto (truncado), para que el statusline no quede vacio
# mientras haiku genera el real en background. autogen pisara este provisional
# con el definitivo al terminar (su guard respeta .provisional).
if [ "${#PROMPT_TRIM}" -ge 12 ]; then
TMP="${F}.tmp.$$"
PROV_GOAL=$(printf '%s' "$PROMPT_TRIM" | head -c 70)
if [ -n "$GOAL_NOW" ]; then
# Ya habia goal provisional: conserva su goal, solo acumula el prompt.
jq --arg p "$PROMPT_TRIM" '.prompts = ((.prompts // []) + [$p])[-12:]' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F" || rm -f "$TMP"
elif [ -f "$F" ]; then
# Archivo PRE-CREADO por spawn_fleet_agent ({role, parent_orchestrator})
# sin goal: fija el provisional PRESERVANDO los campos existentes (role,
# parent_orchestrator) y deja que autogen lo pise con el definitivo.
jq --arg g "$PROV_GOAL" --arg p "$PROMPT_TRIM" \
'. + {goal:$g, phase:"planificando", history:["planificando"], prompts:[$p], provisional:true}' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F" || rm -f "$TMP"
else
jq -n --arg g "$PROV_GOAL" --arg p "$PROMPT_TRIM" \
'{goal:$g, phase:"planificando", history:["planificando"], prompts:[$p], provisional:true}' > "$TMP" 2>/dev/null && mv "$TMP" "$F" || rm -f "$TMP"
fi
nohup bash "$HOME/.claude/hooks/goal_autogen.sh" "$SID" "$F" "$PROMPT" >/dev/null 2>&1 &
fi
echo "GOAL-TRACKER: file=$F (objetivo PROVISIONAL = tu texto; generando el objetivo+DoD real con haiku en background). El usuario tambien puede fijarlo con \"objetivo: ...\" / \"dod: ...\"."
fi
exit 0