eagle-mem 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/eagle-mem CHANGED
@@ -30,6 +30,7 @@ case "$command" in
30
30
  config) bash "$SCRIPTS_DIR/config.sh" "$@" ;;
31
31
  curate) bash "$SCRIPTS_DIR/curate.sh" "$@" ;;
32
32
  feature) bash "$SCRIPTS_DIR/feature.sh" "$@" ;;
33
+ health) bash "$SCRIPTS_DIR/health.sh" "$@" ;;
33
34
  help|--help|-h)
34
35
  bash "$SCRIPTS_DIR/help.sh" ;;
35
36
  version|--version|-v|-V)
@@ -0,0 +1,8 @@
1
+ -- Eagle meta key-value store for system state (curator timestamps, etc.)
2
+ CREATE TABLE IF NOT EXISTS eagle_meta (
3
+ key TEXT NOT NULL,
4
+ project TEXT,
5
+ value TEXT NOT NULL,
6
+ updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
7
+ UNIQUE(key, project)
8
+ );
@@ -31,6 +31,7 @@ esac
31
31
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
32
32
 
33
33
  project=$(eagle_project_from_cwd "$cwd")
34
+ [ -z "$project" ] && exit 0
34
35
 
35
36
  # Ensure session row exists before inserting observations (FK constraint).
36
37
  # PostToolUse can race SessionStart — the session row might not exist yet.
@@ -27,6 +27,7 @@ cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
27
27
  session_id=$(echo "$input" | jq -r '.session_id // empty')
28
28
  cwd=$(echo "$input" | jq -r '.cwd // empty')
29
29
  project=$(eagle_project_from_cwd "$cwd")
30
+ [ -z "$project" ] && exit 0
30
31
 
31
32
  context=""
32
33
 
@@ -2,15 +2,17 @@
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — SessionEnd hook
4
4
  # Fires when the Claude Code session ends
5
- # Marks the session as completed
5
+ # Marks the session as completed + triggers auto-curate
6
6
  # ═══════════════════════════════════════════════════════════
7
7
  set +e
8
8
 
9
9
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
10
  LIB_DIR="$SCRIPT_DIR/../lib"
11
+ SCRIPTS_DIR="$SCRIPT_DIR/../scripts"
11
12
 
12
13
  . "$LIB_DIR/common.sh"
13
14
  . "$LIB_DIR/db.sh"
15
+ . "$LIB_DIR/provider.sh"
14
16
 
15
17
  input=$(eagle_read_stdin)
16
18
  [ -z "$input" ] && exit 0
@@ -21,6 +23,7 @@ session_id=$(echo "$input" | jq -r '.session_id // empty')
21
23
 
22
24
  cwd=$(echo "$input" | jq -r '.cwd // empty')
23
25
  project=$(eagle_project_from_cwd "$cwd")
26
+ [ -z "$project" ] && exit 0
24
27
 
25
28
  # Final sweep: re-capture all task files to catch status changes
26
29
  # Claude Code may update task status without triggering PostToolUse
@@ -41,4 +44,23 @@ eagle_log "INFO" "SessionEnd: session=$session_id marked completed"
41
44
  # Prune observations older than 90 days (keeps DB size bounded)
42
45
  eagle_prune_observations 90 "$project"
43
46
 
47
+ # ─── Auto-curate trigger ─────────────────────────────────
48
+ curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
49
+ if [ "$curator_schedule" = "auto" ]; then
50
+ provider=$(eagle_config_get "provider" "type" "none")
51
+ if [ "$provider" != "none" ]; then
52
+ min_sessions=$(eagle_config_get "curator" "min_sessions" "5")
53
+ min_sessions=$(eagle_sql_int "$min_sessions")
54
+
55
+ last_curated=$(eagle_meta_get "last_curated_at" "$project")
56
+ since="${last_curated:-1970-01-01T00:00:00Z}"
57
+
58
+ sessions_since=$(eagle_count_sessions_since "$project" "$since")
59
+ if [ "${sessions_since:-0}" -ge "$min_sessions" ]; then
60
+ eagle_log "INFO" "SessionEnd: auto-curate triggered (${sessions_since} sessions since last curate)"
61
+ nohup bash "$SCRIPTS_DIR/curate.sh" -p "$project" >> "$EAGLE_MEM_LOG" 2>&1 &
62
+ fi
63
+ fi
64
+ fi
65
+
44
66
  exit 0
