Compare commits

...

15 Commits

Author SHA1 Message Date
egutierrez 393a77b597 chore: auto-commit (1 archivos)
- .claude/CLAUDE.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:32 +02:00
egutierrez 50290a71e7 feat(statusline): objetivo fijo (identificativo), solo el DoD se refina
Simplifica el modelo segun feedback:
- El OBJETIVO (target) es el identificativo de la terminal: se genera una vez y
  NUNCA cambia automaticamente. goal_refine deja de tocarlo.
- goal_refine ahora ajusta SOLO el DoD para mantenerlo coherente con los prompts.
- Se elimina la deteccion de cambio de tarea y el icono de alerta ⚠️ (campo alert
  ya no se escribe ni se lee; queda inocuo en JSONs antiguos).
- Se elimina el comando 'recalcular' y goal_refine.sh modo force.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:23:20 +02:00
egutierrez a3ecb6a4cf feat(statusline): comandos meta fuera de banda (no molestan al agente)
Los comandos del usuario objetivo:/dod:/recalcular/pausa ahora bloquean el prompt
en UserPromptSubmit con {"decision":"block","reason":...}: el hook ejecuta su
efecto, el usuario ve una confirmacion breve, y el prompt NO llega al modelo — el
agente no genera respuesta y sigue idle con su tarea. Antes estos comandos se
procesaban como un turno normal e interrumpian al agente. Los prompts normales se
siguen pasando al modelo (texto plano como contexto) y acumulan/refían el objetivo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:11:30 +02:00
egutierrez 1840402453 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>
2026-06-06 16:01:15 +02:00
egutierrez 9ac52501b5 style(statusline): DoD en linea propia debajo del objetivo
El DoD se mostraba en la misma linea que el objetivo y la expandia demasiado a lo
ancho. Ahora va en una linea separada debajo, atenuado y con sangria. Si no hay
DoD, no se imprime la linea extra.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:51:42 +02:00
egutierrez 1a15108b56 feat(statusline): 'hecho' se decide comparando el resultado contra el DoD
El Stop worker recibe ahora el DoD de la tarea y lo usa como criterio para
distinguir hecho de pendiente_revision/en_pausa: marca 'hecho' unicamente si el
resultado descrito cumple el DoD punto por punto y esta verificado. Si el DoD no
se cumple del todo, cae en pendiente_revision (resultado para revisar) o en_pausa
(avance parcial). Si no hay DoD definido, mantiene el criterio anterior sobre el
objetivo. Hace el estado 'hecho' mucho mas preciso.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:49:28 +02:00
egutierrez 54f47570d1 feat(statusline): autogenerar objetivo + DoD desde el primer prompt
Cuando una terminal no tiene objetivo y el usuario envia su primer prompt
sustantivo (>=12 chars), goal_tracker lanza goal_autogen.sh en background (no
bloquea el turno). El script infiere con ask_llm (haiku) un objetivo corto y un
DoD corto a partir del prompt y crea el goal JSON con phase=planificando. Los
prompts triviales (saludos, ok) no generan nada (el modelo responde {}). El
statusline lo muestra en el siguiente refresco. El usuario puede sobrescribir a
mano con objetivo:/dod: o borrar con objetivo: clear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:46:52 +02:00
egutierrez adfb45015e feat(statusline): triggers para planificando y puliendo
Los dos estados activos que no tenian disparador ahora se asignan de forma
determinista en el PostToolUse:
- planificando: al usar TodoWrite / ExitPlanMode / EnterPlanMode.
- puliendo: al editar (Edit/Write/MultiEdit/NotebookEdit) cuando la fase actual
  ya era testeando o puliendo, es decir retoques finales sobre algo ya probado.
  Una edicion normal (sin testeo previo) sigue siendo haciendo; volver a testear
  saca de puliendo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:31:54 +02:00
egutierrez 3ae472d1f3 feat(statusline): estado 'preguntando', DoD junto al objetivo y comando pausa
- Nuevo estado de reposo 'preguntando' ( esperando respuesta), distinto de
  pendiente_revision: lo usa el Stop worker cuando la respuesta termina con
  preguntas concretas al usuario en vez de dejar un resultado para revisar.
