#!/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: fija/cambia el objetivo a mano (meta:/goal: equivalen). # objetivo: clear lo borra (tambien -, none, borrar, quitar, reset). # dod: 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 al usuario. block() { jq -n --arg r "$1" '{decision:"block", reason:$r}'; exit 0; } # --- objetivo: (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 # --- /rename : guarda el nombre para FleetView (.rename del goal) pero NO # bloquea, para que el comando NATIVO /rename de Claude Code renombre tambien # la sesion (prompt bar + /resume picker). Por eso NO debe existir # ~/.claude/commands/rename.md: competiria con el built-in y lo taparia. --- RENAME_LINE=$(printf '%s' "$PROMPT" | grep -ioE '^[[:space:]]*/rename([[:space:]]+.*)?$' | head -1) if [ -n "$RENAME_LINE" ]; then NEWNAME=$(printf '%s' "$RENAME_LINE" | sed -E 's#^[[:space:]]*/rename[[:space:]]*##; s/[[:space:]]+$//') case "$NEWNAME" in ""|-|clear|none|borrar|quitar|reset) # /rename sin texto: Claude Code auto-genera; borramos el override de FleetView. [ -f "$F" ] && { TMP="${F}.tmp.$$"; jq 'del(.rename)' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F"; } ;; *) if [ -f "$F" ]; then TMP="${F}.tmp.$$"; jq --arg n "$NEWNAME" '.rename=$n' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F" else TMP="${F}.tmp.$$"; jq -n --arg n "$NEWNAME" '{rename:$n, phase:"planificando", prompts:[]}' > "$TMP" 2>/dev/null && mv "$TMP" "$F" fi ;; esac # NO bloquear: el prompt sigue su curso y el built-in /rename de Claude Code # renombra la sesion. El hook solo capturo el nombre para la lista de FleetView. exit 0 fi # --- dod: --- DOD_LINE=$(printf '%s' "$PROMPT" | grep -ioE '^[[:space:]]*dod[[:space:]]*:[[:space:]]*.+' | head -1) if [ -n "$DOD_LINE" ]; then NEWDOD=$(printf '%s' "$DOD_LINE" | sed -E 's/^[^:]*:[[:space:]]*//; s/[[:space:]]+$//') [ -f "$F" ] || block "Fija primero un objetivo (\"objetivo: ...\") antes del DoD." case "$NEWDOD" in -|clear|none|borrar|quitar|reset) TMP="${F}.tmp.$$" jq 'del(.dod)' "$F" > "$TMP" 2>/dev/null && mv "$TMP" "$F" block "🏁 DoD borrado." ;; esac TMP="${F}.tmp.$$" if jq --arg d "$NEWDOD" '.dod=$d' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F"; else rm -f "$TMP"; fi block "🏁 DoD fijado: ${NEWDOD}" fi # --- pausa (marca manual; Ctrl-C no dispara hooks en Claude Code) --- case "$PROMPT_TRIM" in pausa|pause|pausar|"en pausa"|/pausa) [ -f "$F" ] || block "No hay objetivo en esta terminal." TMP="${F}.tmp.$$" if jq '.phase="en_pausa" | .history=((.history // [])+["en_pausa"])[-12:]' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F" fi block "⏸️ Fase marcada en pausa." ;; esac # --- prompt NORMAL: pasa al agente + acumula para refinar el DoD + estado --- if [ -f "$F" ]; then G=$(jq -r '.goal // ""' "$F" 2>/dev/null) P=$(jq -r '.phase // ""' "$F" 2>/dev/null) D=$(jq -r '.dod // ""' "$F" 2>/dev/null) # Acumular el prompt y ajustar SOLO el DoD (background). El objetivo no cambia. if [ "${#PROMPT_TRIM}" -ge 12 ]; then TMP="${F}.tmp.$$" if jq --arg p "$PROMPT_TRIM" '.prompts = ((.prompts // []) + [$p])[-12:]' "$F" > "$TMP" 2>/dev/null; then mv "$TMP" "$F" else rm -f "$TMP" fi nohup bash "$HOME/.claude/hooks/goal_refine.sh" "$SID" "$F" >/dev/null 2>&1 & fi echo "GOAL-TRACKER: file=$F | goal=\"$G\" dod=\"$D\" phase=\"$P\". El objetivo es fijo (identificativo de la terminal, NO lo cambies). El DoD se ajusta solo con los prompts; la fase la mantienen los hooks (PostToolUse=activo, Stop=reposo) β€” NO la escribas. Comandos meta del usuario (no los uses tu): objetivo:/dod:/pausa." else # Sin objetivo: autogenerar objetivo + DoD desde el primer prompt sustantivo, # en background (no bloquea). Se omite para prompts triviales (saludos, ok...). if [ "${#PROMPT_TRIM}" -ge 12 ]; then nohup bash "$HOME/.claude/hooks/goal_autogen.sh" "$SID" "$F" "$PROMPT" >/dev/null 2>&1 & fi echo "GOAL-TRACKER: file=$F (sin objetivo aun). Autogenerando objetivo+DoD desde tu prompt en background (haiku). El usuario tambien puede fijarlo con \"objetivo: ...\" / \"dod: ...\"." fi exit 0