diff --git a/skills/continuous-learning-v2/agents/observer-loop.sh b/skills/continuous-learning-v2/agents/observer-loop.sh index f4aca82b..a0f655dd 100755 --- a/skills/continuous-learning-v2/agents/observer-loop.sh +++ b/skills/continuous-learning-v2/agents/observer-loop.sh @@ -91,7 +91,8 @@ PROMPT max_turns=10 fi - claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 & + # Prevent observe.sh from recording this automated Haiku session as observations + ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 & claude_pid=$! ( diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 33ec6f04..90a4a557 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -98,6 +98,54 @@ if [ -f "$CONFIG_DIR/disabled" ]; then exit 0 fi +# ───────────────────────────────────────────── +# Automated session guards +# Prevents observe.sh from firing on non-human sessions to avoid: +# - ECC observing its own Haiku observer sessions (self-loop) +# - ECC observing other tools' automated sessions (e.g. claude-mem) +# - All-night Haiku usage with no human activity +# ───────────────────────────────────────────── + +# Env-var checks first (cheapest — no subprocess spawning): + +# Layer 1: CLAUDE_CODE_ENTRYPOINT — set by Claude Code itself to indicate how +# it was invoked. Only interactive terminal sessions should continue; treat any +# explicit non-cli entrypoint as automated so future entrypoint types fail closed +# without requiring updates here. +case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in + cli) ;; + *) exit 0 ;; +esac + +# Layer 2: Respect ECC_HOOK_PROFILE=minimal — suppresses non-essential hooks +[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0 + +# Layer 3: Cooperative skip env var — tools like claude-mem can set this +# (export ECC_SKIP_OBSERVE=1) before spawning their automated sessions +[ "${ECC_SKIP_OBSERVE:-0}" = "1" ] && exit 0 + +# Layer 4: Skip subagent sessions — agent_id is only present when a hook fires +# inside a subagent (automated by definition, never a human interactive session). +# Placed after env-var checks to avoid a Python subprocess on sessions that +# already exit via Layers 1-3. +_ECC_AGENT_ID=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('agent_id',''))" 2>/dev/null || true) +[ -n "$_ECC_AGENT_ID" ] && exit 0 + +# Layer 5: CWD path exclusions — skip known observer-session directories. +# Add custom paths via ECC_OBSERVE_SKIP_PATHS (comma-separated substrings). +# Whitespace is trimmed from each pattern; empty patterns are skipped to +# prevent an empty-string glob from matching every path. +_ECC_SKIP_PATHS="${ECC_OBSERVE_SKIP_PATHS:-observer-sessions,.claude-mem}" +if [ -n "$STDIN_CWD" ]; then + IFS=',' read -ra _ECC_SKIP_ARRAY <<< "$_ECC_SKIP_PATHS" + for _pattern in "${_ECC_SKIP_ARRAY[@]}"; do + _pattern="${_pattern#"${_pattern%%[![:space:]]*}"}" # trim leading whitespace + _pattern="${_pattern%"${_pattern##*[![:space:]]}"}" # trim trailing whitespace + [ -z "$_pattern" ] && continue + case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac + done +fi + # Auto-purge observation files older than 30 days (runs once per session) PURGE_MARKER="${PROJECT_DIR}/.last-purge" if [ ! -f "$PURGE_MARKER" ] || [ "$(find "$PURGE_MARKER" -mtime +1 2>/dev/null)" ]; then