Compare commits

...

24 Commits

Author SHA1 Message Date
agent e2b5ac56eb fix(goal-tracker): eliminar regeneracion LLM (haiku) del dod movil en cada prompt
El hook goal_refine.sh regeneraba el campo .dod del goal.json llamando a
ask_llm.py (haiku) en background en CADA UserPromptSubmit de CADA sesion.
Con muchas sesiones de la flota activas esto amplificaba el rate-limit
compartido de la organizacion (una request API por turno por agente).

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. El criterio que
clasifica la flota (RECLAMA/DICE_TERMINADO/ESTANCADO) es dod_contract +
dod_status, escrito por set_dod_contract.py sin LLM y consumido por
ClassifyFleetTermination. Ese sistema queda intacto.

Cambios:
- goal_refine.sh: convertido en no-op (exit 0) documentado.
- goal_tracker.sh: retirado el disparo de goal_refine + la acumulacion de
  .prompts que solo lo alimentaba; mensaje GOAL-TRACKER actualizado.

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 ajusta el
DoD a mano con 'dod: ...'. Resultado: cero llamadas LLM por prompt.
2026-06-21 12:56:14 +02:00
agent 86252b7d2c chore(goals): retirar slash command /rename (lo reemplaza alt+r de FleetView)
El rename de la terminal en FleetView se hace ahora con alt+r dentro de
la TUI, que escribe el campo .rename del goal directamente. Se elimina el
slash command rename.md y la nota del hook lo documenta, dejando libre el
built-in /rename de Claude Code para renombrar la sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:23:09 +02:00
agent e976fb303a fix(goals): /rename solo para FleetView + objetivo provisional
- /rename escribe el nombre en FleetView (.rename del goal). NO renombra el titulo
  de la sesion de Claude Code: el built-in /rename usa estado interno y no re-lee
  el transcript, asi que un evento ai-title no cambia el prompt bar (comprobado).
- objetivo provisional: el primer prompt fija un goal placeholder hasta que haiku
  genera el definitivo, para que el statusline no quede vacio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:09:11 +02:00
agent 8e53e0818e fix(goals): /rename delega al built-in de Claude Code (no bloquea)
El hook capturaba /rename y bloqueaba el prompt, impidiendo que el comando NATIVO
/rename de Claude Code renombrara la sesion. Ahora el hook guarda el nombre para
FleetView (.rename del goal) y NO bloquea, asi el built-in tambien actua. Elimina
commands/rename.md (competia con el built-in y lo tapaba).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:36:09 +02:00
agent bb735cad17 feat(goals): emojis de objetivo + /rename + sidecar de contexto para FleetView
- goal_autogen.sh: genera 3 emojis representativos del objetivo (haiku) junto al
  goal+DoD, guardados en goals/<id>.json.
- goal_tracker.sh: comando meta /rename (y rename:) para nombrar la terminal;
  se guarda en goals/<id>.json .rename.
- commands/rename.md: slash command /rename.
- statusline.sh: persiste el % de contexto por sesion en runtime/<id>.json para
  que FleetView lo muestre.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:04:41 +02:00
egutierrez 0e8d2d2ff2 chore: auto-commit (1 archivos)
- .claude/settings.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-13 21:56:56 +02:00
egutierrez ffb3f9b270 fix(commands): path portable + invocación bash en /git-push y /git-branch
Los comandos hardcodeaban /home/lucas/fn_registry y hacían 'source' del script TBD, lo que fallaba en otros PCs (path inexistente) y bajo zsh (BASH_SOURCE sin definir).

- Path portable: ${FN_REGISTRY_ROOT:-$HOME/fn_registry} — usa la env var si está, si no ~/fn_registry. Válido en cualquier PC del ecosistema.
- Invocación con 'bash <script> <args>' en vez de 'source': los scripts tbd_branch_finish.sh y tbd_branch_create.sh tienen un entry point (if BASH_SOURCE[0] == $0) que llama a la función con los argumentos al ejecutarse directamente. Así funciona aunque la shell de la sesión sea zsh.

No se renombra el archivo del comando; solo se corrige la invocación interna. No incluye .claude/settings.json (cambio ajeno a esta tarea).
2026-06-13 14:47:52 +02:00
egutierrez 1b769a9666 chore: auto-commit (1 archivos)
- .claude/settings.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 00:16:46 +02:00
egutierrez 963b3bd7e1 fix(install): enlazar hooks y CLAUDE.md, reparar symlinks rotos
install.sh ahora gestiona los hooks goal_*.sh y CLAUDE.md ademas de
skills/agents/commands/settings. Antes quedaban fuera del script, por lo
que al mover repo_Claude de ~/DataProyects a fn_registry/external los
symlinks de hooks/ quedaban colgando y los hooks goal_* fallaban con
"not found".