@@ -26,6 +26,9 @@ model=$(echo "$input" | jq -r '.model // empty')
26
26
 
27
27
  project=$(eagle_project_from_cwd "$cwd")
28
28
 
29
+ # Skip ephemeral directories (tmp, Downloads, etc.) — no tracking
30
+ [ -z "$project" ] && exit 0
31
+
29
32
  eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
30
33
 
31
34
  eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
@@ -134,6 +137,15 @@ if [ -n "$update_notice" ]; then
134
137
  "
135
138
  fi
136
139
 
140
+ # Nudge if last session lacked enrichment
141
+ last_enriched=$(eagle_last_session_enriched "$project")
142
+ if [ "${last_enriched:-1}" = "0" ] && [ "$stat_with_summaries" -gt 0 ]; then
143
+ context+="=== EAGLE MEM — Enrichment Reminder ===
144
+ The previous session's summary did NOT include decisions, gotchas, or key_files. These fields power Eagle Mem's self-learning (feature discovery, anti-regression, command intelligence). Please emit an <eagle-summary> block at the end of this session with these fields populated.
145
+
146
+ "
147
+ fi
148
+
137
149
  # Project overview
138
150
  overview=$(eagle_get_overview "$project")
139
151
  if [ -n "$overview" ]; then
package/hooks/stop.sh CHANGED
@@ -12,6 +12,7 @@ LIB_DIR="$SCRIPT_DIR/../lib"
12
12
 
13
13
  . "$LIB_DIR/common.sh"
14
14
  . "$LIB_DIR/db.sh"
15
+ . "$LIB_DIR/provider.sh"
15
16
 
16
17
  eagle_ensure_db
17
18
 
@@ -29,6 +30,7 @@ agent_type=$(echo "$input" | jq -r '.agent_type // empty')
29
30
  [ -n "$agent_type" ] && [ "$agent_type" != "main" ] && exit 0
30
31
 
31
32
  project=$(eagle_project_from_cwd "$cwd")
33
+ [ -z "$project" ] && exit 0
32
34
 
33
35
  eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path"
34
36
 
@@ -111,21 +113,25 @@ if [ -n "$summary_block" ]; then
111
113
  eagle_log "INFO" "Stop: parsed eagle-summary block"
112
114
  fi
113
115
 
114
- # ─── Heuristic fallback: extract from tool calls ───────────
116
+ # ─── Guard: skip fallback work if summary already exists ──
117
+ # Stop fires every assistant turn. Without this, the heuristic and LLM
118
+ # enrichment blocks fire on turn 2+ — wasting tokens and producing
119
+ # empty inserts that get rejected.
115
120
 
116
- if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
117
- # Skip heuristic if we already have a summary for this session.
118
- # Stop fires every turn -- without this guard, each turn creates a duplicate row.
121
+ existing_count=0
122
+ if [ -z "$summary_block" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
119
123
  existing_count=$(eagle_count_session_summaries "$session_id")
120
- if [ "${existing_count:-0}" -gt 0 ]; then
121
- eagle_log "INFO" "Stop: skipping heuristic — summary already exists for session=$session_id (count=$existing_count)"
122
- else
124
+ fi
125
+
126
+ if [ -z "$summary_block" ] && [ "${existing_count:-0}" -eq 0 ]; then
127
+
128
+ # ─── Heuristic fallback: extract from tool calls ───────────
129
+
130
+ if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
123
131
  eagle_log "INFO" "Stop: no eagle-summary found, using heuristic fallback"
124
132
 
125
- # Extract first user prompt as "request"
126
133
  request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null | head -1 | cut -c1-500)
127
134
 
128
- # Extract files from Read/Write/Edit tool calls
129
135
  heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
130
136
  heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
131
137
 
@@ -138,6 +144,52 @@ if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ];
138
144
 
139
145
  completed="(auto-captured from tool usage)"
140
146
  fi
