eagle-mem 3.1.0 → 3.2.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/db/017_eagle_meta_not_null.sql +15 -0
- package/db/018_command_rules_not_null.sql +23 -0
- package/hooks/session-end.sh +1 -22
- package/hooks/session-start.sh +57 -85
- package/hooks/stop.sh +96 -112
- package/lib/common.sh +9 -1
- package/lib/db-observations.sh +2 -2
- package/lib/db-sessions.sh +5 -16
- package/package.json +1 -1
- package/scripts/health.sh +88 -68
- package/scripts/install.sh +1 -1
- package/scripts/update.sh +3 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Fix: UNIQUE(key, project) fails with NULL project (each NULL is unique in SQL).
|
|
2
|
+
-- Change project to NOT NULL DEFAULT '' so ON CONFLICT works for global keys.
|
|
3
|
+
CREATE TABLE IF NOT EXISTS eagle_meta_new (
|
|
4
|
+
key TEXT NOT NULL,
|
|
5
|
+
project TEXT NOT NULL DEFAULT '',
|
|
6
|
+
value TEXT NOT NULL,
|
|
7
|
+
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
8
|
+
UNIQUE(key, project)
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
INSERT OR REPLACE INTO eagle_meta_new (key, project, value, updated_at)
|
|
12
|
+
SELECT key, COALESCE(project, ''), value, updated_at FROM eagle_meta;
|
|
13
|
+
|
|
14
|
+
DROP TABLE eagle_meta;
|
|
15
|
+
ALTER TABLE eagle_meta_new RENAME TO eagle_meta;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Fix: command_rules.project has same NULL uniqueness flaw as eagle_meta had.
|
|
2
|
+
-- Change to NOT NULL DEFAULT '' for consistent UNIQUE constraint behavior.
|
|
3
|
+
CREATE TABLE IF NOT EXISTS command_rules_new (
|
|
4
|
+
id INTEGER PRIMARY KEY,
|
|
5
|
+
project TEXT NOT NULL DEFAULT '',
|
|
6
|
+
pattern TEXT NOT NULL,
|
|
7
|
+
strategy TEXT NOT NULL DEFAULT 'summary' CHECK (strategy IN ('summary', 'truncate')),
|
|
8
|
+
max_lines INTEGER,
|
|
9
|
+
reason TEXT,
|
|
10
|
+
times_seen INTEGER DEFAULT 0,
|
|
11
|
+
avg_output_bytes INTEGER DEFAULT 0,
|
|
12
|
+
enabled INTEGER DEFAULT 1,
|
|
13
|
+
created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
14
|
+
updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
15
|
+
UNIQUE(project, pattern)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
INSERT OR REPLACE INTO command_rules_new (id, project, pattern, strategy, max_lines, reason, times_seen, avg_output_bytes, enabled, created_at, updated_at)
|
|
19
|
+
SELECT id, COALESCE(project, ''), pattern, strategy, max_lines, reason, times_seen, avg_output_bytes, enabled, created_at, updated_at FROM command_rules;
|
|
20
|
+
|
|
21
|
+
DROP TABLE command_rules;
|
|
22
|
+
ALTER TABLE command_rules_new RENAME TO command_rules;
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_command_rules_pattern ON command_rules(pattern);
|
package/hooks/session-end.sh
CHANGED
|
@@ -2,17 +2,15 @@
|
|
|
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
|
|
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"
|
|
12
11
|
|
|
13
12
|
. "$LIB_DIR/common.sh"
|
|
14
13
|
. "$LIB_DIR/db.sh"
|
|
15
|
-
. "$LIB_DIR/provider.sh"
|
|
16
14
|
|
|
17
15
|
input=$(eagle_read_stdin)
|
|
18
16
|
[ -z "$input" ] && exit 0
|
|
@@ -44,23 +42,4 @@ eagle_log "INFO" "SessionEnd: session=$session_id marked completed"
|
|
|
44
42
|
# Prune observations older than 90 days (keeps DB size bounded)
|
|
45
43
|
eagle_prune_observations 90 "$project"
|
|
46
44
|
|
|
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
|
-
|
|
66
45
|
exit 0
|
package/hooks/session-start.sh
CHANGED
|
@@ -8,9 +8,11 @@ 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
|
eagle_ensure_db
|
|
16
18
|
|
|
@@ -29,6 +31,8 @@ project=$(eagle_project_from_cwd "$cwd")
|
|
|
29
31
|
# Skip ephemeral directories (tmp, Downloads, etc.) — no tracking
|
|
30
32
|
[ -z "$project" ] && exit 0
|
|
31
33
|
|
|
34
|
+
p_esc=$(eagle_sql_escape "$project")
|
|
35
|
+
|
|
32
36
|
eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
|
|
33
37
|
|
|
34
38
|
eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
@@ -38,6 +42,25 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
|
38
42
|
# so long-lived sessions with regular compactions aren't falsely abandoned
|
|
39
43
|
eagle_abandon_stale_sessions "$session_id"
|
|
40
44
|
|
|
45
|
+
# ─── Auto-curate trigger (background, non-blocking) ──────────
|
|
46
|
+
# Moved here from SessionEnd because SessionEnd rarely fires in long-lived sessions.
|
|
47
|
+
# SessionStart fires on every new session, resume, and compaction — reliable trigger.
|
|
48
|
+
curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
|
|
49
|
+
if [ "$curator_schedule" = "auto" ]; then
|
|
50
|
+
_curator_provider=$(eagle_config_get "provider" "type" "none")
|
|
51
|
+
if [ "$_curator_provider" != "none" ]; then
|
|
52
|
+
_min_sessions=$(eagle_config_get "curator" "min_sessions" "5")
|
|
53
|
+
_min_sessions=$(eagle_sql_int "$_min_sessions")
|
|
54
|
+
_last_curated=$(eagle_meta_get "last_curated_at" "$project")
|
|
55
|
+
_since="${_last_curated:-1970-01-01T00:00:00Z}"
|
|
56
|
+
_sessions_since=$(eagle_count_sessions_since "$project" "$_since")
|
|
57
|
+
if [ "${_sessions_since:-0}" -ge "$_min_sessions" ]; then
|
|
58
|
+
eagle_log "INFO" "SessionStart: auto-curate triggered (${_sessions_since} sessions since last curate)"
|
|
59
|
+
nohup bash "$SCRIPTS_DIR/curate.sh" -p "$project" >> "$EAGLE_MEM_LOG" 2>&1 &
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
41
64
|
# ─── Version check (non-blocking) ────────────────────────────
|
|
42
65
|
|
|
43
66
|
update_notice=""
|
|
@@ -125,38 +148,25 @@ eagle_banner="======================================
|
|
|
125
148
|
======================================"
|
|
126
149
|
|
|
127
150
|
context="$eagle_banner
|
|
128
|
-
|
|
129
|
-
=== EAGLE MEM — Active (trigger: $source_type) ===
|
|
130
|
-
Eagle Mem (https://github.com/eagleisbatman/eagle-mem) is providing persistent memory for this session. It tracks summaries, observations, tasks, and code context across sessions via SQLite + FTS5. Mention Eagle Mem by name when referencing recalled context.
|
|
131
|
-
|
|
132
151
|
"
|
|
133
152
|
|
|
134
153
|
if [ -n "$update_notice" ]; then
|
|
135
|
-
context+="
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
fi
|
|
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
|
-
|
|
154
|
+
context+="
|
|
155
|
+
=== $update_notice ===
|
|
146
156
|
"
|
|
147
157
|
fi
|
|
148
158
|
|
|
149
159
|
# Project overview
|
|
150
160
|
overview=$(eagle_get_overview "$project")
|
|
151
161
|
if [ -n "$overview" ]; then
|
|
152
|
-
context+="
|
|
162
|
+
context+="
|
|
163
|
+
=== Project Overview ===
|
|
153
164
|
$overview
|
|
154
|
-
|
|
155
165
|
"
|
|
156
166
|
else
|
|
157
|
-
context+="
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
context+="
|
|
168
|
+
=== Action Required ===
|
|
169
|
+
No overview exists for '$project'. Run /eagle-mem-overview to build one.
|
|
160
170
|
"
|
|
161
171
|
fi
|
|
162
172
|
|
|
@@ -164,8 +174,8 @@ fi
|
|
|
164
174
|
recent=$(eagle_get_recent_summaries "$project" 5)
|
|
165
175
|
|
|
166
176
|
if [ -n "$recent" ]; then
|
|
167
|
-
context+="
|
|
168
|
-
Recent
|
|
177
|
+
context+="
|
|
178
|
+
=== Recent Sessions ===
|
|
169
179
|
"
|
|
170
180
|
while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
|
|
171
181
|
[ -z "$request" ] && [ -z "$completed" ] && continue
|
|
@@ -200,8 +210,7 @@ memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, up
|
|
|
200
210
|
LIMIT 5;")
|
|
201
211
|
if [ -n "$memories" ]; then
|
|
202
212
|
context+="
|
|
203
|
-
===
|
|
204
|
-
Recent memories for '$project':
|
|
213
|
+
=== Memories ===
|
|
205
214
|
"
|
|
206
215
|
while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
|
|
207
216
|
[ -z "$mname" ] && continue
|
|
@@ -225,8 +234,7 @@ fi
|
|
|
225
234
|
plans=$(eagle_list_claude_plans "$project" 3)
|
|
226
235
|
if [ -n "$plans" ]; then
|
|
227
236
|
context+="
|
|
228
|
-
===
|
|
229
|
-
Recent plans for '$project':
|
|
237
|
+
=== Plans ===
|
|
230
238
|
"
|
|
231
239
|
while IFS='|' read -r ptitle _pproj _fpath _updated; do
|
|
232
240
|
[ -z "$ptitle" ] && continue
|
|
@@ -247,8 +255,7 @@ synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
|
|
|
247
255
|
LIMIT 10;")
|
|
248
256
|
if [ -n "$synced_tasks" ]; then
|
|
249
257
|
context+="
|
|
250
|
-
===
|
|
251
|
-
Tasks for '$project':
|
|
258
|
+
=== Tasks ===
|
|
252
259
|
"
|
|
253
260
|
while IFS='|' read -r tsubject tstatus tblocked; do
|
|
254
261
|
[ -z "$tsubject" ] && continue
|
|
@@ -261,72 +268,37 @@ Tasks for '$project':
|
|
|
261
268
|
done <<< "$synced_tasks"
|
|
262
269
|
fi
|
|
263
270
|
|
|
264
|
-
#
|
|
265
|
-
context+="
|
|
266
|
-
=== EAGLE MEM INSTRUCTIONS ===
|
|
267
|
-
You have persistent memory powered by Eagle Mem. When you recall context from a previous session or use injected memory, attribute it: \"From Eagle Mem:\" or \"Eagle Mem recalls:\". This helps the user understand where the context came from.
|
|
268
|
-
|
|
269
|
-
IMPORTANT: At the start of your VERY NEXT response (this fires on session start, /clear, AND context compaction — always show this block, even if you think you showed it before, because prior context may have been compressed away). Show the user what Eagle Mem loaded by reproducing this exact banner:
|
|
271
|
+
# ─── Instructions (full on startup, minimal on compact) ──
|
|
270
272
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
MEMORY FRESHNESS: The memories above include age indicators. If you make a change (edit a file, update a config, change a pattern) that contradicts what a loaded memory says, you MUST update that memory file immediately. Read the memory file, edit it to reflect the new reality, and the PostToolUse hook will sync the update to Eagle Mem. Stale memories mislead future sessions — keeping them current is as important as writing good code.
|
|
282
|
-
|
|
283
|
-
=== EAGLE MEM — SESSION SUMMARY (MANDATORY) ===
|
|
284
|
-
You MUST emit an <eagle-summary> block before your FINAL response in this session. This is how Eagle Mem captures what happened — without it, the next session starts blind and wastes tokens rediscovering context.
|
|
285
|
-
|
|
286
|
-
FORMAT — emit this block exactly. Every field is REQUIRED. Do not skip fields, do not leave them empty, do not write \"N/A\".
|
|
287
|
-
|
|
288
|
-
<eagle-summary>
|
|
289
|
-
request: [One sentence: what did the user ask for?]
|
|
290
|
-
investigated: [Comma-separated file paths you read or explored]
|
|
291
|
-
learned: [Non-obvious technical discoveries — things a future session could not guess from reading the code]
|
|
292
|
-
completed: [What was accomplished — be specific about what shipped, not what was \"worked on\"]
|
|
293
|
-
next_steps: [Concrete actions for the next session, not vague aspirations]
|
|
294
|
-
decisions:
|
|
295
|
-
- [Choice made] Why: [the reason — what constraint or tradeoff drove this choice]
|
|
296
|
-
- [Choice made] Why: [reason]
|
|
297
|
-
gotchas:
|
|
298
|
-
- [What failed, surprised, or does not work the obvious way. Be specific — \"X does not work because Y\" not just \"X was tricky\"]
|
|
299
|
-
key_files:
|
|
300
|
-
- [path/to/file.ext] — [one-line role: what this file does in the context of this work]
|
|
301
|
-
- [path/to/other.ext] — [role]
|
|
302
|
-
files_read: [file1, file2, ...]
|
|
303
|
-
files_modified: [file1, file2, ...]
|
|
304
|
-
</eagle-summary>
|
|
273
|
+
if [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
|
|
274
|
+
context+="
|
|
275
|
+
=== Eagle Mem (compact reload) ===
|
|
276
|
+
Persistent memory active. Attribute recalled context to Eagle Mem. Do not revert past decisions surfaced by PostToolUse without asking the user. Emit <eagle-summary> before your final response.
|
|
277
|
+
"
|
|
278
|
+
else
|
|
279
|
+
context+="
|
|
280
|
+
=== Eagle Mem ===
|
|
281
|
+
Persistent memory active for '$project'. Attribute recalled context: \"Eagle Mem recalls:\" When PostToolUse surfaces past decisions about a file, do not revert without explicit user request. Never include raw secrets in eagle-summary fields. If you change something that contradicts a loaded memory, update that memory file.
|
|
305
282
|
|
|
306
|
-
|
|
283
|
+
Emit an <eagle-summary> block before your FINAL response:
|
|
307
284
|
|
|
308
285
|
<eagle-summary>
|
|
309
|
-
request:
|
|
310
|
-
investigated:
|
|
311
|
-
learned:
|
|
312
|
-
completed:
|
|
313
|
-
next_steps:
|
|
286
|
+
request: [what the user asked for]
|
|
287
|
+
investigated: [file paths read or explored]
|
|
288
|
+
learned: [non-obvious discoveries a future session couldn't guess from code]
|
|
289
|
+
completed: [what shipped — be specific]
|
|
290
|
+
next_steps: [concrete actions for next session]
|
|
314
291
|
decisions:
|
|
315
|
-
-
|
|
316
|
-
- Put auth middleware at router level, not app level. Why: healthcheck and public routes must remain unauthenticated; per-router mounting is explicit about what is protected.
|
|
292
|
+
- [choice] Why: [reason]
|
|
317
293
|
gotchas:
|
|
318
|
-
-
|
|
319
|
-
- Setting token expiry below 5 min causes refresh storms under load — the refresh endpoint itself requires a valid (but expired) token, creating a chicken-and-egg problem.
|
|
294
|
+
- [what failed or surprised — \"X doesn't work because Y\"]
|
|
320
295
|
key_files:
|
|
321
|
-
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
files_read: [src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts]
|
|
325
|
-
files_modified: [src/middleware/auth.ts, src/config/env.ts, src/routes/users.ts, package.json]
|
|
296
|
+
- [path] — [role in this work]
|
|
297
|
+
files_read: [file1, file2]
|
|
298
|
+
files_modified: [file1, file2]
|
|
326
299
|
</eagle-summary>
|
|
327
|
-
|
|
328
|
-
WHY THIS MATTERS: Eagle Mem re-injects this summary at the start of future sessions. The 'decisions' field prevents re-debating settled choices. The 'gotchas' field prevents repeating the same mistakes. The 'key_files' field tells the next session exactly where to start reading instead of exploring blindly. Write these fields as if you are briefing a colleague who will pick up your work tomorrow — because that is exactly what happens.
|
|
329
300
|
"
|
|
301
|
+
fi
|
|
330
302
|
|
|
331
303
|
# Output context (plain text stdout = additionalContext for SessionStart)
|
|
332
304
|
if [ -n "$context" ]; then
|
package/hooks/stop.sh
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# ═══════════════════════════════════════════════════════════
|
|
3
3
|
# Eagle Mem — Stop hook
|
|
4
4
|
# Fires when Claude's turn ends
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Primary: extracts summary from transcript heuristically
|
|
6
|
+
# Bonus: eagle-summary block overrides where present
|
|
7
|
+
# LLM enrichment fills in decisions/gotchas/key_files
|
|
7
8
|
# ═══════════════════════════════════════════════════════════
|
|
8
9
|
set +e
|
|
9
10
|
|
|
@@ -37,122 +38,109 @@ eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcr
|
|
|
37
38
|
# Ensure session exists (may not if SessionStart didn't fire)
|
|
38
39
|
eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
|
|
39
40
|
|
|
40
|
-
# ───
|
|
41
|
+
# ─── Guard: skip if summary already exists for this session ──
|
|
42
|
+
# Stop fires every assistant turn. Only process once per session.
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
if [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
existing_count=$(eagle_count_session_summaries "$session_id")
|
|
45
|
+
if [ "${existing_count:-0}" -gt 0 ]; then
|
|
46
|
+
eagle_log "INFO" "Stop: summary already exists for session=$session_id — skipping"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
[ -z "$transcript_path" ] || [ ! -f "$transcript_path" ] && exit 0
|
|
51
|
+
|
|
52
|
+
# ─── Primary: heuristic extraction from transcript ───────────
|
|
53
|
+
|
|
54
|
+
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)
|
|
55
|
+
|
|
56
|
+
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)
|
|
57
|
+
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)
|
|
58
|
+
|
|
59
|
+
files_read="[]"
|
|
60
|
+
files_modified="[]"
|
|
61
|
+
if [ -n "$heuristic_reads" ]; then
|
|
62
|
+
files_read=$(echo "$heuristic_reads" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
63
|
+
fi
|
|
64
|
+
if [ -n "$heuristic_writes" ]; then
|
|
65
|
+
files_modified=$(echo "$heuristic_writes" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
60
66
|
fi
|
|
61
67
|
|
|
62
|
-
# ─── Extract fields from summary block ─────────────────────
|
|
63
|
-
|
|
64
|
-
parse_field() {
|
|
65
|
-
local block="$1"
|
|
66
|
-
local field="$2"
|
|
67
|
-
echo "$block" | awk -v f="$field" '
|
|
68
|
-
BEGIN { IGNORECASE=1; found=0 }
|
|
69
|
-
$0 ~ "^"f":" {
|
|
70
|
-
sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
|
|
71
|
-
}
|
|
72
|
-
found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
|
|
73
|
-
found { val = val " " $0 }
|
|
74
|
-
END { if (found) print val }
|
|
75
|
-
'
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
request=""
|
|
79
68
|
investigated=""
|
|
80
69
|
learned=""
|
|
81
|
-
completed=""
|
|
70
|
+
completed="(auto-captured)"
|
|
82
71
|
next_steps=""
|
|
83
|
-
files_read="[]"
|
|
84
|
-
files_modified="[]"
|
|
85
72
|
notes=""
|
|
86
73
|
decisions=""
|
|
87
74
|
gotchas=""
|
|
88
75
|
key_files=""
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
request=$(parse_field "$summary_block" "request")
|
|
92
|
-
investigated=$(parse_field "$summary_block" "investigated")
|
|
93
|
-
learned=$(parse_field "$summary_block" "learned")
|
|
94
|
-
completed=$(parse_field "$summary_block" "completed")
|
|
95
|
-
next_steps=$(parse_field "$summary_block" "next_steps")
|
|
96
|
-
decisions=$(parse_field "$summary_block" "decisions")
|
|
97
|
-
gotchas=$(parse_field "$summary_block" "gotchas")
|
|
98
|
-
key_files=$(parse_field "$summary_block" "key_files")
|
|
77
|
+
eagle_log "INFO" "Stop: heuristic extraction complete"
|
|
99
78
|
|
|
100
|
-
|
|
101
|
-
|
|
79
|
+
# ─── Bonus: eagle-summary block overrides where present ──────
|
|
80
|
+
|
|
81
|
+
text_content=$(jq -rs '
|
|
82
|
+
[.[] | select(.type == "assistant")] | last |
|
|
83
|
+
if . then
|
|
84
|
+
[.message.content[]? | select(.type == "text") | .text] | join("\n")
|
|
85
|
+
else "" end
|
|
86
|
+
' "$transcript_path" 2>/dev/null)
|
|
87
|
+
|
|
88
|
+
# Strip <private>...</private> blocks
|
|
89
|
+
text_content=$(echo "$text_content" | sed -E '/<[Pp][Rr][Ii][Vv][Aa][Tt][Ee][^>]*>/,/<\/[Pp][Rr][Ii][Vv][Aa][Tt][Ee][[:space:]]*>/d')
|
|
90
|
+
|
|
91
|
+
summary_block=""
|
|
92
|
+
if [ -n "$text_content" ] && echo "$text_content" | grep -q '<eagle-summary>' 2>/dev/null; then
|
|
93
|
+
summary_block=$(echo "$text_content" | sed -n '/<eagle-summary>/,/<\/eagle-summary>/p' | sed '1d;$d')
|
|
94
|
+
fi
|
|
102
95
|
|
|
103
|
-
|
|
96
|
+
if [ -n "$summary_block" ]; then
|
|
97
|
+
parse_field() {
|
|
98
|
+
local block="$1"
|
|
99
|
+
local field="$2"
|
|
100
|
+
echo "$block" | awk -v f="$field" '
|
|
101
|
+
BEGIN { IGNORECASE=1; found=0 }
|
|
102
|
+
$0 ~ "^"f":" {
|
|
103
|
+
sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
|
|
104
|
+
}
|
|
105
|
+
found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
|
|
106
|
+
found { val = val " " $0 }
|
|
107
|
+
END { if (found) print val }
|
|
108
|
+
'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Override heuristic fields with eagle-summary where non-empty
|
|
112
|
+
_val=$(parse_field "$summary_block" "request"); [ -n "$_val" ] && request="$_val"
|
|
113
|
+
_val=$(parse_field "$summary_block" "investigated"); [ -n "$_val" ] && investigated="$_val"
|
|
114
|
+
_val=$(parse_field "$summary_block" "learned"); [ -n "$_val" ] && learned="$_val"
|
|
115
|
+
_val=$(parse_field "$summary_block" "completed"); [ -n "$_val" ] && completed="$_val"
|
|
116
|
+
_val=$(parse_field "$summary_block" "next_steps"); [ -n "$_val" ] && next_steps="$_val"
|
|
117
|
+
_val=$(parse_field "$summary_block" "decisions"); [ -n "$_val" ] && decisions="$_val"
|
|
118
|
+
_val=$(parse_field "$summary_block" "gotchas"); [ -n "$_val" ] && gotchas="$_val"
|
|
119
|
+
_val=$(parse_field "$summary_block" "key_files"); [ -n "$_val" ] && key_files="$_val"
|
|
120
|
+
|
|
121
|
+
# Convert bracket-list to JSON array
|
|
104
122
|
to_json_array() {
|
|
105
123
|
local raw="$1"
|
|
106
124
|
raw=$(echo "$raw" | sed 's/^\[//;s/\]$//')
|
|
107
125
|
echo "$raw" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | jq -Rsc 'split("\n") | map(select(. != ""))'
|
|
108
126
|
}
|
|
109
127
|
|
|
128
|
+
raw_fr=$(parse_field "$summary_block" "files_read")
|
|
129
|
+
raw_fm=$(parse_field "$summary_block" "files_modified")
|
|
110
130
|
[ -n "$raw_fr" ] && files_read=$(to_json_array "$raw_fr")
|
|
111
131
|
[ -n "$raw_fm" ] && files_modified=$(to_json_array "$raw_fm")
|
|
112
132
|
|
|
113
|
-
eagle_log "INFO" "Stop:
|
|
133
|
+
eagle_log "INFO" "Stop: eagle-summary block merged over heuristic data"
|
|
114
134
|
fi
|
|
115
135
|
|
|
116
|
-
# ───
|
|
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.
|
|
120
|
-
|
|
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 ───────────
|
|
136
|
+
# ─── LLM enrichment: fill in decisions/gotchas/key_files ─────
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
|
|
139
|
+
provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
|
|
140
|
+
if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
|
|
141
|
+
excerpt=$(echo "$text_content" | tail -c 2000)
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
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)
|
|
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)
|
|
137
|
-
|
|
138
|
-
if [ -n "$heuristic_reads" ]; then
|
|
139
|
-
files_read=$(echo "$heuristic_reads" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
140
|
-
fi
|
|
141
|
-
if [ -n "$heuristic_writes" ]; then
|
|
142
|
-
files_modified=$(echo "$heuristic_writes" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
143
|
-
fi
|
|
144
|
-
|
|
145
|
-
completed="(auto-captured from tool usage)"
|
|
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:
|
|
143
|
+
enrich_prompt="Extract from this Claude Code session excerpt:
|
|
156
144
|
1. DECISIONS: architectural or design choices made (with WHY). One per line.
|
|
157
145
|
2. GOTCHAS: non-obvious pitfalls, bugs found, things that surprised. One per line.
|
|
158
146
|
3. KEY_FILES: important files that were central to the work. One per line.
|
|
@@ -168,28 +156,24 @@ GOTCHAS:
|
|
|
168
156
|
KEY_FILES:
|
|
169
157
|
- <filepath>"
|
|
170
158
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
fi
|
|
159
|
+
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
|
|
160
|
+
|
|
161
|
+
if [ -n "$enrich_result" ]; then
|
|
162
|
+
extract_section() {
|
|
163
|
+
local result="$1" header="$2"
|
|
164
|
+
echo "$result" | awk -v h="$header:" '
|
|
165
|
+
$0 == h || $0 ~ "^"h { found=1; next }
|
|
166
|
+
found && /^[A-Z_]+:/ { exit }
|
|
167
|
+
found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
|
|
168
|
+
END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
|
|
169
|
+
'
|
|
170
|
+
}
|
|
171
|
+
decisions=$(extract_section "$enrich_result" "DECISIONS")
|
|
172
|
+
gotchas=$(extract_section "$enrich_result" "GOTCHAS")
|
|
173
|
+
key_files=$(extract_section "$enrich_result" "KEY_FILES")
|
|
174
|
+
[ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
|
|
188
175
|
fi
|
|
189
176
|
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)"
|
|
193
177
|
fi
|
|
194
178
|
|
|
195
179
|
# ─── Redact secrets from all text fields before storage ────
|
package/lib/common.sh
CHANGED
|
@@ -27,12 +27,14 @@ eagle_project_from_cwd() {
|
|
|
27
27
|
local cwd="${1:-$(pwd)}"
|
|
28
28
|
local resolved="$cwd"
|
|
29
29
|
|
|
30
|
-
#
|
|
30
|
+
# Normalize macOS /private prefixes
|
|
31
31
|
case "$resolved" in /private/tmp*) resolved="/tmp${resolved#/private/tmp}" ;; esac
|
|
32
|
+
case "$resolved" in /private/var/*) resolved="/var${resolved#/private/var}" ;; esac
|
|
32
33
|
|
|
33
34
|
# Skip ephemeral directories — return empty so hooks early-exit
|
|
34
35
|
case "$resolved" in
|
|
35
36
|
/tmp|/tmp/*|/var/tmp|/var/tmp/*) echo ""; return ;;
|
|
37
|
+
/var/folders|/var/folders/*) echo ""; return ;;
|
|
36
38
|
"$HOME/Downloads"|"$HOME/Downloads/"*) echo ""; return ;;
|
|
37
39
|
"$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
|
|
38
40
|
esac
|
|
@@ -42,6 +44,12 @@ eagle_project_from_cwd() {
|
|
|
42
44
|
if [ -n "$git_root" ]; then
|
|
43
45
|
basename "$git_root"
|
|
44
46
|
else
|
|
47
|
+
local name
|
|
48
|
+
name=$(basename "$cwd")
|
|
49
|
+
# Reject single-character project names (likely temp dir fragments)
|
|
50
|
+
if [ ${#name} -le 1 ]; then
|
|
51
|
+
echo ""; return
|
|
52
|
+
fi
|
|
45
53
|
basename "$cwd"
|
|
46
54
|
fi
|
|
47
55
|
}
|
package/lib/db-observations.sh
CHANGED
|
@@ -50,9 +50,9 @@ eagle_get_command_rule() {
|
|
|
50
50
|
eagle_db "SELECT strategy, max_lines, reason
|
|
51
51
|
FROM command_rules
|
|
52
52
|
WHERE enabled = 1
|
|
53
|
-
AND (project = '$project' OR project
|
|
53
|
+
AND (project = '$project' OR project = '')
|
|
54
54
|
AND ('$cmd' LIKE pattern OR '$cmd' = pattern)
|
|
55
|
-
ORDER BY CASE WHEN project
|
|
55
|
+
ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END
|
|
56
56
|
LIMIT 1;"
|
|
57
57
|
}
|
|
58
58
|
|
package/lib/db-sessions.sh
CHANGED
|
@@ -69,27 +69,16 @@ eagle_count_session_summaries() {
|
|
|
69
69
|
|
|
70
70
|
eagle_meta_get() {
|
|
71
71
|
local key; key=$(eagle_sql_escape "$1")
|
|
72
|
-
local
|
|
73
|
-
|
|
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
|
|
72
|
+
local p_esc; p_esc=$(eagle_sql_escape "${2:-}")
|
|
73
|
+
eagle_db "SELECT value FROM eagle_meta WHERE key = '$key' AND project = '$p_esc' LIMIT 1;"
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
eagle_meta_set() {
|
|
82
77
|
local key; key=$(eagle_sql_escape "$1")
|
|
83
78
|
local value; value=$(eagle_sql_escape "$2")
|
|
84
|
-
local
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
79
|
+
local p_esc; p_esc=$(eagle_sql_escape "${3:-}")
|
|
80
|
+
eagle_db "INSERT INTO eagle_meta (key, project, value) VALUES ('$key', '$p_esc', '$value')
|
|
81
|
+
ON CONFLICT(key, project) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
93
82
|
}
|
|
94
83
|
|
|
95
84
|
eagle_count_sessions_since() {
|
package/package.json
CHANGED
package/scripts/health.sh
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# ════════��══════════��═══════════════════════════════════════
|
|
3
3
|
# Eagle Mem — Health Check
|
|
4
4
|
# Diagnoses how well the self-learning pipeline is working
|
|
5
|
-
#
|
|
5
|
+
# ════════���══════════════════════════════════════════════════
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -13,8 +13,6 @@ LIB_DIR="$SCRIPT_DIR/../lib"
|
|
|
13
13
|
. "$LIB_DIR/db.sh"
|
|
14
14
|
. "$LIB_DIR/provider.sh"
|
|
15
15
|
|
|
16
|
-
eagle_header "Health Check"
|
|
17
|
-
|
|
18
16
|
project=""
|
|
19
17
|
JSON_OUT=0
|
|
20
18
|
|
|
@@ -26,6 +24,12 @@ while [ $# -gt 0 ]; do
|
|
|
26
24
|
esac
|
|
27
25
|
done
|
|
28
26
|
|
|
27
|
+
if [ "$JSON_OUT" -eq 1 ]; then
|
|
28
|
+
exec 3>&1 1>&2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
eagle_header "Health Check"
|
|
32
|
+
|
|
29
33
|
if [ -z "$project" ]; then
|
|
30
34
|
project=$(eagle_project_from_cwd "$(pwd)")
|
|
31
35
|
fi
|
|
@@ -44,135 +48,148 @@ score=0
|
|
|
44
48
|
max_score=0
|
|
45
49
|
issues=()
|
|
46
50
|
|
|
47
|
-
#
|
|
51
|
+
# ─��─ 1. Summary capture rate (25 pts) ───────────────────
|
|
48
52
|
|
|
49
|
-
max_score=$((max_score +
|
|
53
|
+
max_score=$((max_score + 25))
|
|
50
54
|
|
|
55
|
+
total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p_esc';")
|
|
51
56
|
total_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
|
|
57
|
+
heuristic_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc' AND completed = '(auto-captured)';")
|
|
52
58
|
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
59
|
|
|
54
|
-
if [ "${
|
|
55
|
-
|
|
60
|
+
if [ "${total_sessions:-0}" -eq 0 ]; then
|
|
61
|
+
capture_pct=0
|
|
56
62
|
else
|
|
57
|
-
|
|
63
|
+
capture_pct=$((total_summaries * 100 / total_sessions))
|
|
58
64
|
fi
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
model_summaries=$((total_summaries - heuristic_summaries))
|
|
67
|
+
|
|
68
|
+
if [ "${total_summaries:-0}" -gt 0 ]; then
|
|
69
|
+
if [ "$capture_pct" -ge 50 ]; then
|
|
70
|
+
eagle_ok "Capture: ${total_summaries}/${total_sessions} sessions (${capture_pct}%) — ${model_summaries} from model, ${heuristic_summaries} heuristic"
|
|
71
|
+
score=$((score + 25))
|
|
72
|
+
elif [ "$capture_pct" -ge 20 ]; then
|
|
73
|
+
eagle_warn "Capture: ${total_summaries}/${total_sessions} sessions (${capture_pct}%) — ${heuristic_summaries} heuristic"
|
|
74
|
+
score=$((score + 15))
|
|
75
|
+
else
|
|
76
|
+
eagle_fail "Capture: ${total_summaries}/${total_sessions} sessions (${capture_pct}%)"
|
|
77
|
+
score=$((score + 5))
|
|
78
|
+
fi
|
|
70
79
|
else
|
|
71
80
|
eagle_dim " No summaries yet"
|
|
72
81
|
fi
|
|
73
82
|
|
|
74
|
-
# ─── 2.
|
|
83
|
+
# ─── 2. Enrichment rate (25 pts) ────────────────────────
|
|
75
84
|
|
|
76
|
-
max_score=$((max_score +
|
|
85
|
+
max_score=$((max_score + 25))
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
if [ "${total_summaries:-0}" -eq 0 ]; then
|
|
88
|
+
enrich_pct=0
|
|
89
|
+
else
|
|
90
|
+
enrich_pct=$((enriched_summaries * 100 / total_summaries))
|
|
91
|
+
fi
|
|
80
92
|
|
|
81
|
-
if [ "${
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if [ "${total_summaries:-0}" -gt 0 ]; then
|
|
94
|
+
if [ "$enrich_pct" -ge 50 ]; then
|
|
95
|
+
eagle_ok "Enriched: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) have decisions/gotchas/key_files"
|
|
96
|
+
score=$((score + 25))
|
|
97
|
+
elif [ "$enrich_pct" -ge 20 ]; then
|
|
98
|
+
eagle_warn "Enriched: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — LLM extraction may need tuning"
|
|
99
|
+
score=$((score + 12))
|
|
100
|
+
issues+=("Low enrichment (${enrich_pct}%). Check LLM provider is responsive: eagle-mem config")
|
|
101
|
+
elif [ "${enriched_summaries:-0}" -gt 0 ]; then
|
|
102
|
+
eagle_fail "Enriched: ${enriched_summaries}/${total_summaries} (${enrich_pct}%)"
|
|
103
|
+
score=$((score + 5))
|
|
104
|
+
issues+=("${enrich_pct}% enrichment. Decisions/gotchas/key_files mostly missing.")
|
|
105
|
+
else
|
|
106
|
+
eagle_fail "Enriched: 0/${total_summaries} — no summaries have decisions/gotchas/key_files"
|
|
107
|
+
issues+=("Zero enrichment. Check provider config: eagle-mem config")
|
|
108
|
+
fi
|
|
88
109
|
else
|
|
89
|
-
|
|
90
|
-
issues+=("No features discovered. Run: eagle-mem curate")
|
|
110
|
+
eagle_dim " No summaries to enrich"
|
|
91
111
|
fi
|
|
92
112
|
|
|
93
|
-
# ─── 3.
|
|
113
|
+
# ─── 3. Feature discovery (15 pts) ─────────────────────
|
|
94
114
|
|
|
95
115
|
max_score=$((max_score + 15))
|
|
96
116
|
|
|
97
|
-
|
|
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;")
|
|
117
|
+
feature_count=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
|
|
99
118
|
|
|
100
|
-
if [ "${
|
|
101
|
-
eagle_ok "
|
|
119
|
+
if [ "${feature_count:-0}" -ge 3 ]; then
|
|
120
|
+
eagle_ok "Features: ${feature_count} tracked"
|
|
102
121
|
score=$((score + 15))
|
|
103
|
-
elif [ "${
|
|
104
|
-
eagle_warn "
|
|
122
|
+
elif [ "${feature_count:-0}" -ge 1 ]; then
|
|
123
|
+
eagle_warn "Features: ${feature_count} — curator needs more sessions"
|
|
105
124
|
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
125
|
else
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
eagle_fail "Features: 0 — run: eagle-mem curate"
|
|
127
|
+
issues+=("No features discovered. Run: eagle-mem curate")
|
|
112
128
|
fi
|
|
113
129
|
|
|
114
|
-
# ─── 4. Provider configured
|
|
130
|
+
# ─── 4. Provider configured (15 pts) ───────────────────
|
|
115
131
|
|
|
116
132
|
max_score=$((max_score + 15))
|
|
117
133
|
|
|
118
134
|
provider=$(eagle_config_get "provider" "type" "none")
|
|
119
135
|
if [ "$provider" != "none" ]; then
|
|
120
136
|
model=$(eagle_config_get "$provider" "model" "default")
|
|
121
|
-
eagle_ok "
|
|
137
|
+
eagle_ok "Provider: ${provider} (${model})"
|
|
122
138
|
score=$((score + 15))
|
|
123
139
|
else
|
|
124
|
-
eagle_fail "No LLM provider — curator and enrichment
|
|
140
|
+
eagle_fail "No LLM provider — curator and enrichment disabled"
|
|
125
141
|
issues+=("Configure a provider: eagle-mem config init")
|
|
126
142
|
fi
|
|
127
143
|
|
|
128
|
-
# ─── 5.
|
|
144
|
+
# ─── 5. Data quality (10 pts) ──────────────────────────
|
|
129
145
|
|
|
130
146
|
max_score=$((max_score + 10))
|
|
131
147
|
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
# Count noise: ephemeral projects + single-char project names
|
|
149
|
+
noise_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project IN ('tmp', 'private', '', 'T') OR LENGTH(project) <= 1;")
|
|
150
|
+
all_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions;")
|
|
134
151
|
|
|
135
|
-
if [ "${
|
|
152
|
+
if [ "${all_sessions:-0}" -eq 0 ]; then
|
|
136
153
|
noise_pct=0
|
|
137
154
|
else
|
|
138
|
-
noise_pct=$((
|
|
155
|
+
noise_pct=$((noise_sessions * 100 / all_sessions))
|
|
139
156
|
fi
|
|
140
157
|
|
|
141
158
|
if [ "$noise_pct" -le 5 ]; then
|
|
142
|
-
eagle_ok "Data quality: ${
|
|
159
|
+
eagle_ok "Data quality: ${noise_pct}% noise (${noise_sessions} ephemeral sessions)"
|
|
143
160
|
score=$((score + 10))
|
|
144
161
|
elif [ "$noise_pct" -le 20 ]; then
|
|
145
|
-
eagle_warn "Data quality: ${
|
|
162
|
+
eagle_warn "Data quality: ${noise_pct}% noise (${noise_sessions}/${all_sessions})"
|
|
146
163
|
score=$((score + 5))
|
|
147
|
-
issues+=("${noise_pct}%
|
|
164
|
+
issues+=("${noise_pct}% noise. Skiplist prevents new pollution; prune old: eagle-mem prune")
|
|
148
165
|
else
|
|
149
|
-
eagle_fail "Data quality: ${noise_pct}% noise
|
|
150
|
-
issues+=("Heavy
|
|
166
|
+
eagle_fail "Data quality: ${noise_pct}% noise (${noise_sessions}/${all_sessions})"
|
|
167
|
+
issues+=("Heavy noise. Run: eagle-mem prune to clean ephemeral data.")
|
|
151
168
|
fi
|
|
152
169
|
|
|
153
|
-
# ─── 6. Curator activity
|
|
170
|
+
# ─── 6. Curator activity (10 pts) ──────────────────────
|
|
154
171
|
|
|
155
172
|
max_score=$((max_score + 10))
|
|
156
173
|
|
|
157
174
|
curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
|
|
158
|
-
last_curated=$(eagle_db "SELECT value FROM eagle_meta WHERE key = 'last_curated_at' AND
|
|
175
|
+
last_curated=$(eagle_db "SELECT value FROM eagle_meta WHERE key = 'last_curated_at' AND project = '$p_esc' LIMIT 1;" 2>/dev/null || echo "")
|
|
159
176
|
|
|
160
177
|
if [ -n "$last_curated" ]; then
|
|
161
178
|
eagle_ok "Curator: last run ${last_curated} (schedule: ${curator_schedule})"
|
|
162
179
|
score=$((score + 10))
|
|
163
180
|
elif [ "$curator_schedule" = "auto" ]; then
|
|
164
|
-
eagle_warn "Curator: auto-scheduled
|
|
181
|
+
eagle_warn "Curator: auto-scheduled, hasn't run yet (triggers on session start)"
|
|
165
182
|
score=$((score + 5))
|
|
166
|
-
issues+=("Auto-curate
|
|
183
|
+
issues+=("Auto-curate configured but hasn't fired. Needs ${_min_sessions:-5}+ sessions since last run.")
|
|
167
184
|
else
|
|
168
185
|
eagle_fail "Curator: never run (schedule: ${curator_schedule})"
|
|
169
|
-
issues+=("
|
|
186
|
+
issues+=("Run: eagle-mem curate --dry-run")
|
|
170
187
|
fi
|
|
171
188
|
|
|
172
|
-
# ─── Score
|
|
189
|
+
# ─── Score ───────���────────────────────────────────────────
|
|
173
190
|
|
|
174
191
|
echo ""
|
|
175
|
-
echo -e " ${DIM}
|
|
192
|
+
echo -e " ${DIM}────���────────────────────────────────${RESET}"
|
|
176
193
|
|
|
177
194
|
pct=$((score * 100 / max_score))
|
|
178
195
|
if [ "$pct" -ge 80 ]; then
|
|
@@ -205,14 +222,17 @@ if [ "$JSON_OUT" -eq 1 ]; then
|
|
|
205
222
|
--argjson max_score "$max_score" \
|
|
206
223
|
--argjson pct "$pct" \
|
|
207
224
|
--arg grade "$grade" \
|
|
225
|
+
--argjson total_sessions "${total_sessions:-0}" \
|
|
208
226
|
--argjson total_summaries "${total_summaries:-0}" \
|
|
227
|
+
--argjson heuristic_summaries "${heuristic_summaries:-0}" \
|
|
209
228
|
--argjson enriched_summaries "${enriched_summaries:-0}" \
|
|
210
229
|
--argjson features "${feature_count:-0}" \
|
|
211
|
-
--argjson command_rules "${rule_count:-0}" \
|
|
212
230
|
--arg provider "$provider" \
|
|
213
231
|
--argjson noise_pct "$noise_pct" \
|
|
232
|
+
--arg last_curated "${last_curated:-never}" \
|
|
214
233
|
'{project:$project, score:$score, max:$max_score, pct:$pct, grade:$grade,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
provider:$provider,
|
|
234
|
+
capture:{sessions:$total_sessions, summaries:$total_summaries, heuristic:$heuristic_summaries},
|
|
235
|
+
enrichment:$enriched_summaries,
|
|
236
|
+
features:$features, provider:$provider,
|
|
237
|
+
noise_pct:$noise_pct, last_curated:$last_curated}' >&3
|
|
218
238
|
fi
|
package/scripts/install.sh
CHANGED
|
@@ -142,7 +142,7 @@ cp "$PACKAGE_DIR"/hooks/*.sh "$EAGLE_MEM_DIR/hooks/"
|
|
|
142
142
|
cp "$PACKAGE_DIR"/lib/*.sh "$EAGLE_MEM_DIR/lib/"
|
|
143
143
|
cp "$PACKAGE_DIR"/db/*.sh "$EAGLE_MEM_DIR/db/"
|
|
144
144
|
cp "$PACKAGE_DIR"/db/*.sql "$EAGLE_MEM_DIR/db/"
|
|
145
|
-
cp "$PACKAGE_DIR"/scripts
|
|
145
|
+
cp "$PACKAGE_DIR"/scripts/*.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
|
|
146
146
|
|
|
147
147
|
chmod +x "$EAGLE_MEM_DIR"/hooks/*.sh
|
|
148
148
|
chmod +x "$EAGLE_MEM_DIR"/db/migrate.sh
|
package/scripts/update.sh
CHANGED
|
@@ -32,15 +32,17 @@ fi
|
|
|
32
32
|
|
|
33
33
|
# ─── Update files ──────────────────────────────────────────
|
|
34
34
|
|
|
35
|
-
mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db}
|
|
35
|
+
mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db,scripts}
|
|
36
36
|
|
|
37
37
|
cp "$PACKAGE_DIR"/hooks/*.sh "$EAGLE_MEM_DIR/hooks/"
|
|
38
38
|
cp "$PACKAGE_DIR"/lib/*.sh "$EAGLE_MEM_DIR/lib/"
|
|
39
39
|
cp "$PACKAGE_DIR"/db/*.sh "$EAGLE_MEM_DIR/db/"
|
|
40
40
|
cp "$PACKAGE_DIR"/db/*.sql "$EAGLE_MEM_DIR/db/"
|
|
41
|
+
cp "$PACKAGE_DIR"/scripts/*.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
|
|
41
42
|
|
|
42
43
|
chmod +x "$EAGLE_MEM_DIR"/hooks/*.sh
|
|
43
44
|
chmod +x "$EAGLE_MEM_DIR"/db/migrate.sh
|
|
45
|
+
chmod +x "$EAGLE_MEM_DIR"/scripts/*.sh 2>/dev/null
|
|
44
46
|
|
|
45
47
|
eagle_ok "Files updated"
|
|
46
48
|
|