feat(statusline): objetivo+DoD coherentes con los prompts + alerta de mezcla de tareas

El objetivo y el DoD dejan de quedarse congelados en el primer prompt:

- goal_tracker acumula cada prompt sustantivo del usuario en .prompts y lanza
  goal_refine.sh (background, haiku) para mantener objetivo y DoD coherentes con
  TODO lo pedido (action refine), o dejarlos igual (action same).
- goal_refine marca alert=true (action switch) cuando el ultimo prompt introduce
  una tarea completamente distinta del objetivo: senal de que la terminal mezcla
  tareas (principio: una terminal = una tarea). No cambia el objetivo, solo avisa.
- statusline muestra ⚠️ en rojo antes del objetivo cuando alert=true.
- Comando 'recalcular' (recalcula/replantea): fuerza regenerar objetivo+DoD desde
  los prompts y limpia la alerta (para cuando el cambio de tarea es intencional).
- goal_autogen inicializa .prompts con el primer prompt.

Coste: 1 haiku/prompt sustantivo en background (ademas del haiku de reposo del
Stop), solicitado para mantener la coherencia. No bloquea el turno.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 16:01:15 +02:00
parent 9ac52501b5
commit 1840402453
4 changed files with 108 additions and 4 deletions
+2 -2
View File
@@ -37,8 +37,8 @@ DOD=$(printf '%s' "$JSON" | jq -r '.dod // ""' 2>/dev/null)
[ -f "$F" ] && exit 0
TMP="${F}.tmp.$$"
if jq -n --arg g "$GOAL" --arg d "$DOD" \
'{goal:$g, phase:"planificando", history:["planificando"]} | if $d != "" then .dod=$d else . end' > "$TMP" 2>/dev/null; then
if jq -n --arg g "$GOAL" --arg d "$DOD" --arg p "$P" \
'{goal:$g, phase:"planificando", history:["planificando"], prompts:[$p]} | if $d != "" then .dod=$d else . end' > "$TMP" 2>/dev/null; then
mv "$TMP" "$F"
else
rm -f "$TMP"
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# Mantiene objetivo+DoD coherentes con los prompts acumulados del usuario y avisa
# si la terminal empieza a mezclar tareas. Lo lanza goal_tracker.sh en background
# (no bloquea). Usa ask_llm (haiku, API directa; nunca `claude -p`).
#
# Args: <session_id> <goal_json_file> [force]
# force: regenera siempre objetivo+DoD desde los prompts y limpia la alerta
# (lo usa el comando "recalcular").
#
# Decision normal (sin force): el modelo devuelve
# {"action":"same|refine|switch","goal":"...","dod":"..."}
# - same : el objetivo ya describe bien el conjunto -> no se toca nada.
# - refine : ajusta objetivo/DoD para englobar mejor TODO lo pedido (estable).
# - switch : el ultimo prompt es una tarea COMPLETAMENTE distinta -> alert=true
# (una terminal deberia ser una sola tarea). No cambia el objetivo.
SID="$1"
F="$2"
FORCE="$3"
[ -f "$F" ] || exit 0
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
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
[ -z "$GOAL" ] && exit 0
DOD=$(jq -r '.dod // ""' "$F" 2>/dev/null)
PROMPTS=$(jq -r '(.prompts // []) | to_entries | map("\(.key+1). \(.value)") | join("\n")' "$F" 2>/dev/null)
[ -z "$PROMPTS" ] && exit 0
if [ "$FORCE" = "force" ]; then
SYS="Regenera el OBJETIVO y el DoD de una terminal de trabajo a partir de la lista de prompts del usuario, sintetizando lo que se esta haciendo realmente. Responde SOLO JSON en una linea, sin markdown: {\"action\":\"refine\",\"goal\":\"...\",\"dod\":\"...\"}. goal: maximo 9 palabras en espanol. dod: condicion concreta de terminado, maximo 9 palabras en espanol."
else
SYS="Mantienes coherentes el OBJETIVO y el DoD de una terminal de trabajo segun los prompts del usuario. Principio: una terminal = una sola tarea. Analiza si el conjunto de prompts sigue describiendo UNA tarea coherente. Responde SOLO JSON en una linea, sin markdown: {\"action\":\"same|refine|switch\",\"goal\":\"...\",\"dod\":\"...\"}. action=same: el objetivo actual ya describe bien el conjunto (deja goal y dod vacios, no se cambia nada). action=refine: conviene ajustar objetivo/DoD para englobar mejor TODO lo pedido, manteniendolo estable y breve (goal y dod max 9 palabras cada uno, en espanol). action=switch: el ULTIMO prompt introduce una tarea COMPLETAMENTE distinta y no relacionada con el objetivo actual (la terminal esta mezclando tareas); deja goal y dod vacios. Se conservador con 'switch': uselo solo ante un cambio claro de tema, no por matices o sub-tareas del mismo objetivo."
fi
PROMPT="OBJETIVO ACTUAL: ${GOAL}
DoD ACTUAL: ${DOD}
PROMPTS DEL USUARIO (orden cronologico):
${PROMPTS}
Responde el JSON:"
RAW=$("$PY" "$ASK" --system "$SYS" "$PROMPT" 2>/dev/null)
[ -z "$RAW" ] && exit 0
JSON=$(printf '%s' "$RAW" | tr '\n' ' ' | grep -o '{[^{}]*}' | head -1)
[ -z "$JSON" ] && exit 0
ACTION=$(printf '%s' "$JSON" | jq -r '.action // ""' 2>/dev/null)
NG=$(printf '%s' "$JSON" | jq -r '.goal // ""' 2>/dev/null)
ND=$(printf '%s' "$JSON" | jq -r '.dod // ""' 2>/dev/null)
case "$ACTION" in
refine)
TMP="${F}.tmp.$$"
if jq --arg g "$NG" --arg d "$ND" '
(if $g != "" then .goal=$g else . end)
| (if $d != "" then .dod=$d else . end)
| .alert=false
' "$F" > "$TMP" 2>/dev/null; then
mv "$TMP" "$F"
else
rm -f "$TMP"
fi
;;
switch)
TMP="${F}.tmp.$$"
if jq '.alert=true' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F"; else rm -f "$TMP"; fi
;;
*)
: # same / desconocido -> no tocar
;;
esac
exit 0
+22 -1
View File
@@ -66,6 +66,15 @@ if [ -n "$DOD_LINE" ]; then
exit 0
fi
# --- recalcular (regenera objetivo+DoD desde los prompts; limpia la alerta) ---
case "$PROMPT_TRIM" in
recalcular|recalcula|replantea|replantear|/recalcular)
[ -f "$F" ] || { echo "GOAL-TRACKER: no hay objetivo que recalcular."; exit 0; }
nohup bash "$HOME/.claude/hooks/goal_refine.sh" "$SID" "$F" force >/dev/null 2>&1 &
echo "GOAL-TRACKER: recalculando objetivo+DoD desde tus prompts (background)."
exit 0 ;;
esac
# --- pausa (marca manual; Ctrl-C no dispara hooks en Claude Code) ---
case "$PROMPT_TRIM" in
pausa|pause|pausar|"en pausa"|/pausa)
@@ -83,7 +92,19 @@ if [ -f "$F" ]; 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\". La fase la mantienen los hooks (PostToolUse=activo, Stop=reposo) — NO la escribas. El usuario fija objetivo con \"objetivo: ...\" y un DoD corto con \"dod: ...\"; si redefine la tarea en lenguaje natural, actualiza \"goal\" en ese JSON."
# Acumular el prompt y refinar objetivo+DoD para mantenerlos coherentes con
# todo lo pedido (background). Si el prompt es una tarea totalmente distinta,
# goal_refine marca alert=true (la terminal mezcla tareas).
if [ "${#PROMPT_TRIM}" -ge 12 ]; then
TMP="${F}.tmp.$$"
if jq --arg p "$PROMPT_TRIM" '.prompts = ((.prompts // []) + [$p])[-12:]' "$F" > "$TMP" 2>/dev/null; then
mv "$TMP" "$F"
else
rm -f "$TMP"
fi
nohup bash "$HOME/.claude/hooks/goal_refine.sh" "$SID" "$F" >/dev/null 2>&1 &
fi
echo "GOAL-TRACKER: file=$F | goal=\"$G\" dod=\"$D\" phase=\"$P\". La fase la mantienen los hooks (PostToolUse=activo, Stop=reposo) — NO la escribas. El usuario fija objetivo con \"objetivo: ...\" y un DoD corto con \"dod: ...\"; si redefine la tarea en lenguaje natural, actualiza \"goal\" en ese JSON. El objetivo/DoD se autorefinan con tus prompts; si ves ⚠️ es que la terminal esta mezclando tareas."
else
# Sin objetivo: autogenerar objetivo + DoD desde el primer prompt sustantivo,
# en background (no bloquea). Se omite para prompts triviales (saludos, ok...).
+7 -1
View File
@@ -304,9 +304,15 @@ 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)
DOD=$(jq -r '.dod // ""' "$GOAL_FILE" 2>/dev/null)
ALERT=$(jq -r '.alert // false' "$GOAL_FILE" 2>/dev/null)
if [ -n "$GOAL" ]; then
GC=$(goal_color "$SESSION_ID")
LEFT="${GC}🎯 ${GOAL}${RESET}"
# ⚠️ si la terminal esta mezclando tareas (objetivo cambio por completo).
if [ "$ALERT" = "true" ]; then
LEFT="\033[1;31m⚠️\033[0m ${GC}🎯 ${GOAL}${RESET}"
else
LEFT="${GC}🎯 ${GOAL}${RESET}"
fi
LINE0="${LEFT}"