- DoD corto opcional junto al objetivo: se fija con "dod: <texto>" ("dod: clear"
  lo borra) y se muestra atenuado con 🏁 tras el objetivo. Re-fijar el objetivo
  preserva el DoD existente.
- Comando "pausa" (prompt) marca la fase en en_pausa. Es la alternativa manual a
  Ctrl-C: Claude Code no dispara ningun hook al interrumpir un turno (el Stop
  hook solo corre en finalizacion normal; feature pedido, sin implementar), asi
  que no es posible detectar la interrupcion automaticamente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:26:51 +02:00
egutierrez fa2b2e16bc feat(statusline): refresco idle via refreshInterval + cache de git
El statusline solo se re-ejecutaba al cambiar los mensajes, asi que el estado de
reposo que el Stop hook escribe ~2s despues (en background) no se reflejaba hasta
que el usuario interactuaba. Se anade refreshInterval=2 para que el harness re-
ejecute el statusline cada 2s tambien estando idle, mostrando el valor final sin
necesidad de escribir y sin bloquear el turno.

Para que el refresco continuo no sea caro en repos grandes, el bloque git se
cachea por directorio con TTL de 6s (el estado git no cambia estando parado); los
ticks idle reusan el cache (~0.1s vs ~0.33s recomputando). El goal file (la fase)
se lee siempre fresco.

Se revierte el intento previo de Stop sincrono (bloqueaba ~2s el control).

Nota: Claude Code no soporta acotar el refresco a una ventana (p.ej. 10s y
parar); refreshInterval es continuo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:19:21 +02:00
egutierrez 4d1fff53e9 fix(statusline): al parar, salir del estado activo de inmediato
El Stop hook ahora pone en_pausa de forma sincrona si la fase estaba en un
estado activo (investigando/haciendo/testeando/puliendo), antes de lanzar el
worker haiku en background. Evita que el statusline se quede mostrando el ultimo
estado activo (p.ej. 'investigando' por un 'git log' final) durante los ~2s que
tarda la clasificacion de reposo. El provisional no toca el historial; el worker
escribe el reposo final (hecho/pendiente_revision/bloqueado/en_pausa) + history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:07:57 +02:00
egutierrez eb42966295 style(statusline): historial con emojis pegados, sin separador entre ellos
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:04:43 +02:00
egutierrez 8c9919f1f8 feat(statusline): estados activo (deterministas) + reposo (haiku)
Separa el ciclo de trabajo en dos grupos con la fuente adecuada para cada uno:

- ACTIVO (mientras se trabaja): lo marca el hook PostToolUse de forma
  determinista, sin LLM, segun la herramienta usada — Read/Grep/Glob ->
  investigando; Edit/Write -> haciendo; Bash con tests -> testeando; Bash de
  lectura (ls/cat/git status...) -> investigando; mcp fn_search/show/... ->
  investigando. Refleja en tiempo real lo que hace el asistente.
- REPOSO (al parar y ceder el control): lo resuelve el Stop hook con ask_llm
  (haiku) -> hecho / pendiente_revision / bloqueado / en_pausa. Al parar nunca
  queda en un estado activo.

Cambios:
- goal_phase_active.sh: nuevo hook PostToolUse (mapa herramienta -> fase activa).
- goal_phase_worker.sh: ahora solo produce estados de reposo; se elimina el modo
  prompt. Mantiene el gate (resuelve reposo solo si hubo trabajo o se venia de
  activo) y el historial.
- goal_tracker.sh: deja de lanzar clasificacion LLM en el prompt (redundante);
  vuelve a fijar objetivo desde el prompt + informar estado.
- statusline.sh: nuevo estado en_pausa (en pausa); set de fases reordenado.
- settings.json: registra el hook PostToolUse.