147
+
148
+ # ─── LLM enrichment: extract decisions/gotchas/key_files ──
149
+
150
+ if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
151
+ provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
152
+ if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
153
+ excerpt=$(echo "$text_content" | tail -c 2000)
154
+
155
+ enrich_prompt="Extract from this Claude Code session excerpt:
156
+ 1. DECISIONS: architectural or design choices made (with WHY). One per line.
157
+ 2. GOTCHAS: non-obvious pitfalls, bugs found, things that surprised. One per line.
158
+ 3. KEY_FILES: important files that were central to the work. One per line.
159
+
160
+ SESSION EXCERPT:
161
+ $excerpt
162
+
163
+ Output EXACTLY this format (omit sections with nothing to report):
164
+ DECISIONS:
165
+ - <decision> — why: <reason>
166
+ GOTCHAS:
167
+ - <gotcha>
168
+ KEY_FILES:
169
+ - <filepath>"
170
+
171
+ enrich_result=$(eagle_llm_call "$enrich_prompt" "Extract structured facts from development sessions. Be concise. Only include items with clear evidence." 512 2>/dev/null) || true
172
+
173
+ if [ -n "$enrich_result" ]; then
174
+ extract_section() {
175
+ local result="$1" header="$2"
176
+ echo "$result" | awk -v h="$header:" '
177
+ $0 == h || $0 ~ "^"h { found=1; next }
178
+ found && /^[A-Z_]+:/ { exit }
179
+ found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
180
+ END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
181
+ '
182
+ }
183
+ decisions=$(extract_section "$enrich_result" "DECISIONS")
184
+ gotchas=$(extract_section "$enrich_result" "GOTCHAS")
185
+ key_files=$(extract_section "$enrich_result" "KEY_FILES")
186
+ [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
187
+ fi
188
+ fi
189
+ fi
190
+
191
+ elif [ -z "$summary_block" ] && [ "${existing_count:-0}" -gt 0 ]; then
192
+ eagle_log "INFO" "Stop: skipping fallback — summary already exists for session=$session_id (count=$existing_count)"
141
193
  fi
142
194
 
143
195
  # ─── Redact secrets from all text fields before storage ────
package/lib/common.sh CHANGED
@@ -25,6 +25,18 @@ eagle_log() {
25
25
 
26
26
  eagle_project_from_cwd() {
27
27
  local cwd="${1:-$(pwd)}"
28
+ local resolved="$cwd"
29
+
30
+ # Resolve /private/tmp → /tmp on macOS
31
+ case "$resolved" in /private/tmp*) resolved="/tmp${resolved#/private/tmp}" ;; esac
32
+
33
+ # Skip ephemeral directories — return empty so hooks early-exit
34
+ case "$resolved" in
35
+ /tmp|/tmp/*|/var/tmp|/var/tmp/*) echo ""; return ;;
36
+ "$HOME/Downloads"|"$HOME/Downloads/"*) echo ""; return ;;
37
+ "$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
38
+ esac
39
+
28
40
  local git_root
29
41
  git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
30
42
  if [ -n "$git_root" ]; then
@@ -66,3 +66,34 @@ eagle_count_session_summaries() {
66
66
  local sid; sid=$(eagle_sql_escape "$1")
67
67
  eagle_db "SELECT COUNT(*) FROM summaries WHERE session_id = '$sid';"
68
68
  }
69
+
70
+ eagle_meta_get() {
71
+ local key; key=$(eagle_sql_escape "$1")
72
+ local project="${2:-}"
73
+ if [ -n "$project" ]; then
74
+ local p_esc; p_esc=$(eagle_sql_escape "$project")
75
+ eagle_db "SELECT value FROM eagle_meta WHERE key = '$key' AND project = '$p_esc' LIMIT 1;"
76
+ else
77
+ eagle_db "SELECT value FROM eagle_meta WHERE key = '$key' AND project IS NULL LIMIT 1;"
78
+ fi
79
+ }
80
+
81
+ eagle_meta_set() {
82
+ local key; key=$(eagle_sql_escape "$1")
83
+ local value; value=$(eagle_sql_escape "$2")
84
+ local project="${3:-}"
85
+ if [ -n "$project" ]; then
86
+ local p_esc; p_esc=$(eagle_sql_escape "$project")
87
+ eagle_db "INSERT INTO eagle_meta (key, project, value) VALUES ('$key', '$p_esc', '$value')
88
+ ON CONFLICT(key, project) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
89
+ else
90
+ eagle_db "INSERT INTO eagle_meta (key, project, value) VALUES ('$key', NULL, '$value')
91
+ ON CONFLICT(key, project) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
92
+ fi
93
+ }
94
+
95
+ eagle_count_sessions_since() {
96
+ local project; project=$(eagle_sql_escape "$1")
97
+ local since; since=$(eagle_sql_escape "$2")
98
+ eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$project' AND started_at > '$since';"
99
+ }
@@ -130,6 +130,17 @@ eagle_search_decisions_for_file() {
130
130
  LIMIT 1;"
131
131
  }
132
132
 
133
+ eagle_last_session_enriched() {
134
+ local project; project=$(eagle_sql_escape "$1")
135
+ eagle_db "SELECT CASE
136
+ WHEN (decisions IS NOT NULL AND decisions != '')
137
+ OR (gotchas IS NOT NULL AND gotchas != '')
138
+ OR (key_files IS NOT NULL AND key_files != '')
139
+ THEN 1 ELSE 0 END
140
+ FROM summaries WHERE project = '$project'
141
+ ORDER BY created_at DESC LIMIT 1;"
142
+ }
143
+
133
144
  eagle_search_stale_memories() {
134
145
  local project; project=$(eagle_sql_escape "$1")
135
146
  local fts_query; fts_query=$(eagle_sql_escape "$2")
package/lib/provider.sh CHANGED
@@ -151,8 +151,9 @@ model = "claude-haiku-4-5-20251001"
151
151
  model = "gpt-4o-mini"
152
152
 
153
153
  [curator]
154
- # How often the curator runs: "manual", "daily", "weekly"
154
+ # "auto" = triggers at session end after min_sessions; "manual" = CLI only
155
155
  schedule = "manual"
156
+ min_sessions = 5
156
157
 
157
158
  [redaction]
158
159
  # Additional secret patterns (regex) beyond built-in defaults
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Persistent memory for Claude Code — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -361,5 +361,6 @@ echo ""
361
361
  if [ "$DRY_RUN" -eq 1 ]; then
362
362
  eagle_footer "Dry run complete. Run without --dry-run to apply changes."
363
363
  else
364
+ eagle_meta_set "last_curated_at" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$project"
364
365
  eagle_footer "Curation complete for '$project'."
365
366
  fi
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Health Check
4
+ # Diagnoses how well the self-learning pipeline is working
5
+ # ═══════════════════════════════════════════════════════════
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ LIB_DIR="$SCRIPT_DIR/../lib"
10
+
11
+ . "$LIB_DIR/common.sh"
12
+ . "$SCRIPT_DIR/style.sh"
13
+ . "$LIB_DIR/db.sh"
14
+ . "$LIB_DIR/provider.sh"
15
+
16
+ eagle_header "Health Check"
17
+
18
+ project=""
19
+ JSON_OUT=0
20
+
21
+ while [ $# -gt 0 ]; do
22
+ case "$1" in
23
+ -p|--project) project="$2"; shift 2 ;;
24
+ -j|--json) JSON_OUT=1; shift ;;
25
+ *) shift ;;
26
+ esac
27
+ done
28
+
29
+ if [ -z "$project" ]; then
30
+ project=$(eagle_project_from_cwd "$(pwd)")
31
+ fi
32
+
33
+ if [ -z "$project" ]; then
34
+ eagle_err "Cannot determine project (ephemeral directory). Use -p <project>."
35
+ exit 1
36
+ fi
37
+
38
+ p_esc=$(eagle_sql_escape "$project")
39
+
40
+ eagle_info "Project: ${BOLD}$project${RESET}"
41
+ echo ""
42
+
43
+ score=0
44
+ max_score=0
45
+ issues=()
46
+
47
+ # ─── 1. Summary enrichment rate ──────────────────────────
48
+
49
+ max_score=$((max_score + 30))
50
+
51
+ total_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
52
+ enriched_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc' AND (decisions IS NOT NULL AND decisions != '' OR gotchas IS NOT NULL AND gotchas != '' OR key_files IS NOT NULL AND key_files != '');")
53
+
54
+ if [ "${total_summaries:-0}" -eq 0 ]; then
55
+ enrich_pct=0
56
+ else
57
+ enrich_pct=$((enriched_summaries * 100 / total_summaries))
58
+ fi
59
+
60
+ if [ "$enrich_pct" -ge 50 ]; then
61
+ eagle_ok "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%)"
62
+ score=$((score + 30))
63
+ elif [ "$enrich_pct" -ge 20 ]; then
64
+ eagle_warn "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — aim for 50%+"
65
+ score=$((score + 15))
66
+ issues+=("Low enrichment rate (${enrich_pct}%). Eagle-summary blocks aren't being emitted reliably.")
67
+ elif [ "${total_summaries:-0}" -gt 0 ]; then
68
+ eagle_fail "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — self-learning not working"
69
+ issues+=("Critical: ${enrich_pct}% enrichment. Decisions/gotchas/key_files are not being captured.")
70
+ else
71
+ eagle_dim " No summaries yet"
72
+ fi
73
+
74
+ # ─── 2. Feature discovery ────────────────────────────────
75
+
76
+ max_score=$((max_score + 20))
77
+
78
+ feature_count=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
79
+ feature_file_count=$(eagle_db "SELECT COUNT(*) FROM feature_files ff JOIN features f ON ff.feature_id = f.id WHERE f.project = '$p_esc' AND f.status = 'active';")
80
+
81
+ if [ "${feature_count:-0}" -ge 3 ]; then
82
+ eagle_ok "Features tracked: ${feature_count} (${feature_file_count} files mapped)"
83
+ score=$((score + 20))
84
+ elif [ "${feature_count:-0}" -ge 1 ]; then
85
+ eagle_warn "Features tracked: ${feature_count} — curator needs more sessions"
86
+ score=$((score + 10))
87
+ issues+=("Only ${feature_count} features discovered. Run curator more often.")
88
+ else
89
+ eagle_fail "Features tracked: 0 — feature graph is empty"
90
+ issues+=("No features discovered. Run: eagle-mem curate")
91
+ fi
92
+
93
+ # ─── 3. Command intelligence ─────────────────────────────
94
+
95
+ max_score=$((max_score + 15))
96
+
97
+ rule_count=$(eagle_db "SELECT COUNT(*) FROM command_rules WHERE (project = '$p_esc' OR project IS NULL) AND enabled = 1;")
98
+ obs_with_metrics=$(eagle_db "SELECT COUNT(*) FROM observations WHERE project = '$p_esc' AND tool_name = 'Bash' AND output_bytes IS NOT NULL AND output_bytes > 0;")
99
+
100
+ if [ "${rule_count:-0}" -ge 2 ]; then
101
+ eagle_ok "Command rules: ${rule_count} active (${obs_with_metrics} observations with metrics)"
102
+ score=$((score + 15))
103
+ elif [ "${rule_count:-0}" -ge 1 ]; then
104
+ eagle_warn "Command rules: ${rule_count} — learning in progress"
105
+ score=$((score + 8))
106
+ elif [ "${obs_with_metrics:-0}" -gt 20 ]; then
107
+ eagle_fail "Command rules: 0 (but ${obs_with_metrics} observations available — run curator)"
108
+ issues+=("Command metrics collected but no rules generated yet.")
109
+ else
110
+ eagle_dim " Command metrics: ${obs_with_metrics} observations (need more data)"
111
+ score=$((score + 5))
112
+ fi
113
+
114
+ # ─── 4. Provider configured ──────────────────────────────
115
+
116
+ max_score=$((max_score + 15))
117
+
118
+ provider=$(eagle_config_get "provider" "type" "none")
119
+ if [ "$provider" != "none" ]; then
120
+ model=$(eagle_config_get "$provider" "model" "default")
121
+ eagle_ok "LLM provider: ${provider} (${model})"
122
+ score=$((score + 15))
123
+ else
124
+ eagle_fail "No LLM provider — curator and enrichment extraction disabled"
125
+ issues+=("Configure a provider: eagle-mem config init")
126
+ fi
127
+
128
+ # ─── 5. Project data quality ─────────────────────────────
129
+
130
+ max_score=$((max_score + 10))
131
+
132
+ tmp_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project IN ('tmp', 'private', '');")
133
+ total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions;")
134
+
135
+ if [ "${total_sessions:-0}" -eq 0 ]; then
136
+ noise_pct=0
137
+ else
138
+ noise_pct=$((tmp_sessions * 100 / total_sessions))
139
+ fi
140
+
141
+ if [ "$noise_pct" -le 5 ]; then
142
+ eagle_ok "Data quality: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
143
+ score=$((score + 10))
144
+ elif [ "$noise_pct" -le 20 ]; then
145
+ eagle_warn "Data quality: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
146
+ score=$((score + 5))
147
+ issues+=("${noise_pct}% of sessions from ephemeral dirs. Skiplist should prevent new ones.")
148
+ else
149
+ eagle_fail "Data quality: ${noise_pct}% noise — ${tmp_sessions}/${total_sessions} sessions from ephemeral dirs"
150
+ issues+=("Heavy ephemeral pollution. Update Eagle Mem to get skiplist protection.")
151
+ fi
152
+
153
+ # ─── 6. Curator activity ─────────────────────────────────
154
+
155
+ max_score=$((max_score + 10))
156
+
157
+ curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
158
+ last_curated=$(eagle_db "SELECT value FROM eagle_meta WHERE key = 'last_curated_at' AND (project = '$p_esc' OR project IS NULL) ORDER BY CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END LIMIT 1;" 2>/dev/null || echo "")
159
+
160
+ if [ -n "$last_curated" ]; then
161
+ eagle_ok "Curator: last run ${last_curated} (schedule: ${curator_schedule})"
162
+ score=$((score + 10))
163
+ elif [ "$curator_schedule" = "auto" ]; then
164
+ eagle_warn "Curator: auto-scheduled but hasn't run yet"
165
+ score=$((score + 5))
166
+ issues+=("Auto-curate is configured but hasn't run. It triggers at session end.")
167
+ else
168
+ eagle_fail "Curator: never run (schedule: ${curator_schedule})"
169
+ issues+=("Curator has never run. Try: eagle-mem curate --dry-run")
170
+ fi
171
+
172
+ # ─── Score ────────────────────────────────────────────────
173
+
174
+ echo ""
175
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
176
+
177
+ pct=$((score * 100 / max_score))
178
+ if [ "$pct" -ge 80 ]; then
179
+ color="$GREEN"
180
+ grade="Healthy"
181
+ elif [ "$pct" -ge 50 ]; then
182
+ color="$YELLOW"
183
+ grade="Needs attention"
184
+ else
185
+ color="$RED"
186
+ grade="Unhealthy"
187
+ fi
188
+
189
+ echo -e " ${BOLD}Score: ${color}${score}/${max_score} (${pct}%)${RESET} ${color}${grade}${RESET}"
190
+
191
+ if [ ${#issues[@]} -gt 0 ]; then
192
+ echo ""
193
+ echo -e " ${BOLD}Issues:${RESET}"
194
+ for issue in "${issues[@]}"; do
195
+ echo -e " ${YELLOW}!${RESET} $issue"
196
+ done
197
+ fi
198
+
199
+ eagle_footer "Health check complete."
200
+
201
+ if [ "$JSON_OUT" -eq 1 ]; then
202
+ jq -nc \
203
+ --arg project "$project" \
204
+ --argjson score "$score" \
205
+ --argjson max_score "$max_score" \
206
+ --argjson pct "$pct" \
207
+ --arg grade "$grade" \
208
+ --argjson total_summaries "${total_summaries:-0}" \
209
+ --argjson enriched_summaries "${enriched_summaries:-0}" \
210
+ --argjson features "${feature_count:-0}" \
211
+ --argjson command_rules "${rule_count:-0}" \
212
+ --arg provider "$provider" \
213
+ --argjson noise_pct "$noise_pct" \
214
+ '{project:$project, score:$score, max:$max_score, pct:$pct, grade:$grade,
215
+ enrichment:{total:$total_summaries, enriched:$enriched_summaries},
216
+ features:$features, command_rules:$command_rules,
217
+ provider:$provider, noise_pct:$noise_pct}'
218
+ fi
package/scripts/help.sh CHANGED
@@ -27,6 +27,7 @@ echo -e " ${CYAN}prune${RESET} Remove old observations and orphaned chu
27
27
  echo -e " ${CYAN}config${RESET} View or change LLM provider settings"
28
28
  echo -e " ${CYAN}curate${RESET} Run the self-learning curator (LLM-powered analysis)"
29
29
  echo -e " ${CYAN}feature${RESET} Manage feature graph (list/show/verify/add)"
30
+ echo -e " ${CYAN}health${RESET} Diagnose self-learning pipeline health"
30
31
  echo -e " ${CYAN}install${RESET} First-time setup: hooks, database, skills"
31
32
  echo -e " ${CYAN}update${RESET} Re-deploy hooks and run migrations"
32
33
  echo -e " ${CYAN}uninstall${RESET} Remove hooks and optionally delete data"