eagle-mem 3.1.1 → 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/018_command_rules_not_null.sql +23 -0
- package/hooks/session-end.sh +1 -22
- package/hooks/session-start.sh +55 -85
- package/hooks/stop.sh +96 -112
- package/lib/common.sh +9 -1
- package/lib/db-observations.sh +2 -2
- package/package.json +1 -1
- package/scripts/health.sh +82 -67
|
@@ -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
|
|
|
@@ -40,6 +42,25 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
|
40
42
|
# so long-lived sessions with regular compactions aren't falsely abandoned
|
|
41
43
|
eagle_abandon_stale_sessions "$session_id"
|
|
42
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
|
+
|
|
43
64
|
# ─── Version check (non-blocking) ────────────────────────────
|
|
44
65
|
|
|
45
66
|
update_notice=""
|
|
@@ -127,38 +148,25 @@ eagle_banner="======================================
|
|
|
127
148
|
======================================"
|
|
128
149
|
|
|
129
150
|
context="$eagle_banner
|
|
130
|
-
|
|
131
|
-
=== EAGLE MEM — Active (trigger: $source_type) ===
|
|
132
|
-
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.
|
|
133
|
-
|
|
134
151
|
"
|
|
135
152
|
|
|
136
153
|
if [ -n "$update_notice" ]; then
|
|
137
|
-
context+="
|
|
138
|
-
|
|
139
|
-
"
|
|
140
|
-
fi
|
|
141
|
-
|
|
142
|
-
# Nudge if last session lacked enrichment
|
|
143
|
-
last_enriched=$(eagle_last_session_enriched "$project")
|
|
144
|
-
if [ "${last_enriched:-1}" = "0" ] && [ "$stat_with_summaries" -gt 0 ]; then
|
|
145
|
-
context+="=== EAGLE MEM — Enrichment Reminder ===
|
|
146
|
-
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.
|
|
147
|
-
|
|
154
|
+
context+="
|
|
155
|
+
=== $update_notice ===
|
|
148
156
|
"
|
|
149
157
|
fi
|
|
150
158
|
|
|
151
159
|
# Project overview
|
|
152
160
|
overview=$(eagle_get_overview "$project")
|
|
153
161
|
if [ -n "$overview" ]; then
|
|
154
|
-
context+="
|
|
162
|
+
context+="
|
|
163
|
+
=== Project Overview ===
|
|
155
164
|
$overview
|
|
156
|
-
|
|
157
165
|
"
|
|
158
166
|
else
|
|
159
|
-
context+="
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
context+="
|
|
168
|
+
=== Action Required ===
|
|
169
|
+
No overview exists for '$project'. Run /eagle-mem-overview to build one.
|
|
162
170
|
"
|
|
163
171
|
fi
|
|
164
172
|
|
|
@@ -166,8 +174,8 @@ fi
|
|
|
166
174
|
recent=$(eagle_get_recent_summaries "$project" 5)
|
|
167
175
|
|
|
168
176
|
if [ -n "$recent" ]; then
|
|
169
|
-
context+="
|
|
170
|
-
Recent
|
|
177
|
+
context+="
|
|
178
|
+
=== Recent Sessions ===
|
|
171
179
|
"
|
|
172
180
|
while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
|
|
173
181
|
[ -z "$request" ] && [ -z "$completed" ] && continue
|
|
@@ -202,8 +210,7 @@ memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, up
|
|
|
202
210
|
LIMIT 5;")
|
|
203
211
|
if [ -n "$memories" ]; then
|
|
204
212
|
context+="
|
|
205
|
-
===
|
|
206
|
-
Recent memories for '$project':
|
|
213
|
+
=== Memories ===
|
|
207
214
|
"
|
|
208
215
|
while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
|
|
209
216
|
[ -z "$mname" ] && continue
|
|
@@ -227,8 +234,7 @@ fi
|
|
|
227
234
|
plans=$(eagle_list_claude_plans "$project" 3)
|
|
228
235
|
if [ -n "$plans" ]; then
|
|
229
236
|
context+="
|
|
230
|
-
===
|
|
231
|
-
Recent plans for '$project':
|
|
237
|
+
=== Plans ===
|
|
232
238
|
"
|
|
233
239
|
while IFS='|' read -r ptitle _pproj _fpath _updated; do
|
|
234
240
|
[ -z "$ptitle" ] && continue
|
|
@@ -249,8 +255,7 @@ synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
|
|
|
249
255
|
LIMIT 10;")
|
|
250
256
|
if [ -n "$synced_tasks" ]; then
|
|
251
257
|
context+="
|
|
252
|
-
===
|
|
253
|
-
Tasks for '$project':
|
|
258
|
+
=== Tasks ===
|
|
254
259
|
"
|
|
255
260
|
while IFS='|' read -r tsubject tstatus tblocked; do
|
|
256
261
|
[ -z "$tsubject" ] && continue
|
|
@@ -263,72 +268,37 @@ Tasks for '$project':
|
|
|
263
268
|
done <<< "$synced_tasks"
|
|
264
269
|
fi
|
|
265
270
|
|
|
266
|
-
#
|
|
267
|
-
context+="
|
|
268
|
-
=== EAGLE MEM INSTRUCTIONS ===
|
|
269
|
-
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.
|
|
270
|
-
|
|
271
|
-
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) ──
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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.
|
|
284
|
-
|
|
285
|
-
=== EAGLE MEM — SESSION SUMMARY (MANDATORY) ===
|
|
286
|
-
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.
|
|
287
|
-
|
|
288
|
-
FORMAT — emit this block exactly. Every field is REQUIRED. Do not skip fields, do not leave them empty, do not write \"N/A\".
|
|
289
|
-
|
|
290
|
-
<eagle-summary>
|
|
291
|
-
request: [One sentence: what did the user ask for?]
|
|
292
|
-
investigated: [Comma-separated file paths you read or explored]
|
|
293
|
-
learned: [Non-obvious technical discoveries — things a future session could not guess from reading the code]
|
|
294
|
-
completed: [What was accomplished — be specific about what shipped, not what was \"worked on\"]
|
|
295
|
-
next_steps: [Concrete actions for the next session, not vague aspirations]
|
|
296
|
-
decisions:
|
|
297
|
-
- [Choice made] Why: [the reason — what constraint or tradeoff drove this choice]
|
|
298
|
-
- [Choice made] Why: [reason]
|
|
299
|
-
gotchas:
|
|
300
|
-
- [What failed, surprised, or does not work the obvious way. Be specific — \"X does not work because Y\" not just \"X was tricky\"]
|
|
301
|
-
key_files:
|
|
302
|
-
- [path/to/file.ext] — [one-line role: what this file does in the context of this work]
|
|
303
|
-
- [path/to/other.ext] — [role]
|
|
304
|
-
files_read: [file1, file2, ...]
|
|
305
|
-
files_modified: [file1, file2, ...]
|
|
306
|
-
</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.
|
|
307
282
|
|
|
308
|
-
|
|
283
|
+
Emit an <eagle-summary> block before your FINAL response:
|
|
309
284
|
|
|
310
285
|
<eagle-summary>
|
|
311
|
-
request:
|
|
312
|
-
investigated:
|
|
313
|
-
learned:
|
|
314
|
-
completed:
|
|
315
|
-
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]
|
|
316
291
|
decisions:
|
|
317
|
-
-
|
|
318
|
-
- 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]
|
|
319
293
|
gotchas:
|
|
320
|
-
-
|
|
321
|
-
- 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\"]
|
|
322
295
|
key_files:
|
|
323
|
-
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
files_read: [src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts]
|
|
327
|
-
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]
|
|
328
299
|
</eagle-summary>
|
|
329
|
-
|
|
330
|
-
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.
|
|
331
300
|
"
|
|
301
|
+
fi
|
|
332
302
|
|
|
333
303
|
# Output context (plain text stdout = additionalContext for SessionStart)
|
|
334
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/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)"
|
|
@@ -24,7 +24,6 @@ while [ $# -gt 0 ]; do
|
|
|
24
24
|
esac
|
|
25
25
|
done
|
|
26
26
|
|
|
27
|
-
# In JSON mode, redirect all text output to stderr so stdout is clean JSON
|
|
28
27
|
if [ "$JSON_OUT" -eq 1 ]; then
|
|
29
28
|
exec 3>&1 1>&2
|
|
30
29
|
fi
|
|
@@ -49,135 +48,148 @@ score=0
|
|
|
49
48
|
max_score=0
|
|
50
49
|
issues=()
|
|
51
50
|
|
|
52
|
-
#
|
|
51
|
+
# ─��─ 1. Summary capture rate (25 pts) ───────────────────
|
|
53
52
|
|
|
54
|
-
max_score=$((max_score +
|
|
53
|
+
max_score=$((max_score + 25))
|
|
55
54
|
|
|
55
|
+
total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p_esc';")
|
|
56
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)';")
|
|
57
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 != '');")
|
|
58
59
|
|
|
59
|
-
if [ "${
|
|
60
|
-
|
|
60
|
+
if [ "${total_sessions:-0}" -eq 0 ]; then
|
|
61
|
+
capture_pct=0
|
|
61
62
|
else
|
|
62
|
-
|
|
63
|
+
capture_pct=$((total_summaries * 100 / total_sessions))
|
|
63
64
|
fi
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
79
|
else
|
|
76
80
|
eagle_dim " No summaries yet"
|
|
77
81
|
fi
|
|
78
82
|
|
|
79
|
-
# ─── 2.
|
|
83
|
+
# ─── 2. Enrichment rate (25 pts) ────────────────────────
|
|
80
84
|
|
|
81
|
-
max_score=$((max_score +
|
|
85
|
+
max_score=$((max_score + 25))
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
if [ "${total_summaries:-0}" -eq 0 ]; then
|
|
88
|
+
enrich_pct=0
|
|
89
|
+
else
|
|
90
|
+
enrich_pct=$((enriched_summaries * 100 / total_summaries))
|
|
91
|
+
fi
|
|
85
92
|
|
|
86
|
-
if [ "${
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
93
109
|
else
|
|
94
|
-
|
|
95
|
-
issues+=("No features discovered. Run: eagle-mem curate")
|
|
110
|
+
eagle_dim " No summaries to enrich"
|
|
96
111
|
fi
|
|
97
112
|
|
|
98
|
-
# ─── 3.
|
|
113
|
+
# ─── 3. Feature discovery (15 pts) ─────────────────────
|
|
99
114
|
|
|
100
115
|
max_score=$((max_score + 15))
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
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';")
|
|
104
118
|
|
|
105
|
-
if [ "${
|
|
106
|
-
eagle_ok "
|
|
119
|
+
if [ "${feature_count:-0}" -ge 3 ]; then
|
|
120
|
+
eagle_ok "Features: ${feature_count} tracked"
|
|
107
121
|
score=$((score + 15))
|
|
108
|
-
elif [ "${
|
|
109
|
-
eagle_warn "
|
|
122
|
+
elif [ "${feature_count:-0}" -ge 1 ]; then
|
|
123
|
+
eagle_warn "Features: ${feature_count} — curator needs more sessions"
|
|
110
124
|
score=$((score + 8))
|
|
111
|
-
elif [ "${obs_with_metrics:-0}" -gt 20 ]; then
|
|
112
|
-
eagle_fail "Command rules: 0 (but ${obs_with_metrics} observations available — run curator)"
|
|
113
|
-
issues+=("Command metrics collected but no rules generated yet.")
|
|
114
125
|
else
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
eagle_fail "Features: 0 — run: eagle-mem curate"
|
|
127
|
+
issues+=("No features discovered. Run: eagle-mem curate")
|
|
117
128
|
fi
|
|
118
129
|
|
|
119
|
-
# ─── 4. Provider configured
|
|
130
|
+
# ─── 4. Provider configured (15 pts) ───────────────────
|
|
120
131
|
|
|
121
132
|
max_score=$((max_score + 15))
|
|
122
133
|
|
|
123
134
|
provider=$(eagle_config_get "provider" "type" "none")
|
|
124
135
|
if [ "$provider" != "none" ]; then
|
|
125
136
|
model=$(eagle_config_get "$provider" "model" "default")
|
|
126
|
-
eagle_ok "
|
|
137
|
+
eagle_ok "Provider: ${provider} (${model})"
|
|
127
138
|
score=$((score + 15))
|
|
128
139
|
else
|
|
129
|
-
eagle_fail "No LLM provider — curator and enrichment
|
|
140
|
+
eagle_fail "No LLM provider — curator and enrichment disabled"
|
|
130
141
|
issues+=("Configure a provider: eagle-mem config init")
|
|
131
142
|
fi
|
|
132
143
|
|
|
133
|
-
# ─── 5.
|
|
144
|
+
# ─── 5. Data quality (10 pts) ──────────────────────────
|
|
134
145
|
|
|
135
146
|
max_score=$((max_score + 10))
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
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;")
|
|
139
151
|
|
|
140
|
-
if [ "${
|
|
152
|
+
if [ "${all_sessions:-0}" -eq 0 ]; then
|
|
141
153
|
noise_pct=0
|
|
142
154
|
else
|
|
143
|
-
noise_pct=$((
|
|
155
|
+
noise_pct=$((noise_sessions * 100 / all_sessions))
|
|
144
156
|
fi
|
|
145
157
|
|
|
146
158
|
if [ "$noise_pct" -le 5 ]; then
|
|
147
|
-
eagle_ok "Data quality: ${
|
|
159
|
+
eagle_ok "Data quality: ${noise_pct}% noise (${noise_sessions} ephemeral sessions)"
|
|
148
160
|
score=$((score + 10))
|
|
149
161
|
elif [ "$noise_pct" -le 20 ]; then
|
|
150
|
-
eagle_warn "Data quality: ${
|
|
162
|
+
eagle_warn "Data quality: ${noise_pct}% noise (${noise_sessions}/${all_sessions})"
|
|
151
163
|
score=$((score + 5))
|
|
152
|
-
issues+=("${noise_pct}%
|
|
164
|
+
issues+=("${noise_pct}% noise. Skiplist prevents new pollution; prune old: eagle-mem prune")
|
|
153
165
|
else
|
|
154
|
-
eagle_fail "Data quality: ${noise_pct}% noise
|
|
155
|
-
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.")
|
|
156
168
|
fi
|
|
157
169
|
|
|
158
|
-
# ─── 6. Curator activity
|
|
170
|
+
# ─── 6. Curator activity (10 pts) ──────────────────────
|
|
159
171
|
|
|
160
172
|
max_score=$((max_score + 10))
|
|
161
173
|
|
|
162
174
|
curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
|
|
163
|
-
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 "")
|
|
164
176
|
|
|
165
177
|
if [ -n "$last_curated" ]; then
|
|
166
178
|
eagle_ok "Curator: last run ${last_curated} (schedule: ${curator_schedule})"
|
|
167
179
|
score=$((score + 10))
|
|
168
180
|
elif [ "$curator_schedule" = "auto" ]; then
|
|
169
|
-
eagle_warn "Curator: auto-scheduled
|
|
181
|
+
eagle_warn "Curator: auto-scheduled, hasn't run yet (triggers on session start)"
|
|
170
182
|
score=$((score + 5))
|
|
171
|
-
issues+=("Auto-curate
|
|
183
|
+
issues+=("Auto-curate configured but hasn't fired. Needs ${_min_sessions:-5}+ sessions since last run.")
|
|
172
184
|
else
|
|
173
185
|
eagle_fail "Curator: never run (schedule: ${curator_schedule})"
|
|
174
|
-
issues+=("
|
|
186
|
+
issues+=("Run: eagle-mem curate --dry-run")
|
|
175
187
|
fi
|
|
176
188
|
|
|
177
|
-
# ─── Score
|
|
189
|
+
# ─── Score ───────���────────────────────────────────────────
|
|
178
190
|
|
|
179
191
|
echo ""
|
|
180
|
-
echo -e " ${DIM}
|
|
192
|
+
echo -e " ${DIM}────���────────────────────────────────${RESET}"
|
|
181
193
|
|
|
182
194
|
pct=$((score * 100 / max_score))
|
|
183
195
|
if [ "$pct" -ge 80 ]; then
|
|
@@ -210,14 +222,17 @@ if [ "$JSON_OUT" -eq 1 ]; then
|
|
|
210
222
|
--argjson max_score "$max_score" \
|
|
211
223
|
--argjson pct "$pct" \
|
|
212
224
|
--arg grade "$grade" \
|
|
225
|
+
--argjson total_sessions "${total_sessions:-0}" \
|
|
213
226
|
--argjson total_summaries "${total_summaries:-0}" \
|
|
227
|
+
--argjson heuristic_summaries "${heuristic_summaries:-0}" \
|
|
214
228
|
--argjson enriched_summaries "${enriched_summaries:-0}" \
|
|
215
229
|
--argjson features "${feature_count:-0}" \
|
|
216
|
-
--argjson command_rules "${rule_count:-0}" \
|
|
217
230
|
--arg provider "$provider" \
|
|
218
231
|
--argjson noise_pct "$noise_pct" \
|
|
232
|
+
--arg last_curated "${last_curated:-never}" \
|
|
219
233
|
'{project:$project, score:$score, max:$max_score, pct:$pct, grade:$grade,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
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
|
|
223
238
|
fi
|