Cambios:
- Enlace simbolico por archivo de todos los hooks .sh del repo.
- Enlace simbolico de CLAUDE.md (preferencias globales).
- statusline.sh pasa de copia a symlink (elimina backups basura por corrida).
- Logica de relink idempotente: symlink roto o mal-apuntado se borra y
  recrea; solo los archivos reales se respaldan en backup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:12:56 +02:00
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
12 changed files with 733 additions and 19 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.
+7 -3
View File
@@ -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**:
```bash
source /home/lucas/fn_registry/bash/functions/infra/tbd_branch_create.sh
tbd_branch_create issue 0021 hot-reload
# Path portable (cualquier PC): FN_REGISTRY_ROOT si está, si no ~/fn_registry.
# 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
tbd_branch_create quick fix-typo-readme
bash "$FN_TBD" quick fix-typo-readme
```
La función:
+5 -2
View File
@@ -81,8 +81,11 @@ Si autocontenido, saltar.
### 5. Cerrar la rama (registry)
```bash
source /home/lucas/fn_registry/bash/functions/infra/tbd_branch_finish.sh
tbd_branch_finish "<descripción corta del merge>"
# Path portable (cualquier PC): FN_REGISTRY_ROOT si está, si no ~/fn_registry.
# 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:
+66
View File
@@ -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
+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
+22
View File
@@ -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
+129
View File
@@ -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
View File
@@ -4,19 +4,66 @@
"Edit(~/.claude/**)",
"Write(~/.claude/**)",
"Edit(.claude/**)",
"Write(.claude/**)"
"Write(.claude/**)",
"Bash(CGO_ENABLED=1 go test *)",
"Bash(sqlite3 *)",
"Read(//home/enmanuel/.claude/**)"
],
"deny": [
"Edit(~/.claude/.git/**)",
"Write(~/.claude/.git/**)",
"Edit(.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": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 1
"padding": 1,
"refreshInterval": 2
},
"enabledPlugins": {
"gopls-lsp@claude-plugins-official": true,
@@ -30,7 +77,14 @@
}
}
},
"language": "Español",
"effortLevel": "xhigh",
"voice": {
"enabled": true,
"mode": "hold"
},
"skipDangerousModePermissionPrompt": true,
"agentPushNotifEnabled": false
"preferredNotifChannel": "notifications_disabled",
"agentPushNotifEnabled": false,
"voiceEnabled": true
}
+121 -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')
@@ -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)
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
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')
@@ -58,10 +82,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 +121,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 +316,60 @@ 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)
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)
echo -e "$LINE1"
echo -e "$LINE2"
+77 -9
View File
@@ -54,20 +54,26 @@ done
echo ""
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_TARGET="$CLAUDE_DIR/statusline.sh"
if [ -f "$STATUSLINE_SOURCE" ]; then
if [ -f "$STATUSLINE_TARGET" ]; then
BACKUP="$STATUSLINE_TARGET.backup.$(date +%Y%m%d_%H%M%S)"
echo "Backup: statusline.sh -> $BACKUP"
mv "$STATUSLINE_TARGET" "$BACKUP"
chmod +x "$STATUSLINE_SOURCE"
if [ -L "$STATUSLINE_TARGET" ] && [ "$(readlink "$STATUSLINE_TARGET")" = "$STATUSLINE_SOURCE" ]; then
echo "OK: statusline.sh ya está enlazado correctamente"
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
cp "$STATUSLINE_SOURCE" "$STATUSLINE_TARGET"
chmod +x "$STATUSLINE_TARGET"
echo "Copiado: statusline.sh (ejecutable)"
else
echo "WARN: statusline.sh no encontrado en el repo"
fi
@@ -96,6 +102,66 @@ else
echo "WARN: settings.json no encontrado en el repo"
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 ===
echo ""
echo "=== Limpiando configuración inmutable ==="
@@ -189,6 +255,8 @@ echo "Tus comandos y configuración ahora están sincronizados con el repositori
echo ""
echo "Configuración instalada:"
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 " • Settings.json enlazado (compartido entre repos)"
echo " • Backups viejos limpiados (>7 días)"