mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 11:23:32 +08:00
Merge branch 'main' into main
This commit is contained in:
@@ -38,6 +38,12 @@ analyze_observations() {
|
||||
return
|
||||
fi
|
||||
|
||||
# session-guardian: gate observer cycle (active hours, cooldown, idle detection)
|
||||
if ! bash "$(dirname "$0")/session-guardian.sh"; then
|
||||
echo "[$(date)] Observer cycle skipped by session-guardian" >> "$LOG_FILE"
|
||||
return
|
||||
fi
|
||||
|
||||
prompt_file="$(mktemp "${TMPDIR:-/tmp}/ecc-observer-prompt.XXXXXX")"
|
||||
cat > "$prompt_file" <<PROMPT
|
||||
Read ${OBSERVATIONS_FILE} and identify patterns for the project ${PROJECT_NAME} (user corrections, error resolutions, repeated workflows, tool preferences).
|
||||
@@ -91,7 +97,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=$!
|
||||
|
||||
(
|
||||
|
||||
150
skills/continuous-learning-v2/agents/session-guardian.sh
Executable file
150
skills/continuous-learning-v2/agents/session-guardian.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# session-guardian.sh — Observer session guard
|
||||
# Exit 0 = proceed. Exit 1 = skip this observer cycle.
|
||||
# Called by observer-loop.sh before spawning any Claude session.
|
||||
#
|
||||
# Config (env vars, all optional):
|
||||
# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown)
|
||||
# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log
|
||||
# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, set to 0 to disable)
|
||||
# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, set to 0 to disable)
|
||||
# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; set to 0 to disable)
|
||||
#
|
||||
# Gate execution order (cheapest first):
|
||||
# Gate 1: Time window check (~0ms, string comparison)
|
||||
# Gate 2: Project cooldown log (~1ms, file read + mkdir lock)
|
||||
# Gate 3: Idle detection (~5-50ms, OS syscall; fail open)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INTERVAL="${OBSERVER_INTERVAL_SECONDS:-300}"
|
||||
LOG_PATH="${OBSERVER_LAST_RUN_LOG:-$HOME/.claude/observer-last-run.log}"
|
||||
ACTIVE_START="${OBSERVER_ACTIVE_HOURS_START:-800}"
|
||||
ACTIVE_END="${OBSERVER_ACTIVE_HOURS_END:-2300}"
|
||||
MAX_IDLE="${OBSERVER_MAX_IDLE_SECONDS:-1800}"
|
||||
|
||||
# ── Gate 1: Time Window ───────────────────────────────────────────────────────
|
||||
# Skip observer cycles outside configured active hours (local system time).
|
||||
# Uses HHMM integer comparison. Works on BSD date (macOS) and GNU date (Linux).
|
||||
# Supports overnight windows such as 2200-0600.
|
||||
# Set both ACTIVE_START and ACTIVE_END to 0 to disable this gate.
|
||||
if [ "$ACTIVE_START" -ne 0 ] || [ "$ACTIVE_END" -ne 0 ]; then
|
||||
current_hhmm=$(date +%k%M | tr -d ' ')
|
||||
current_hhmm_num=$(( 10#${current_hhmm:-0} ))
|
||||
active_start_num=$(( 10#${ACTIVE_START:-800} ))
|
||||
active_end_num=$(( 10#${ACTIVE_END:-2300} ))
|
||||
|
||||
within_active_hours=0
|
||||
if [ "$active_start_num" -lt "$active_end_num" ]; then
|
||||
if [ "$current_hhmm_num" -ge "$active_start_num" ] && [ "$current_hhmm_num" -lt "$active_end_num" ]; then
|
||||
within_active_hours=1
|
||||
fi
|
||||
else
|
||||
if [ "$current_hhmm_num" -ge "$active_start_num" ] || [ "$current_hhmm_num" -lt "$active_end_num" ]; then
|
||||
within_active_hours=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$within_active_hours" -ne 1 ]; then
|
||||
echo "session-guardian: outside active hours (${current_hhmm}, window ${ACTIVE_START}-${ACTIVE_END})" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Gate 2: Project Cooldown Log ─────────────────────────────────────────────
|
||||
# Prevent the same project being observed faster than OBSERVER_INTERVAL_SECONDS.
|
||||
# Key: PROJECT_DIR when provided by the observer, otherwise git root path.
|
||||
# Uses mkdir-based lock for safe concurrent access. Skips the cycle on lock contention.
|
||||
# stderr uses basename only — never prints the full absolute path.
|
||||
|
||||
project_root="${PROJECT_DIR:-}"
|
||||
if [ -z "$project_root" ] || [ ! -d "$project_root" ]; then
|
||||
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
|
||||
fi
|
||||
project_name="$(basename "$project_root")"
|
||||
now="$(date +%s)"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_PATH")" || {
|
||||
echo "session-guardian: cannot create log dir, proceeding" >&2
|
||||
exit 0
|
||||
}
|
||||
|
||||
_lock_dir="${LOG_PATH}.lock"
|
||||
if ! mkdir "$_lock_dir" 2>/dev/null; then
|
||||
# Another observer holds the lock — skip this cycle to avoid double-spawns
|
||||
echo "session-guardian: log locked by concurrent process, skipping cycle" >&2
|
||||
exit 1
|
||||
else
|
||||
trap 'rm -rf "$_lock_dir"' EXIT INT TERM
|
||||
|
||||
last_spawn=0
|
||||
last_spawn=$(awk -F '\t' -v key="$project_root" '$1 == key { value = $2 } END { if (value != "") print value }' "$LOG_PATH" 2>/dev/null) || true
|
||||
last_spawn="${last_spawn:-0}"
|
||||
[[ "$last_spawn" =~ ^[0-9]+$ ]] || last_spawn=0
|
||||
|
||||
elapsed=$(( now - last_spawn ))
|
||||
if [ "$elapsed" -lt "$INTERVAL" ]; then
|
||||
rm -rf "$_lock_dir"
|
||||
trap - EXIT INT TERM
|
||||
echo "session-guardian: cooldown active for '${project_name}' (last spawn ${elapsed}s ago, interval ${INTERVAL}s)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update log: remove old entry for this project, append new timestamp (tab-delimited)
|
||||
tmp_log="$(mktemp "$(dirname "$LOG_PATH")/observer-last-run.XXXXXX")"
|
||||
awk -F '\t' -v key="$project_root" '$1 != key' "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
|
||||
printf '%s\t%s\n' "$project_root" "$now" >> "$tmp_log"
|
||||
mv "$tmp_log" "$LOG_PATH"
|
||||
|
||||
rm -rf "$_lock_dir"
|
||||
trap - EXIT INT TERM
|
||||
fi
|
||||
|
||||
# ── Gate 3: Idle Detection ────────────────────────────────────────────────────
|
||||
# Skip cycles when no user input received for too long. Fail open if idle time
|
||||
# cannot be determined (Linux without xprintidle, headless, unknown OS).
|
||||
# Set OBSERVER_MAX_IDLE_SECONDS=0 to disable this gate.
|
||||
|
||||
get_idle_seconds() {
|
||||
local _raw
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
_raw=$( { /usr/sbin/ioreg -c IOHIDSystem \
|
||||
| /usr/bin/awk '/HIDIdleTime/ {print int($NF/1000000000); exit}'; } \
|
||||
2>/dev/null ) || true
|
||||
printf '%s\n' "${_raw:-0}" | head -n1
|
||||
;;
|
||||
Linux)
|
||||
if command -v xprintidle >/dev/null 2>&1; then
|
||||
_raw=$(xprintidle 2>/dev/null) || true
|
||||
echo $(( ${_raw:-0} / 1000 ))
|
||||
else
|
||||
echo 0 # fail open: xprintidle not installed
|
||||
fi
|
||||
;;
|
||||
*MINGW*|*MSYS*|*CYGWIN*)
|
||||
_raw=$(powershell.exe -NoProfile -NonInteractive -Command \
|
||||
"try { \
|
||||
Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO p); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public int dwTime; }' -Name WinAPI -Namespace PInvoke; \
|
||||
\$l = New-Object PInvoke.WinAPI+LASTINPUTINFO; \$l.cbSize = 8; \
|
||||
[PInvoke.WinAPI]::GetLastInputInfo([ref]\$l) | Out-Null; \
|
||||
[int][Math]::Max(0, [long]([Environment]::TickCount - [long]\$l.dwTime) / 1000) \
|
||||
} catch { 0 }" \
|
||||
2>/dev/null | tr -d '\r') || true
|
||||
printf '%s\n' "${_raw:-0}" | head -n1
|
||||
;;
|
||||
*)
|
||||
echo 0 # fail open: unknown platform
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$MAX_IDLE" -gt 0 ]; then
|
||||
idle_seconds=$(get_idle_seconds)
|
||||
if [ "$idle_seconds" -gt "$MAX_IDLE" ]; then
|
||||
echo "session-guardian: user idle ${idle_seconds}s (threshold ${MAX_IDLE}s), skipping" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -113,8 +113,56 @@ if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# FIX: Skip if a previous run already aborted due to confirmation/permission prompt
|
||||
# This is the circuit-breaker — stops retrying after a non-interactive failure
|
||||
# ─────────────────────────────────────────────
|
||||
# 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
|
||||
|
||||
# Skip if a previous run already aborted due to confirmation/permission prompt.
|
||||
# This is the circuit-breaker — stops retrying after a non-interactive failure.
|
||||
if [ -f "$SENTINEL_FILE" ]; then
|
||||
echo "[observe] Skipping: previous run aborted due to confirmation/permission prompt. Remove ${SENTINEL_FILE} to re-enable." >&2
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user