Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2b5ac56eb | |||
| 86252b7d2c | |||
| e976fb303a | |||
| 8e53e0818e | |||
| bb735cad17 | |||
| 0e8d2d2ff2 | |||
| ffb3f9b270 | |||
| 1b769a9666 | |||
| 963b3bd7e1 | |||
| 393a77b597 | |||
| 50290a71e7 | |||
| a3ecb6a4cf | |||
| 1840402453 | |||
| 9ac52501b5 | |||
| 1a15108b56 | |||
| 54f47570d1 | |||
| adfb45015e | |||
| 3ae472d1f3 | |||
| fa2b2e16bc | |||
| 4d1fff53e9 | |||
| eb42966295 | |||
| 8c9919f1f8 | |||
| f881b7703b | |||
| 5efcedf9ba |
@@ -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
|
> (`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`;
|
> `~/.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.
|
> 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.
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ Wrapper sobre `tbd_branch_create_bash_infra`. La función del registry maneja to
|
|||||||
|
|
||||||
2. **Llamar la función del registry**:
|
2. **Llamar la función del registry**:
|
||||||
```bash
|
```bash
|
||||||
source /home/lucas/fn_registry/bash/functions/infra/tbd_branch_create.sh
|
# Path portable (cualquier PC): FN_REGISTRY_ROOT si está, si no ~/fn_registry.
|
||||||
tbd_branch_create issue 0021 hot-reload
|
# Se invoca con `bash` (no `source`): el script llama a tbd_branch_create con
|
||||||
|
# los argumentos al ejecutarse directamente, y así funciona aunque la shell de
|
||||||
|
# la sesión sea zsh (evita el fallo de BASH_SOURCE).
|
||||||
|
FN_TBD="${FN_REGISTRY_ROOT:-$HOME/fn_registry}/bash/functions/infra/tbd_branch_create.sh"
|
||||||
|
bash "$FN_TBD" issue 0021 hot-reload
|
||||||
# o
|
# o
|
||||||
tbd_branch_create quick fix-typo-readme
|
bash "$FN_TBD" quick fix-typo-readme
|
||||||
```
|
```
|
||||||
|
|
||||||
La función:
|
La función:
|
||||||
|
|||||||
@@ -81,8 +81,11 @@ Si autocontenido, saltar.
|
|||||||
### 5. Cerrar la rama (registry)
|
### 5. Cerrar la rama (registry)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source /home/lucas/fn_registry/bash/functions/infra/tbd_branch_finish.sh
|
# Path portable (cualquier PC): FN_REGISTRY_ROOT si está, si no ~/fn_registry.
|
||||||
tbd_branch_finish "<descripción corta del merge>"
|
# Se invoca con `bash` (no `source`): el script tiene un entry point que llama a
|
||||||
|
# tbd_branch_finish con los argumentos cuando se ejecuta directamente, y así
|
||||||
|
# funciona aunque la shell de la sesión sea zsh (evita el fallo de BASH_SOURCE).
|
||||||
|
bash "${FN_REGISTRY_ROOT:-$HOME/fn_registry}/bash/functions/infra/tbd_branch_finish.sh" "<descripción corta del merge>"
|
||||||
```
|
```
|
||||||
|
|
||||||
La función:
|
La función:
|
||||||
|
|||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/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 DEFINITIVO (usuario manual u otro autogen ya termino), no
|
||||||
|
# pisar. Un archivo PROVISIONAL (.provisional=true) SI se pisa: es el placeholder
|
||||||
|
# (= texto del usuario) que pusimos para que el statusline no quede vacio.
|
||||||
|
if [ -f "$F" ] && [ "$(jq -r '.provisional // false' "$F" 2>/dev/null)" != "true" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
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), un DoD breve (definition of done: condicion concreta de 'terminado', maximo 8 palabras, en espanol) y EXACTAMENTE 3 EMOJIS que representen visualmente la tarea (3 emojis pegados, sin espacios ni texto entre ellos). Responde SOLO un objeto JSON en una sola linea, sin markdown ni texto extra: {\"goal\":\"...\",\"dod\":\"...\",\"emojis\":\"🔭✨🌌\"}. 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)
|
||||||
|
EMOJIS=$(printf '%s' "$JSON" | jq -r '.emojis // ""' 2>/dev/null)
|
||||||
|
[ -z "$GOAL" ] && exit 0
|
||||||
|
|
||||||
|
# Carrera: si entre tanto aparecio un objetivo DEFINITIVO (manual), respetarlo.
|
||||||
|
# Si solo esta el provisional, lo pisamos abajo con el definitivo.
|
||||||
|
if [ -f "$F" ] && [ "$(jq -r '.provisional // false' "$F" 2>/dev/null)" != "true" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="${F}.tmp.$$"
|
||||||
|
if [ -f "$F" ]; then
|
||||||
|
# Pisar el provisional: fija goal/dod/emojis definitivos y quita el flag,
|
||||||
|
# preservando phase/history/prompts que el provisional ya hubiera acumulado.
|
||||||
|
if jq --arg g "$GOAL" --arg d "$DOD" --arg e "$EMOJIS" \
|
||||||
|
'del(.provisional) | .goal=$g | (if $d != "" then .dod=$d else . end) | (if $e != "" then .emojis=$e else . end)' \
|
||||||
|
"$F" > "$TMP" 2>/dev/null; then
|
||||||
|
mv "$TMP" "$F"
|
||||||
|
else
|
||||||
|
rm -f "$TMP"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if jq -n --arg g "$GOAL" --arg d "$DOD" --arg e "$EMOJIS" --arg p "$P" \
|
||||||
|
'{goal:$g, phase:"planificando", history:["planificando"], prompts:[$p]} | (if $d != "" then .dod=$d else . end) | (if $e != "" then .emojis=$e else . end)' > "$TMP" 2>/dev/null; then
|
||||||
|
mv "$TMP" "$F"
|
||||||
|
else
|
||||||
|
rm -f "$TMP"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
Executable
+75
@@ -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
|
||||||
Executable
+38
@@ -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
|
||||||
Executable
+122
@@ -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
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# DESACTIVADO (2026-06-21): este hook regeneraba el campo `.dod` (movil) del
|
||||||
|
# goal.json llamando a un LLM (haiku via ask_llm.py) en CADA prompt de CADA
|
||||||
|
# sesion. Con muchas sesiones de la flota activas a la vez eso amplificaba el
|
||||||
|
# rate-limit compartido de la organizacion ("Server is temporarily limiting
|
||||||
|
# requests"). Una request API por turno por agente = coste innecesario.
|
||||||
|
#
|
||||||
|
# El `.dod` movil NO lo consume nadie: el parser de la flota
|
||||||
|
# (functions/infra/list_claude_fleet.go, struct goalFile/readGoal) solo lee
|
||||||
|
# goal/phase/emojis/rename/dod_contract/dod_status/role; ignora `.dod` por
|
||||||
|
# completo. El criterio de aceptacion real que clasifica la flota es
|
||||||
|
# `dod_contract` + `dod_status`, escrito por set_dod_contract.py (sin LLM) y
|
||||||
|
# consumido por ClassifyFleetTermination. Ese sistema queda intacto.
|
||||||
|
#
|
||||||
|
# Por tanto la regeneracion del `.dod` movil con haiku se elimina por completo:
|
||||||
|
# cero llamadas LLM por prompt. El objetivo+DoD inicial los sigue generando
|
||||||
|
# goal_autogen.sh una sola vez por terminal (junto con goal/emojis, que si se
|
||||||
|
# usan); el usuario puede ajustar el DoD a mano con "dod: ...".
|
||||||
|
#
|
||||||
|
# Se conserva el archivo como no-op para no romper ningun disparador historico
|
||||||
|
# (defensa en profundidad). El disparo desde goal_tracker.sh tambien se retiro.
|
||||||
|
exit 0
|
||||||
Executable
+129
@@ -0,0 +1,129 @@
|
|||||||
|
#!/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"
|
||||||
|
[ -f "$F" ] && PROV=$(jq -r '.provisional // false' "$F" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -f "$F" ] && [ "$PROV" != "true" ]; 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.$$"
|
||||||
|
if [ -f "$F" ]; then
|
||||||
|
# Ya habia 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"
|
||||||
|
else
|
||||||
|
PROV_GOAL=$(printf '%s' "$PROMPT_TRIM" | head -c 70)
|
||||||
|
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
|
||||||
+57
-3
@@ -4,19 +4,66 @@
|
|||||||
"Edit(~/.claude/**)",
|
"Edit(~/.claude/**)",
|
||||||
"Write(~/.claude/**)",
|
"Write(~/.claude/**)",
|
||||||
"Edit(.claude/**)",
|
"Edit(.claude/**)",
|
||||||
"Write(.claude/**)"
|
"Write(.claude/**)",
|
||||||
|
"Bash(CGO_ENABLED=1 go test *)",
|
||||||
|
"Bash(sqlite3 *)",
|
||||||
|
"Read(//home/enmanuel/.claude/**)"
|
||||||
],
|
],
|
||||||
"deny": [
|
"deny": [
|
||||||
"Edit(~/.claude/.git/**)",
|
"Edit(~/.claude/.git/**)",
|
||||||
"Write(~/.claude/.git/**)",
|
"Write(~/.claude/.git/**)",
|
||||||
"Edit(.git/**)",
|
"Edit(.git/**)",
|
||||||
"Write(.git/**)"
|
"Write(.git/**)"
|
||||||
|
],
|
||||||
|
"defaultMode": "dontAsk"
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/hooks/goal_tracker.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Notification": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/hooks/goal_notify.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/hooks/goal_phase_eval.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/hooks/goal_phase_active.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"statusLine": {
|
"statusLine": {
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/statusline.sh",
|
"command": "~/.claude/statusline.sh",
|
||||||
"padding": 1
|
"padding": 1,
|
||||||
|
"refreshInterval": 2
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"gopls-lsp@claude-plugins-official": true,
|
"gopls-lsp@claude-plugins-official": true,
|
||||||
@@ -30,7 +77,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"language": "Español",
|
||||||
"effortLevel": "xhigh",
|
"effortLevel": "xhigh",
|
||||||
|
"voice": {
|
||||||
|
"enabled": true,
|
||||||
|
"mode": "hold"
|
||||||
|
},
|
||||||
"skipDangerousModePermissionPrompt": true,
|
"skipDangerousModePermissionPrompt": true,
|
||||||
"agentPushNotifEnabled": false
|
"preferredNotifChannel": "notifications_disabled",
|
||||||
|
"agentPushNotifEnabled": false,
|
||||||
|
"voiceEnabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-2
@@ -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_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')
|
CONTEXT_TOTAL=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 200000')
|
||||||
CURRENT_DIR=$(echo "$INPUT" | jq -r '.workspace.current_dir // "~"' | sed "s|$HOME|~|")
|
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)
|
# 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')
|
INPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.input_tokens // 0')
|
||||||
@@ -40,6 +45,25 @@ if [ "$CONTEXT_PCT" -eq 0 ] && [ "$CONTEXT_USED" -gt 0 ]; then
|
|||||||
CONTEXT_PCT=$(echo "scale=0; $CONTEXT_USED * 100 / $CONTEXT_TOTAL" | bc)
|
CONTEXT_PCT=$(echo "scale=0; $CONTEXT_USED * 100 / $CONTEXT_TOTAL" | bc)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Persistir el contexto por sesión en un sidecar para que fleetview (y otras
|
||||||
|
# herramientas) puedan mostrarlo sin tener este stdin. El statusline se re-ejecuta
|
||||||
|
# cada pocos segundos, así que el dato se mantiene fresco mientras la sesión vive.
|
||||||
|
if [ -n "$SESSION_ID" ]; then
|
||||||
|
RTDIR="$HOME/.claude/runtime"
|
||||||
|
mkdir -p "$RTDIR" 2>/dev/null
|
||||||
|
RTF="$RTDIR/${SESSION_ID}.json"
|
||||||
|
RTMP="${RTF}.tmp.$$"
|
||||||
|
if jq -n \
|
||||||
|
--argjson pct "${CONTEXT_PCT:-0}" \
|
||||||
|
--argjson used "${CONTEXT_USED:-0}" \
|
||||||
|
--argjson total "${CONTEXT_TOTAL:-200000}" \
|
||||||
|
'{ctx_pct:$pct, ctx_used:$used, ctx_total:$total}' > "$RTMP" 2>/dev/null; then
|
||||||
|
mv "$RTMP" "$RTF" 2>/dev/null
|
||||||
|
else
|
||||||
|
rm -f "$RTMP" 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Costos
|
# Costos
|
||||||
TOTAL_COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0' | xargs printf "%.3f")
|
TOTAL_COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0' | xargs printf "%.3f")
|
||||||
SESSION_DURATION=$(echo "$INPUT" | jq -r '.cost.total_duration_ms // 0')
|
SESSION_DURATION=$(echo "$INPUT" | jq -r '.cost.total_duration_ms // 0')
|
||||||
@@ -58,10 +82,18 @@ RESET_7D=""
|
|||||||
[ "$RESET_5H_EPOCH" -gt 0 ] && RESET_5H=$(date -d "@$RESET_5H_EPOCH" +"%H:%M" 2>/dev/null)
|
[ "$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)
|
[ "$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_BRANCH=""
|
||||||
GIT_STATUS=""
|
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")
|
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
|
||||||
|
|
||||||
# Obtener archivos staged, modified, untracked
|
# Obtener archivos staged, modified, untracked
|
||||||
@@ -89,8 +121,41 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
|
|||||||
|
|
||||||
# Trim trailing space
|
# Trim trailing space
|
||||||
GIT_STATUS=$(echo "$GIT_STATUS" | sed 's/ $//')
|
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
|
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
|
# Función para crear barra de progreso
|
||||||
progress_bar() {
|
progress_bar() {
|
||||||
local pct=$1
|
local pct=$1
|
||||||
@@ -251,6 +316,60 @@ fi
|
|||||||
# 6. Directorio actual
|
# 6. Directorio actual
|
||||||
LINE2="${LINE2} ${GRAY}│${RESET} ${BLUE}${CURRENT_DIR}${RESET}"
|
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)
|
||||||
|
EMOJIS=$(jq -r '.emojis // ""' "$GOAL_FILE" 2>/dev/null)
|
||||||
|
PROVISIONAL=$(jq -r '.provisional // false' "$GOAL_FILE" 2>/dev/null)
|
||||||
|
if [ -n "$GOAL" ]; then
|
||||||
|
GC=$(goal_color "$SESSION_ID")
|
||||||
|
# Prefijo del objetivo:
|
||||||
|
# - provisional (= tu propio texto, mientras haiku genera el real) -> ⏳ atenuado.
|
||||||
|
# - los 3 emojis generados (representan la tarea, igual que FleetView).
|
||||||
|
# - fallback al marcador generico de objetivo.
|
||||||
|
if [ "$PROVISIONAL" = "true" ]; then
|
||||||
|
LEFT="${GC}⏳ ${DIM}${GOAL}${RESET}"
|
||||||
|
elif [ -n "$EMOJIS" ]; then
|
||||||
|
LEFT="${GC}${EMOJIS} ${GOAL}${RESET}"
|
||||||
|
else
|
||||||
|
LEFT="${GC}🎯 ${GOAL}${RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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)
|
# Imprimir resultado (2 líneas)
|
||||||
echo -e "$LINE1"
|
echo -e "$LINE1"
|
||||||
echo -e "$LINE2"
|
echo -e "$LINE2"
|
||||||
|
|||||||
+77
-9
@@ -54,20 +54,26 @@ done
|
|||||||
echo ""
|
echo ""
|
||||||
echo "=== Instalando archivos de configuración ==="
|
echo "=== Instalando archivos de configuración ==="
|
||||||
|
|
||||||
# 1. Status Line Script
|
# 1. Status Line Script (enlace simbólico)
|
||||||
STATUSLINE_SOURCE="$REPO_DIR/.claude/statusline.sh"
|
STATUSLINE_SOURCE="$REPO_DIR/.claude/statusline.sh"
|
||||||
STATUSLINE_TARGET="$CLAUDE_DIR/statusline.sh"
|
STATUSLINE_TARGET="$CLAUDE_DIR/statusline.sh"
|
||||||
|
|
||||||
if [ -f "$STATUSLINE_SOURCE" ]; then
|
if [ -f "$STATUSLINE_SOURCE" ]; then
|
||||||
if [ -f "$STATUSLINE_TARGET" ]; then
|
chmod +x "$STATUSLINE_SOURCE"
|
||||||
BACKUP="$STATUSLINE_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
if [ -L "$STATUSLINE_TARGET" ] && [ "$(readlink "$STATUSLINE_TARGET")" = "$STATUSLINE_SOURCE" ]; then
|
||||||
echo "Backup: statusline.sh -> $BACKUP"
|
echo "OK: statusline.sh ya está enlazado correctamente"
|
||||||
mv "$STATUSLINE_TARGET" "$BACKUP"
|
else
|
||||||
|
# Symlink (roto o apuntando mal): borrar; archivo real: backup
|
||||||
|
if [ -L "$STATUSLINE_TARGET" ]; then
|
||||||
|
rm -f "$STATUSLINE_TARGET"
|
||||||
|
elif [ -e "$STATUSLINE_TARGET" ]; then
|
||||||
|
BACKUP="$STATUSLINE_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo "Backup: statusline.sh -> $BACKUP"
|
||||||
|
mv "$STATUSLINE_TARGET" "$BACKUP"
|
||||||
|
fi
|
||||||
|
ln -s "$STATUSLINE_SOURCE" "$STATUSLINE_TARGET"
|
||||||
|
echo "Enlazado: statusline.sh -> $STATUSLINE_SOURCE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp "$STATUSLINE_SOURCE" "$STATUSLINE_TARGET"
|
|
||||||
chmod +x "$STATUSLINE_TARGET"
|
|
||||||
echo "Copiado: statusline.sh (ejecutable)"
|
|
||||||
else
|
else
|
||||||
echo "WARN: statusline.sh no encontrado en el repo"
|
echo "WARN: statusline.sh no encontrado en el repo"
|
||||||
fi
|
fi
|
||||||
@@ -96,6 +102,66 @@ else
|
|||||||
echo "WARN: settings.json no encontrado en el repo"
|
echo "WARN: settings.json no encontrado en el repo"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 3. CLAUDE.md (enlace simbólico - preferencias globales)
|
||||||
|
CLAUDEMD_SOURCE="$REPO_DIR/.claude/CLAUDE.md"
|
||||||
|
CLAUDEMD_TARGET="$CLAUDE_DIR/CLAUDE.md"
|
||||||
|
|
||||||
|
if [ -f "$CLAUDEMD_SOURCE" ]; then
|
||||||
|
if [ -L "$CLAUDEMD_TARGET" ] && [ "$(readlink "$CLAUDEMD_TARGET")" = "$CLAUDEMD_SOURCE" ]; then
|
||||||
|
echo "OK: CLAUDE.md ya está enlazado correctamente"
|
||||||
|
else
|
||||||
|
# Symlink (roto o apuntando mal): borrar; archivo real: backup
|
||||||
|
if [ -L "$CLAUDEMD_TARGET" ]; then
|
||||||
|
rm -f "$CLAUDEMD_TARGET"
|
||||||
|
elif [ -e "$CLAUDEMD_TARGET" ]; then
|
||||||
|
BACKUP="$CLAUDEMD_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo "Backup: CLAUDE.md -> $BACKUP"
|
||||||
|
mv "$CLAUDEMD_TARGET" "$BACKUP"
|
||||||
|
fi
|
||||||
|
ln -s "$CLAUDEMD_SOURCE" "$CLAUDEMD_TARGET"
|
||||||
|
echo "Enlazado: CLAUDE.md -> $CLAUDEMD_SOURCE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARN: CLAUDE.md no encontrado en el repo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Instalando hooks (enlace simbólico por archivo) ===
|
||||||
|
echo ""
|
||||||
|
echo "=== Instalando hooks ==="
|
||||||
|
|
||||||
|
HOOKS_SOURCE_DIR="$REPO_DIR/.claude/hooks"
|
||||||
|
HOOKS_TARGET_DIR="$CLAUDE_DIR/hooks"
|
||||||
|
|
||||||
|
if [ -d "$HOOKS_SOURCE_DIR" ]; then
|
||||||
|
mkdir -p "$HOOKS_TARGET_DIR"
|
||||||
|
for hook in "$HOOKS_SOURCE_DIR"/*.sh; do
|
||||||
|
[ -e "$hook" ] || continue
|
||||||
|
chmod +x "$hook"
|
||||||
|
HOOK_NAME="$(basename "$hook")"
|
||||||
|
HOOK_TARGET="$HOOKS_TARGET_DIR/$HOOK_NAME"
|
||||||
|
|
||||||
|
# Si ya es symlink correcto, saltar
|
||||||
|
if [ -L "$HOOK_TARGET" ] && [ "$(readlink "$HOOK_TARGET")" = "$hook" ]; then
|
||||||
|
echo "OK: hooks/$HOOK_NAME ya está enlazado correctamente"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Symlink (roto o apuntando mal): borrar sin backup; archivo real: backup
|
||||||
|
if [ -L "$HOOK_TARGET" ]; then
|
||||||
|
rm -f "$HOOK_TARGET"
|
||||||
|
elif [ -e "$HOOK_TARGET" ]; then
|
||||||
|
BACKUP="$HOOK_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo "Backup: hooks/$HOOK_NAME -> $BACKUP"
|
||||||
|
mv "$HOOK_TARGET" "$BACKUP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -s "$hook" "$HOOK_TARGET"
|
||||||
|
echo "Enlazado: hooks/$HOOK_NAME -> $hook"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "WARN: $HOOKS_SOURCE_DIR no existe, saltando hooks"
|
||||||
|
fi
|
||||||
|
|
||||||
# === Limpieza de configuración que no debe cambiar ===
|
# === Limpieza de configuración que no debe cambiar ===
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Limpiando configuración inmutable ==="
|
echo "=== Limpiando configuración inmutable ==="
|
||||||
@@ -189,6 +255,8 @@ echo "Tus comandos y configuración ahora están sincronizados con el repositori
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Configuración instalada:"
|
echo "Configuración instalada:"
|
||||||
echo " • Skills, Agents y Commands enlazados simbólicamente"
|
echo " • Skills, Agents y Commands enlazados simbólicamente"
|
||||||
|
echo " • Hooks (goal_*.sh) enlazados simbólicamente"
|
||||||
|
echo " • CLAUDE.md (preferencias globales) enlazado"
|
||||||
echo " • Status Line configurada con vibecoding setup"
|
echo " • Status Line configurada con vibecoding setup"
|
||||||
echo " • Settings.json enlazado (compartido entre repos)"
|
echo " • Settings.json enlazado (compartido entre repos)"
|
||||||
echo " • Backups viejos limpiados (>7 días)"
|
echo " • Backups viejos limpiados (>7 días)"
|
||||||
|
|||||||
Reference in New Issue
Block a user