eagle-mem 3.0.1 → 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
+ );
@@ -2,7 +2,7 @@
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — PostToolUse hook
4
4
  # Fires after every tool use
5
- # Captures file read/write operations as lightweight observations
5
+ # Captures observations + dispatches to extracted responsibilities
6
6
  # ═══════════════════════════════════════════════════════════
7
7
  set +e
8
8
 
@@ -11,6 +11,7 @@ LIB_DIR="$SCRIPT_DIR/../lib"
11
11
 
12
12
  . "$LIB_DIR/common.sh"
13
13
  . "$LIB_DIR/db.sh"
14
+ . "$LIB_DIR/hooks-posttool.sh"
14
15
 
15
16
  input=$(eagle_read_stdin)
16
17
  [ -z "$input" ] && exit 0
@@ -30,11 +31,15 @@ esac
30
31
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
31
32
 
32
33
  project=$(eagle_project_from_cwd "$cwd")
34
+ [ -z "$project" ] && exit 0
33
35
 
34
36
  # Ensure session row exists before inserting observations (FK constraint).
35
37
  # PostToolUse can race SessionStart — the session row might not exist yet.
36
38
  eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
37
39
 
40
+ # ─── Extract observation data from tool call ──────────────
41
+
42
+ fp=""
38
43
  files_read="[]"
39
44
  files_modified="[]"
40
45
  tool_summary=""
@@ -63,14 +68,12 @@ case "$tool_name" in
63
68
  cmd=$(echo "$cmd" | eagle_redact)
64
69
  tool_summary="Bash: $cmd"
65
70
 
66
- # Output metrics
67
71
  tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
68
72
  if [ -n "$tool_output" ]; then
69
73
  output_bytes=${#tool_output}
70
74
  output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
71
75
  fi
72
76
 
73
- # Command category extraction
74
77
  first_word=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
75
78
  case "$first_word" in
76
79
  git|gh) command_category="git" ;;
@@ -95,151 +98,14 @@ case "$tool_name" in
95
98
  ;;
96
99
  esac
97
100
 