Resultado: 1 sola llamada haiku por turno (Stop); el estado activo es gratis y
refleja las acciones reales en vez de la intencion del prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:04:07 +02:00
egutierrez f881b7703b feat(statusline): historial de estados + clasificacion al escribir el usuario
- statusline.sh: muestra los ultimos 7 estados previos como emojis atenuados
  (DIM) separados por │, entre el objetivo y la fase actual. El historial se
  guarda en el goal JSON (campo .history), colapsando estados consecutivos
  repetidos, hasta 12 entradas.
- goal_phase_worker.sh: dos modos. 'stop' (tras la respuesta del asistente, con
  filtro de trabajo real) y 'prompt' (tras el prompt del usuario, clasifica la
  intencion para feedback inmediato). Nuevo veredicto 'sin_cambio' para
  preguntas/charla que no implican cambio de actividad; ante la duda, no toca.
  Ambos modos mantienen el historial.
- goal_tracker.sh: en cada prompt con objetivo activo lanza el worker en modo
  prompt (background) ademas del Stop hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:07:39 +02:00
egutierrez 5efcedf9ba feat(statusline): seguimiento de objetivo + fase por terminal
Cada terminal muestra su objetivo (color estable por session_id) y la fase de
trabajo actual, para distinguir sesiones y saber cuando algo esta hecho.

- statusline.sh: linea 0 con objetivo (izq, color por sesion) + fase (der) con
  el separador estandar; 9 fases (investigando, planificando, haciendo,
  testeando, puliendo, iterando, pendiente_revision, bloqueado, hecho) con icono,
  color y etiqueta. Purga de goal files de sesiones muertas (>7 dias).
- hooks/goal_tracker.sh (UserPromptSubmit): fija el objetivo leyendo el prompt
  del usuario ("objetivo: ...", "objetivo: clear" lo borra); si no, informa el
  estado actual al modelo.
- hooks/goal_phase_eval.sh (Stop): al terminar el turno lanza el worker en
  background, sin bloquear.
- hooks/goal_phase_worker.sh: clasifica la fase con ask_llm (haiku, API directa,
  nunca claude -p) usando la peticion del usuario + la ultima respuesta del
  asistente. Solo reevalua si el turno tuvo trabajo real (tool_use); en charla
  pura no toca la fase ni gasta llamada.
- settings.json: registra los hooks UserPromptSubmit y Stop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:01:05 +02:00
9 changed files with 564 additions and 3 deletions
+14
View File
@@ -16,3 +16,17 @@ Aplican a todas las sesiones de Claude Code, en cualquier proyecto.
> (`skills/caveman/SKILL.md` + `src/hooks/caveman-mode-tracker.js`). Las copias del plugin en
> `~/.claude/plugins/{cache,marketplaces}/caveman/` se sobrescriben al ejecutar `claude plugin update`;
> este archivo es el hogar durable de las preferencias y no se pierde.
## Navegación web — usa SIEMPRE el MCP del navegador
Para CUALQUIER tarea de navegación, lectura o automatización web (abrir páginas, login, scraping, rellenar formularios, reconocimiento de endpoints) usa SIEMPRE el MCP `browser_mcp`. NUNCA CDP crudo inline (heredoc WebSocket, `Runtime.evaluate` a mano), NUNCA Playwright/Selenium, NUNCA lanzar `chromium`/`google-chrome` a pelo para esto.
- El MCP opera sobre un Chrome aislado (puerto 9333) separado del navegador diario.
- **Navegar:** `tab_new` / `tab_navigate` (+ `tab_select` para elegir pestaña, `nav_back` / `nav_forward`).
- **Esperar:** `page_wait_load` (DOM listo) / `page_wait_idle` (red en reposo; ya ignora WebSocket/EventSource, no cuelga en SPAs).
- **Leer (por defecto, SIN capturas):** `page_perceive` (accessibility tree → outline indentado con marcadores `#ref` accionables) y `page_get_text` (texto visible, truncable). NO uses `page_screenshot` para leer: hoy guarda la imagen a archivo y el agente no la ve; las capturas son solo para depuración visual puntual, no para percepción.
- **Actuar:** `dom_click_ref` / `dom_type_ref` / `dom_hover_ref` (por el `#ref` del outline de `page_perceive`), `dom_find_ref_by_text`, `press_key`, `scroll`. El bucle natural es: `page_perceive` → decidir sobre los `#ref``dom_*_ref``page_perceive` de nuevo (auto-observa el efecto).
Si el MCP no expone una capacidad concreta, usa `fn run cdp_<x>` antes de escribir CDP crudo: hay 46 funciones del dominio `browser` indexadas en el registry (incluidas `cdp_navigate`, `cdp_get_text`, `cdp_perceive_outline`, `cdp_click_ref`). El registry SÍ tiene navegación CDP genérica — si no la encuentras por búsqueda, mejora la búsqueda, no reinventes con un heredoc.
Requisito de disponibilidad: el `browser_mcp` debe estar registrado en el `.mcp.json` accesible a la sesión (hoy en `projects/web_scraping/.mcp.json`). Si trabajas en otra carpeta y las tools `browser_*`/`page_*`/`dom_*` no aparecen, registra el MCP en el `.mcp.json` de esa sesión.
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Autogeneracion de objetivo + DoD a partir del primer prompt sustantivo de una
# terminal que aun no tiene objetivo. Lo lanza goal_tracker.sh en background (no
# bloquea el turno). Usa ask_llm (haiku, API directa; nunca `claude -p`).
#
# Args: <session_id> <goal_json_file> <prompt_text>
SID="$1"
F="$2"
PROMPT="$3"
# Si ya existe objetivo (lo creo otro proceso o el usuario), no pisar.
[ -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
P=$(printf '%s' "$PROMPT" | tail -c 2000)
[ -z "$P" ] && exit 0
SYS="Dado el PRIMER mensaje de un usuario a un asistente de codigo en una terminal, infiere un OBJETIVO breve de la tarea (maximo 8 palabras, en espanol, sin comillas) y un DoD breve (definition of done: condicion concreta de 'terminado', maximo 8 palabras, en espanol). Responde SOLO un objeto JSON en una sola linea, sin markdown ni texto extra: {\"goal\":\"...\",\"dod\":\"...\"}. Si el mensaje es un saludo, charla trivial o no describe ninguna tarea, responde exactamente {}."
RAW=$("$PY" "$ASK" --system "$SYS" "$P" 2>/dev/null)
[ -z "$RAW" ] && exit 0
# Extraer el primer objeto JSON de la salida (tolerante a texto/markdown extra).
JSON=$(printf '%s' "$RAW" | tr '\n' ' ' | grep -o '{[^{}]*}' | head -1)
[ -z "$JSON" ] && exit 0
GOAL=$(printf '%s' "$JSON" | jq -r '.goal // ""' 2>/dev/null)
DOD=$(printf '%s' "$JSON" | jq -r '.dod // ""' 2>/dev/null)
[ -z "$GOAL" ] && exit 0
# Carrera: si entre tanto se creo el archivo, no pisar.
[ -f "$F" ] && exit 0
TMP="${F}.tmp.$$"
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"
fi
exit 0
+75
View File
@@ -0,0 +1,75 @@
#!/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
CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null)
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)
# Editar tras haber testeado = retoques finales -> puliendo. Si no, es
# implementacion normal -> haciendo.
case "$CUR" in
testeando|puliendo) PHASE=puliendo ;;
*) PHASE=haciendo ;;
esac
;;
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 ;;
TodoWrite|ExitPlanMode|EnterPlanMode|Plan)
PHASE=planificando ;;
*)
# Herramientas que no representan un cambio de actividad (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
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Stop hook: tras cada respuesta del asistente, dispara (en background) la
# clasificacion de la fase de la tarea. Lee la ultima respuesta del transcript,
# la clasifica con ask_llm (haiku) y escribe el resultado en el goal JSON de la
# sesion. El statusline lo pinta en el siguiente render.
#
# No bloquea el cierre del turno: el trabajo pesado va al worker en background.
INPUT=$(cat)
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
[ -z "$SID" ] && exit 0
F="$HOME/.claude/goals/${SID}.json"
# Solo si esta terminal tiene un objetivo fijado.
[ -f "$F" ] || exit 0
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
[ -z "$GOAL" ] && exit 0
# Salir del estado ACTIVO de inmediato (sincrono, instantaneo): al parar no debe
# quedarse mostrando investigando/haciendo/testeando mientras el worker (haiku,
# background) afina el reposo a hecho/pendiente_revision/bloqueado/en_pausa.
CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null)
case "$CUR" in
investigando|planificando|haciendo|testeando|puliendo)
TMP="${F}.prov.$$"
if jq '.phase="en_pausa"' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F"; else rm -f "$TMP"; fi
;;
esac
[ -z "$TRANSCRIPT" ] && exit 0
[ -f "$TRANSCRIPT" ] || exit 0
# Afinar el reposo en background; el hook retorna de inmediato (no bloquea el
# turno). El statusline reflejara el valor final en el siguiente refresco.
nohup bash "$HOME/.claude/hooks/goal_phase_worker.sh" "$SID" "$TRANSCRIPT" "$F" >/dev/null 2>&1 &
exit 0
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
# 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.
#
# 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: <session_id> <transcript_path> <goal_json>
SID="$1"
TRANSCRIPT="$2"
F="$3"
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
[ -f "$TRANSCRIPT" ] || exit 0
GOAL=$(jq -r '.goal // ""' "$F" 2>/dev/null)
[ -z "$GOAL" ] && exit 0
CUR=$(jq -r '.phase // ""' "$F" 2>/dev/null)
DOD=$(jq -r '.dod // ""' "$F" 2>/dev/null)
[ -z "$DOD" ] && DOD="(no definido)"
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=""
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")
# 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="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 preguntando bloqueado en_pausa sin_cambio. hecho=el objetivo esta completo y verificado; pendiente_revision=el asistente termino un trabajo y espera que el humano lo revise o apruebe (no hace una pregunta directa); preguntando=el asistente termina formulando una o varias PREGUNTAS concretas al usuario y necesita su respuesta o decision para continuar; bloqueado=no puede avanzar por un error o por falta de informacion/acceso; en_pausa=hizo un avance y espera la siguiente indicacion, sin estar terminado ni preguntar ni bloqueado; sin_cambio=el turno no altera el estado de reposo actual (charla irrelevante). Distingue: si la respuesta acaba con preguntas al usuario es 'preguntando'; si deja un resultado para que lo mire es 'pendiente_revision'. REGLA DEL DoD: se te da el DoD (definition of done) que define cuando la tarea esta TERMINADA. Marca 'hecho' UNICAMENTE si el resultado descrito por el asistente CUMPLE ese DoD de forma clara y verificada; compara punto por punto el resultado contra el DoD. Si el DoD no se cumple del todo, o no esta verificado, NO uses 'hecho': usa 'pendiente_revision' (dejas un resultado para que el humano lo revise) o 'en_pausa' (avance parcial). Si el DoD es '(no definido)', usa tu criterio: 'hecho' solo si el objetivo esta claramente completo y verificado."
PROMPT="OBJETIVO DE LA TAREA: ${GOAL}
DEFINITION OF DONE (DoD) — la tarea esta TERMINADA solo si esto se cumple:
${DOD}
ULTIMA PETICION DEL USUARIO:
${USER_MSG}
ULTIMA RESPUESTA DEL ASISTENTE:
${LAST}
Compara el resultado contra el DoD y 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
case "$RAW" in
*sin_cambio*|*sincambio*|*ninguna*|*charla*) exit 0 ;;
*pregunt*|*consulta*|*respuesta*) PHASE=preguntando ;;
*pendiente*revis*|*revis*|*aprob*) PHASE=pendiente_revision ;;
*bloque*) PHASE=bloqueado ;;
*hecho*|*complet*|*termin*|*done*) PHASE=hecho ;;
*pausa*|*pause*|*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 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
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Ajusta SOLO el DoD para mantenerlo coherente con los prompts del usuario hacia
# el objetivo. El OBJETIVO es fijo (identificativo de la terminal) y NUNCA se
# toca aqui. Lo lanza goal_tracker.sh en background (no bloquea). Usa ask_llm
# (haiku, API directa; nunca `claude -p`, ver regla llm_invocation.md).
#
# Args: <session_id> <goal_json_file>
SID="$1"
F="$2"
[ -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
SYS="Dado un OBJETIVO fijo de una terminal de trabajo y los prompts del usuario, define o ajusta el DoD (definition of done): la condicion concreta de 'terminado' para ese objetivo, coherente con lo que el usuario va pidiendo. El OBJETIVO no se toca, solo el DoD. Responde SOLO JSON en una linea, sin markdown: {\"dod\":\"...\"}. dod en espanol, breve, maximo 9 palabras. Si el DoD actual ya es adecuado, devuelvelo igual."
PROMPT="OBJETIVO (fijo, no lo cambies): ${GOAL}
DoD ACTUAL: ${DOD}
PROMPTS DEL USUARIO (orden cronologico):
${PROMPTS}
Responde el JSON con el DoD:"
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
ND=$(printf '%s' "$JSON" | jq -r '.dod // ""' 2>/dev/null)
[ -z "$ND" ] && exit 0
TMP="${F}.tmp.$$"
if jq --arg d "$ND" '.dod=$d' "$F" > "$TMP" 2>/dev/null; then
mv "$TMP" "$F"
else
rm -f "$TMP"
fi
exit 0
+106
View File
@@ -0,0 +1,106 @@
#!/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
# --- 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 + acumula para refinar el DoD + estado ---
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)
# Acumular el prompt y ajustar SOLO el DoD (background). El objetivo no cambia.
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\". El objetivo es fijo (identificativo de la terminal, NO lo cambies). El DoD se ajusta solo con los prompts; 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: autogenerar objetivo + DoD desde el primer prompt sustantivo,
# en background (no bloquea). Se omite para prompts triviales (saludos, ok...).
if [ "${#PROMPT_TRIM}" -ge 12 ]; then
nohup bash "$HOME/.claude/hooks/goal_autogen.sh" "$SID" "$F" "$PROMPT" >/dev/null 2>&1 &
fi
echo "GOAL-TRACKER: file=$F (sin objetivo aun). Autogenerando objetivo+DoD desde tu prompt en background (haiku). El usuario tambien puede fijarlo con \"objetivo: ...\" / \"dod: ...\"."
fi
exit 0
+25 -1
View File
@@ -13,10 +13,34 @@
"Write(.git/**)"
]
},
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/goal_tracker.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/goal_phase_eval.sh" }
]
}
],
"PostToolUse": [
{
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/goal_phase_active.sh" }
]
}
]
},
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 1
"padding": 1,
"refreshInterval": 2
},
"enabledPlugins": {
"gopls-lsp@claude-plugins-official": true,
+90 -2
View File
@@ -23,6 +23,11 @@ MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "Unknown"')
CONTEXT_PCT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0' | xargs printf "%.0f")
CONTEXT_TOTAL=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 200000')
CURRENT_DIR=$(echo "$INPUT" | jq -r '.workspace.current_dir // "~"' | sed "s|$HOME|~|")
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
# Purga: borra goal files de sesiones muertas (no tocados en >7 dias). El worker
# refresca el mtime en cada respuesta, asi que las sesiones vivas nunca caen.
find "$HOME/.claude/goals" -maxdepth 1 -name '*.json' -mtime +7 -delete 2>/dev/null
# Tokens de entrada y salida (current_usage puede ser null antes del primer API call)
INPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.input_tokens // 0')
@@ -58,10 +63,18 @@ RESET_7D=""
[ "$RESET_5H_EPOCH" -gt 0 ] && RESET_5H=$(date -d "@$RESET_5H_EPOCH" +"%H:%M" 2>/dev/null)
[ "$RESET_7D_EPOCH" -gt 0 ] && RESET_7D=$(date -d "@$RESET_7D_EPOCH" +"%a %H:%M" 2>/dev/null)
# Git info (si estamos en un repo)
# Git info (si estamos en un repo). Con cache de TTL corto: como el statusline
# se re-ejecuta cada pocos segundos (refreshInterval), recomputar git en cada
# tick es caro en repos grandes y el estado git no cambia estando idle. Se cachea
# por directorio y se recomputa solo si el cache tiene mas de 6s.
GIT_BRANCH=""
GIT_STATUS=""
if git rev-parse --git-dir > /dev/null 2>&1; then
GIT_CACHE="/tmp/fn_sl_git_$(printf '%s' "$CURRENT_DIR" | cksum | cut -d' ' -f1).cache"
GIT_CACHE_AGE=999
[ -f "$GIT_CACHE" ] && GIT_CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$GIT_CACHE" 2>/dev/null || echo 0) ))
if [ "$GIT_CACHE_AGE" -lt 6 ]; then
. "$GIT_CACHE"
elif git rev-parse --git-dir > /dev/null 2>&1; then
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
# Obtener archivos staged, modified, untracked
@@ -89,8 +102,41 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
# Trim trailing space
GIT_STATUS=$(echo "$GIT_STATUS" | sed 's/ $//')
# Guardar en cache (quoting seguro para re-source).
printf 'GIT_BRANCH=%q\nGIT_STATUS=%q\n' "$GIT_BRANCH" "$GIT_STATUS" > "$GIT_CACHE" 2>/dev/null
fi
# Color estable por sesión (hash del session_id → paleta ANSI 256 legible).
# Cada terminal mantiene su color toda su vida; distinto entre terminales.
goal_color() {
local sid="$1"
local palette=(39 45 51 75 81 114 120 156 183 210 215 222 213 159 228)
local h
h=$(printf '%s' "$sid" | cksum | cut -d' ' -f1)
local idx=$(( h % ${#palette[@]} ))
printf '\033[1;38;5;%dm' "${palette[$idx]}"
}
# Fase de trabajo → icono | color ANSI | etiqueta visible.
# El slug (clave) lo escribe el agente del Stop hook; aqui se mapea a su estilo.
phase_style() {
case "$1" in
investigando) printf '🔎|36|investigando' ;;
planificando) printf '📋|34|planificando' ;;
haciendo) printf '🔨|33|haciendo' ;;
testeando) printf '🧪|35|testeando' ;;
puliendo) printf '✨|95|puliendo detalles' ;;
pendiente_revision) printf '👀|93|pendiente de revisión' ;;
preguntando) printf '❓|96|esperando respuesta' ;;
bloqueado) printf '⛔|31|bloqueado' ;;
en_pausa) printf '⏸️|90|en pausa' ;;
hecho) printf '✅|32|hecho' ;;
iterando) printf '🔁|94|iterando' ;;
*) printf "•|90|$1" ;;
esac
}
# Función para crear barra de progreso
progress_bar() {
local pct=$1
@@ -251,6 +297,48 @@ fi
# 6. Directorio actual
LINE2="${LINE2} ${GRAY}${RESET} ${BLUE}${CURRENT_DIR}${RESET}"
# ===== LÍNEA 0: Objetivo (izq) + Fase (der) =====
# Solo si la sesión tiene archivo de objetivo con goal no vacío.
GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json"
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)
if [ -n "$GOAL" ]; then
GC=$(goal_color "$SESSION_ID")
LEFT="${GC}🎯 ${GOAL}${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%%|*}"
HJOIN="${HJOIN}${HIC}"
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%%|*}"
REST="${PS#*|}"
PCOL="${REST%%|*}"
PLABEL="${REST#*|}"
LINE0="${LINE0} ${GRAY}${RESET} \033[1;${PCOL}m${PICON} ${PLABEL}${RESET}"
fi
echo -e "$LINE0"
# DoD en su propia linea debajo del objetivo, atenuado (🏁 = condicion de hecho).
[ -n "$DOD" ] && echo -e " ${DIM}🏁 ${DOD}${RESET}"
fi
fi
# Imprimir resultado (2 líneas)
echo -e "$LINE1"
echo -e "$LINE2"