98
- # ─── Claude memory + plan mirror ─────────────────────────
99
- # Intercept writes to Claude Code's auto-memory and plan files
100
- case "$tool_name" in
101
- Write|Edit)
102
- if [ -n "$fp" ]; then
103
- # Reject path traversal: bash case `*` matches `/`, so
104
- # patterns like projects/*/memory/*.md would match paths
105
- # containing /../ segments. Block any path with `..` first.
106
- case "$fp" in
107
- *..*) ;; # path traversal — skip
108
- "$HOME/.claude/projects"/*/memory/*.md)
109
- mem_base=$(basename "$fp")
110
- if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
111
- eagle_capture_claude_memory "$fp" "$session_id" "$project"
112
- fi
113
- ;;
114
- "$HOME/.claude/plans/"*.md)
115
- if [ -f "$fp" ]; then
116
- eagle_capture_claude_plan "$fp" "$session_id" "$project"
117
- fi
118
- ;;
119
- esac
120
- fi
121
- ;;
122
- esac
123
-
124
- # ─── Claude task mirror ─────────────────────────────────
125
- # Intercept TaskCreate/TaskUpdate and capture the resulting JSON files
126
- case "$tool_name" in
127
- TaskCreate|TaskUpdate)
128
- if eagle_validate_session_id "$session_id"; then
129
- task_dir="$HOME/.claude/tasks/$session_id"
130
- if [ -d "$task_dir" ]; then
131
- task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
132
- if [ -z "$task_id" ]; then
133
- newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
134
- [ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
135
- elif eagle_validate_session_id "$task_id"; then
136
- task_json="$task_dir/$task_id.json"
137
- [ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
138
- fi
139
- fi
140
- fi
141
- ;;
142
- esac
101
+ # ─── Dispatch to extracted responsibilities ───────────────
143
102
 
144
- # ─── Stale memory hint ──────────────────────────────────
145
- # After editing a project file, FTS5-search memories for the filename.
146
- # If a memory mentions this file, remind Claude to check for staleness.
147
- case "$tool_name" in
148
- Write|Edit)
149
- if [ -n "$fp" ]; then
150
- fname=$(basename "$fp")
151
- fname_stem="${fname%.*}"
152
- case "$fp" in
153
- "$HOME/.claude/"*) ;; # skip Claude config files
154
- *)
155
- if [ ${#fname_stem} -ge 3 ]; then
156
- fts_query=$(eagle_fts_sanitize "$fname_stem")
157
- if [ -n "$fts_query" ]; then
158
- fts_esc=$(eagle_sql_escape "$fts_query")
159
- p_esc=$(eagle_sql_escape "$project")
160
- stale_hit=$(eagle_db "SELECT m.memory_name
161
- FROM claude_memories m
162
- JOIN claude_memories_fts f ON f.rowid = m.id
163
- WHERE claude_memories_fts MATCH '$fts_esc'
164
- AND m.project = '$p_esc'
165
- LIMIT 1;")
166
- if [ -n "$stale_hit" ]; then
167
- stale_msg="Eagle Mem: Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory."
168
- jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
169
- fi
170
- fi
171
- fi
172
- ;;
173
- esac
174
- fi
175
- ;;
176
- esac
103
+ eagle_posttool_mirror_writes "$tool_name" "$fp" "$session_id" "$project"
104
+ eagle_posttool_mirror_tasks "$tool_name" "$session_id" "$project" "$input"
105
+ eagle_posttool_stale_hint "$tool_name" "$fp" "$project"
106
+ eagle_posttool_decision_surface "$tool_name" "$fp" "$project"
177
107
 
178
- # ─── Decision + feature surfacing on Read ──────────────────
179
- # When Claude reads a file, surface past decisions and feature pipeline context.
180
- case "$tool_name" in
181
- Read)
182
- if [ -n "$fp" ]; then
183
- fname=$(basename "$fp")
184
- fname_stem="${fname%.*}"
185
- read_context=""
186
- case "$fp" in
187
- "$HOME/.claude/"*) ;; # skip Claude config files
188
- *)
189
- p_esc=$(eagle_sql_escape "$project")
190
-
191
- # Decision history from summaries
192
- if [ ${#fname_stem} -ge 3 ]; then
193
- fts_query=$(eagle_fts_sanitize "$fname_stem")
194
- if [ -n "$fts_query" ]; then
195
- fts_esc=$(eagle_sql_escape "$fts_query")
196
- decision_hit=$(eagle_db "SELECT s.decisions
197
- FROM summaries s
198
- JOIN summaries_fts f ON f.rowid = s.id
199
- WHERE summaries_fts MATCH '$fts_esc'
200
- AND s.project = '$p_esc'
201
- AND s.decisions IS NOT NULL
202
- AND s.decisions != ''
203
- ORDER BY s.created_at DESC
204
- LIMIT 1;")
205
- if [ -n "$decision_hit" ]; then
206
- read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
207
- fi
208
- fi
209
- fi
210
-
211
- # Feature pipeline context
212
- feature_hit=$(eagle_find_features_for_file "$project" "$fp")
213
- if [ -n "$feature_hit" ]; then
214
- while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
215
- [ -z "$feat_name" ] && continue
216
- read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
217
- [ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
218
- read_context+="."
219
- if [ -n "$feat_verified" ]; then
220
- read_context+=" Last verified: ${feat_verified}."
221
- fi
222
- if [ -n "$feat_deps" ]; then
223
- read_context+=" Dependencies: ${feat_deps}."
224
- fi
225
- if [ -n "$feat_other_files" ]; then
226
- read_context+=" Other files in pipeline: ${feat_other_files}."
227
- fi
228
- if [ -n "$feat_smoke" ]; then
229
- read_context+=" Smoke tests: ${feat_smoke}."
230
- fi
231
- read_context+=" Changes require re-testing after deploy. "
232
- done <<< "$feature_hit"
233
- fi
234
-
235
- if [ -n "$read_context" ]; then
236
- jq -nc --arg ctx "$read_context" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
237
- fi
238
- ;;
239
- esac
240
- fi
241
- ;;
242
- esac
108
+ # ─── Record observation ──────────────────────────────────
243
109
 
244
110
  if ! eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"; then
245
111
  eagle_log "ERROR" "PostToolUse: observation insert failed for session=$session_id tool=$tool_name"
@@ -27,7 +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
- p_esc=$(eagle_sql_escape "$project")
30
+ [ -z "$project" ] && exit 0
31
31
 
32
32
  context=""
33
33
 
@@ -35,7 +35,7 @@ context=""
35
35
 
36
36
  case "$cmd" in
37
37
  *"git push"*|*"gh pr create"*)
38
- has_features=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
38
+ has_features=$(eagle_count_active_features "$project")
39
39
  if [ "${has_features:-0}" -gt 0 ]; then
40
40
  changed_files=""
41
41
  if [ -n "$cwd" ] && [ -d "$cwd" ]; then
@@ -49,19 +49,8 @@ case "$cmd" in
49
49
  [ -z "$changed_file" ] && continue
50
50
  fname=$(basename "$changed_file")
51
51
  fname_esc=$(eagle_sql_escape "$fname")
52
- fname_like=$(eagle_like_escape "$fname_esc")
53
-
54
- feature_hits=$(eagle_db "SELECT DISTINCT f.name,
55
- (SELECT GROUP_CONCAT(fst.command, '; ')
56
- FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke,
57
- (SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
58
- FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
59
- f.last_verified_at
60
- FROM features f
61
- JOIN feature_files ff ON ff.feature_id = f.id
62
- WHERE f.project = '$p_esc'
63
- AND f.status = 'active'
64
- AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\');")
52
+
53
+ feature_hits=$(eagle_find_feature_for_push "$project" "$fname_esc")
65
54
 
66
55
  while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
67
56
  [ -z "$feat_name" ] && continue
@@ -93,16 +82,8 @@ esac
93
82
 
94
83
  # Extract the base command for rule matching
95
84
  base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
96
- cmd_esc=$(eagle_sql_escape "$base_cmd")
97
-
98
- rule=$(eagle_db "SELECT strategy, max_lines, reason
99
- FROM command_rules
100
- WHERE enabled = 1
101
- AND (project = '$p_esc' OR project IS NULL)
102
- AND ('$cmd_esc' LIKE pattern OR '$cmd_esc' = pattern)
103
- ORDER BY
104
- CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END
105
- LIMIT 1;")
85
+
86
+ rule=$(eagle_get_command_rule "$project" "$base_cmd")
106
87
 
107
88
  if [ -n "$rule" ]; then
108
89
  IFS='|' read -r strategy max_lines reason <<< "$rule"
@@ -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,11 +23,12 @@ 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
27
30
  if eagle_validate_session_id "$session_id"; then
28
- task_dir="$HOME/.claude/tasks/$session_id"
31
+ task_dir="$EAGLE_CLAUDE_TASKS_DIR/$session_id"
29
32
  if [ -d "$task_dir" ]; then
30
33
  for task_file in "$task_dir"/*.json; do
31
34
  [ ! -f "$task_file" ] && continue
@@ -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"
@@ -33,10 +36,7 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
33
36
  # ─── Sweep stuck sessions (no activity for 7 days) ─────────
34
37
  # Uses last_activity_at (updated by trigger on every observation insert)
35
38
  # so long-lived sessions with regular compactions aren't falsely abandoned
36
- eagle_db "UPDATE sessions SET status = 'abandoned'
37
- WHERE status = 'active'
38
- AND id != '$(eagle_sql_escape "$session_id")'
39
- AND COALESCE(last_activity_at, started_at) < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days');"
39
+ eagle_abandon_stale_sessions "$session_id"
40
40
 
41
41
  # ─── Version check (non-blocking) ────────────────────────────
42
42
 
@@ -68,30 +68,27 @@ fi
68
68
 
69
69
  # ─── Gather stats ───────────────────────────────────────────
70
70
 
71
- p_esc=$(eagle_sql_escape "$project")
72
-
73
- stat_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p_esc';")
74
- stat_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
75
- stat_memories=$(eagle_db "SELECT COUNT(*) FROM claude_memories WHERE project = '$p_esc';")
76
- stat_tasks_pending=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'pending';")
77
- stat_tasks_progress=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'in_progress';")
78
- stat_tasks_done=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'completed';")
79
- stat_chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$p_esc';")
80
- stat_observations=$(eagle_db "SELECT COUNT(*) FROM observations WHERE session_id IN (SELECT id FROM sessions WHERE project = '$p_esc');")
81
- stat_plans=$(eagle_db "SELECT COUNT(*) FROM claude_plans WHERE project = '$p_esc';")
82
- stat_last_active=$(eagle_db "SELECT COALESCE(MAX(date(COALESCE(last_activity_at, started_at))), 'never') FROM sessions WHERE project = '$p_esc';")
83
- stat_last_summary=$(eagle_db "SELECT request FROM summaries WHERE project = '$p_esc' ORDER BY created_at DESC LIMIT 1;")
84
-
85
- # Trim to defaults
86
- stat_sessions="${stat_sessions:-0}"
87
- stat_summaries="${stat_summaries:-0}"
88
- stat_memories="${stat_memories:-0}"
89
- stat_tasks_pending="${stat_tasks_pending:-0}"
90
- stat_tasks_progress="${stat_tasks_progress:-0}"
91
- stat_tasks_done="${stat_tasks_done:-0}"
92
- stat_chunks="${stat_chunks:-0}"
93
- stat_observations="${stat_observations:-0}"
94
- stat_plans="${stat_plans:-0}"
71
+ stat_sessions=0; stat_summaries=0; stat_with_summaries=0; stat_memories=0
72
+ stat_tasks_pending=0; stat_tasks_progress=0; stat_tasks_done=0
73
+ stat_chunks=0; stat_observations=0; stat_plans=0
74
+ stat_last_active="never"; stat_last_summary=""
75
+
76
+ while IFS='|' read -r key val; do
77
+ case "$key" in
78
+ sessions) stat_sessions="$val" ;;
79
+ summaries) stat_summaries="$val" ;;
80
+ with_summaries) stat_with_summaries="$val" ;;
81
+ memories) stat_memories="$val" ;;
82
+ plans) stat_plans="$val" ;;
83
+ tasks_pending) stat_tasks_pending="$val" ;;
84
+ tasks_progress) stat_tasks_progress="$val" ;;
85
+ tasks_done) stat_tasks_done="$val" ;;
86
+ chunks) stat_chunks="$val" ;;
87
+ observations) stat_observations="$val" ;;
88
+ last_active) stat_last_active="$val" ;;
89
+ last_summary) stat_last_summary="$val" ;;
90
+ esac
91
+ done <<< "$(eagle_get_project_stats "$project")"
95
92
 
96
93
  # Build task summary line
97
94
  task_parts=""
@@ -117,7 +114,7 @@ eagle_banner="======================================
117
114
  Eagle Mem Loaded
118
115
  ======================================
119
116
  Project | $project
120
- Sessions | $stat_sessions total ($stat_summaries with summaries)
117
+ Sessions | $stat_sessions total ($stat_with_summaries with summaries)
121
118
  Memories | $stat_memories stored
122
119
  Plans | $stat_plans saved
123
120
  Tasks | $task_parts
@@ -140,6 +137,15 @@ if [ -n "$update_notice" ]; then
140
137
  "
141
138
  fi
142
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
+
143
149
  # Project overview
144
150
  overview=$(eagle_get_overview "$project")
145
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.
119
- existing_count=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE session_id = '$(eagle_sql_escape "$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
121
+ existing_count=0
122
+ if [ -z "$summary_block" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
123
+ existing_count=$(eagle_count_session_summaries "$session_id")
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
@@ -9,6 +9,9 @@ EAGLE_MEM_DB="$EAGLE_MEM_DIR/memory.db"
9
9
  EAGLE_MEM_LOG="$EAGLE_MEM_DIR/eagle-mem.log"
10
10
  EAGLE_SETTINGS="${EAGLE_SETTINGS:-$HOME/.claude/settings.json}"
11
11
  EAGLE_SKILLS_DIR="$HOME/.claude/skills"
12
+ EAGLE_CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"
13
+ EAGLE_CLAUDE_PLANS_DIR="$HOME/.claude/plans"
14
+ EAGLE_CLAUDE_TASKS_DIR="$HOME/.claude/tasks"
12
15
 
13
16
  eagle_log() {
14
17
  local level="$1"
@@ -22,6 +25,18 @@ eagle_log() {
22
25
 
23
26
  eagle_project_from_cwd() {
24
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
+
25
40
  local git_root
26
41
  git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
27
42
  if [ -n "$git_root" ]; then