eagle-mem 3.1.1 → 3.3.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/README.md +14 -2
- package/db/018_command_rules_not_null.sql +23 -0
- package/db/020_drop_seeded_rules.sql +12 -0
- package/hooks/post-tool-use.sh +20 -1
- package/hooks/pre-tool-use.sh +115 -69
- package/hooks/session-end.sh +1 -22
- package/hooks/session-start.sh +59 -85
- package/hooks/stop.sh +96 -112
- package/lib/common.sh +9 -1
- package/lib/db-observations.sh +3 -2
- package/lib/db-sessions.sh +3 -3
- package/lib/hooks.sh +9 -1
- package/package.json +1 -1
- package/scripts/curate.sh +7 -0
- package/scripts/health.sh +82 -67
- package/scripts/install.sh +5 -1
- package/scripts/update.sh +1 -0
package/README.md
CHANGED
|
@@ -159,16 +159,27 @@ The `update` command copies new files, runs any pending database migrations, and
|
|
|
159
159
|
|
|
160
160
|
## How it works
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
Six hooks fire automatically at different points in Claude Code's lifecycle:
|
|
163
163
|
|
|
164
164
|
| Hook | Fires when | What it does |
|
|
165
165
|
|------|-----------|--------------|
|
|
166
166
|
| **SessionStart** | startup, resume, clear, compact | Injects overview, summaries, memories, tasks |
|
|
167
|
+
| **PreToolUse** | before Bash and Read calls | Rewrites noisy commands (learned rules), detects redundant reads |
|
|
167
168
|
| **UserPromptSubmit** | user sends a message | FTS5 search for relevant past context |
|
|
168
|
-
| **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes |
|
|
169
|
+
| **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes, tracks modifications |
|
|
169
170
|
| **Stop** | Claude's turn ends | Extracts `<eagle-summary>`, strips `<private>` tags |
|
|
170
171
|
| **SessionEnd** | session closes | Re-syncs tasks, marks session completed |
|
|
171
172
|
|
|
173
|
+
### Token savings
|
|
174
|
+
|
|
175
|
+
Eagle Mem actively reduces token consumption:
|
|
176
|
+
|
|
177
|
+
- **Command rewriting** — PreToolUse rewrites noisy Bash commands (e.g., `find`, `grep`) to pipe through `head -N`, using `updatedInput` to modify the command before execution. Rules are learned by the curator from real usage, not hardcoded.
|
|
178
|
+
- **Read-after-modify detection** — If you just edited or wrote a file, Eagle Mem nudges that the diff is already in context before a redundant Read.
|
|
179
|
+
- **Read dedup tracking** — Files read 3+ times in a session get a soft nudge that contents are likely already in context.
|
|
180
|
+
|
|
181
|
+
### Data
|
|
182
|
+
|
|
172
183
|
Data lives in a single SQLite database at `~/.eagle-mem/memory.db` (WAL mode, FTS5 full-text search):
|
|
173
184
|
|
|
174
185
|
| Table | What it stores |
|
|
@@ -178,6 +189,7 @@ Data lives in a single SQLite database at `~/.eagle-mem/memory.db` (WAL mode, FT
|
|
|
178
189
|
| observations | Per-tool-use file touch records |
|
|
179
190
|
| overviews | One overview per project (scan or manual) |
|
|
180
191
|
| code_chunks | FTS5-indexed source file chunks |
|
|
192
|
+
| command_rules | Curator-learned command output rules |
|
|
181
193
|
| claude_memories | Mirror of Claude Code auto-memories |
|
|
182
194
|
| claude_plans | Mirror of Claude Code plans |
|
|
183
195
|
| claude_tasks | Mirror of Claude Code tasks |
|
|
@@ -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);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Migration 020: Remove seeded global command rules.
|
|
2
|
+
-- Self-learning pipeline (curator) now generates rules from real usage.
|
|
3
|
+
-- Per-project rules created by the curator are preserved.
|
|
4
|
+
--
|
|
5
|
+
-- Safety: back up any existing global rules before deleting so they
|
|
6
|
+
-- can be restored if a user manually created rules they want to keep.
|
|
7
|
+
-- The backup table is left in place intentionally.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS _backup_020_global_rules AS
|
|
10
|
+
SELECT * FROM command_rules WHERE project = '';
|
|
11
|
+
|
|
12
|
+
DELETE FROM command_rules WHERE project = '';
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -68,7 +68,7 @@ case "$tool_name" in
|
|
|
68
68
|
cmd=$(echo "$cmd" | eagle_redact)
|
|
69
69
|
tool_summary="Bash: $cmd"
|
|
70
70
|
|
|
71
|
-
tool_output=$(echo "$input" | jq -r '.
|
|
71
|
+
tool_output=$(echo "$input" | jq -r '.tool_response.stdout // empty' 2>/dev/null)
|
|
72
72
|
if [ -n "$tool_output" ]; then
|
|
73
73
|
output_bytes=${#tool_output}
|
|
74
74
|
output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
|
|
@@ -98,6 +98,25 @@ case "$tool_name" in
|
|
|
98
98
|
;;
|
|
99
99
|
esac
|
|
100
100
|
|
|
101
|
+
# ─── Track recent Edit/Write targets for Read-after-modify detection ──
|
|
102
|
+
|
|
103
|
+
if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
|
|
104
|
+
case "$tool_name" in
|
|
105
|
+
Edit|Write)
|
|
106
|
+
mod_dir="$EAGLE_MEM_DIR/mod-tracker"
|
|
107
|
+
mkdir -p "$mod_dir" 2>/dev/null
|
|
108
|
+
mod_file="$mod_dir/${session_id}"
|
|
109
|
+
echo "$fp" >> "$mod_file"
|
|
110
|
+
# Keep only last 3 entries — use per-process tmp to avoid
|
|
111
|
+
# race when parallel PostToolUse hooks fire on same session
|
|
112
|
+
if [ -f "$mod_file" ]; then
|
|
113
|
+
_mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || _mod_tmp="${mod_file}.$$"
|
|
114
|
+
tail -3 "$mod_file" > "$_mod_tmp" && mv "$_mod_tmp" "$mod_file" || rm -f "$_mod_tmp"
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
fi
|
|
119
|
+
|
|
101
120
|
# ─── Dispatch to extracted responsibilities ───────────────
|
|
102
121
|
|
|
103
122
|
eagle_posttool_mirror_writes "$tool_name" "$fp" "$session_id" "$project"
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ═══════════════════════════════════════════════════════════
|
|
3
3
|
# Eagle Mem — PreToolUse hook
|
|
4
|
-
# Fires before
|
|
4
|
+
# Fires before Bash and Read tool calls
|
|
5
5
|
# 1. Surfaces feature verification checklists before git push
|
|
6
|
-
# 2.
|
|
6
|
+
# 2. Truncates noisy commands via updatedInput (curator-learned rules)
|
|
7
|
+
# 3. Detects Read-after-Edit/Write (content already in context)
|
|
8
|
+
# 4. Nudges on repeated file reads (dedup tracker)
|
|
7
9
|
# ═══════════════════════════════════════════════════════════
|
|
8
10
|
set +e
|
|
9
11
|
|
|
@@ -17,12 +19,13 @@ input=$(eagle_read_stdin)
|
|
|
17
19
|
[ -z "$input" ] && exit 0
|
|
18
20
|
|
|
19
21
|
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
20
|
-
[ "$tool_name" != "Bash" ] && exit 0
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
case "$tool_name" in
|
|
24
|
+
Bash|Read) ;;
|
|
25
|
+
*) exit 0 ;;
|
|
26
|
+
esac
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
[ -z "$cmd" ] && exit 0
|
|
28
|
+
[ ! -f "$EAGLE_MEM_DB" ] && exit 0
|
|
26
29
|
|
|
27
30
|
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
28
31
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
@@ -30,77 +33,120 @@ project=$(eagle_project_from_cwd "$cwd")
|
|
|
30
33
|
[ -z "$project" ] && exit 0
|
|
31
34
|
|
|
32
35
|
context=""
|
|
36
|
+
updated_input=""
|
|
37
|
+
|
|
38
|
+
case "$tool_name" in
|
|
39
|
+
Bash)
|
|
40
|
+
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
|
|
41
|
+
[ -z "$cmd" ] && exit 0
|
|
42
|
+
|
|
43
|
+
# ─── Feature verification on git push ─────────────────────
|
|
44
|
+
|
|
45
|
+
case "$cmd" in
|
|
46
|
+
*"git push"*|*"gh pr create"*)
|
|
47
|
+
has_features=$(eagle_count_active_features "$project")
|
|
48
|
+
if [ "${has_features:-0}" -gt 0 ]; then
|
|
49
|
+
changed_files=""
|
|
50
|
+
if [ -n "$cwd" ] && [ -d "$cwd" ]; then
|
|
51
|
+
changed_files=$(git -C "$cwd" diff --name-only HEAD 2>/dev/null)
|
|
52
|
+
[ -z "$changed_files" ] && changed_files=$(git -C "$cwd" diff --cached --name-only 2>/dev/null)
|
|
53
|
+
fi
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
context
|
|
61
|
-
[ -n "$feat_smoke" ] && context+=" | smoke: $feat_smoke"
|
|
62
|
-
[ -n "$feat_deps" ] && context+=" | deps: $feat_deps"
|
|
63
|
-
if [ -n "$feat_verified" ]; then
|
|
64
|
-
context+=" | last verified: $feat_verified"
|
|
65
|
-
else
|
|
66
|
-
context+=" | never verified"
|
|
67
|
-
fi
|
|
68
|
-
context+=$'\n'
|
|
69
|
-
done <<< "$feature_hits"
|
|
70
|
-
done <<< "$changed_files"
|
|
71
|
-
|
|
72
|
-
if [ -n "$context" ]; then
|
|
73
|
-
context="Eagle Mem: This push affects the following features. After deploy, verify each works and run 'eagle-mem feature verify <name>'.
|
|
55
|
+
if [ -n "$changed_files" ]; then
|
|
56
|
+
seen_features=""
|
|
57
|
+
while IFS= read -r changed_file; do
|
|
58
|
+
[ -z "$changed_file" ] && continue
|
|
59
|
+
fname=$(basename "$changed_file")
|
|
60
|
+
|
|
61
|
+
feature_hits=$(eagle_find_feature_for_push "$project" "$fname")
|
|
62
|
+
|
|
63
|
+
while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
|
|
64
|
+
[ -z "$feat_name" ] && continue
|
|
65
|
+
case "$seen_features" in *"|$feat_name|"*) continue ;; esac
|
|
66
|
+
seen_features+="|$feat_name|"
|
|
67
|
+
|
|
68
|
+
context+=" - $feat_name"
|
|
69
|
+
[ -n "$feat_smoke" ] && context+=" | smoke: $feat_smoke"
|
|
70
|
+
[ -n "$feat_deps" ] && context+=" | deps: $feat_deps"
|
|
71
|
+
if [ -n "$feat_verified" ]; then
|
|
72
|
+
context+=" | last verified: $feat_verified"
|
|
73
|
+
else
|
|
74
|
+
context+=" | never verified"
|
|
75
|
+
fi
|
|
76
|
+
context+=$'\n'
|
|
77
|
+
done <<< "$feature_hits"
|
|
78
|
+
done <<< "$changed_files"
|
|
79
|
+
|
|
80
|
+
if [ -n "$context" ]; then
|
|
81
|
+
context="Eagle Mem: This push affects the following features. After deploy, verify each works and run 'eagle-mem feature verify <name>'.
|
|
74
82
|
${context}"
|
|
83
|
+
fi
|
|
75
84
|
fi
|
|
76
85
|
fi
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
esac
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
80
88
|
|
|
81
|
-
# ─── Command output filtering (learned rules) ─────────────
|
|
89
|
+
# ─── Command output filtering (learned rules) ─────────────
|
|
90
|
+
|
|
91
|
+
base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
|
|
92
|
+
rule=$(eagle_get_command_rule "$project" "$base_cmd")
|
|
93
|
+
|
|
94
|
+
if [ -n "$rule" ]; then
|
|
95
|
+
IFS='|' read -r strategy max_lines reason <<< "$rule"
|
|
96
|
+
case "$strategy" in
|
|
97
|
+
truncate)
|
|
98
|
+
if [ -n "$max_lines" ] && [ "$max_lines" -gt 0 ] 2>/dev/null; then
|
|
99
|
+
case "$cmd" in
|
|
100
|
+
*"&&"*|*"||"*|*";"*)
|
|
101
|
+
context+="Eagle Mem: '${base_cmd}' produces long output (${reason}). Consider: | head -${max_lines}"
|
|
102
|
+
;;
|
|
103
|
+
*"| head"*|*"| tail"*|*"| wc"*|*"| grep"*|*">"*|*">>"*)
|
|
104
|
+
;;
|
|
105
|
+
*)
|
|
106
|
+
updated_input=$(jq -nc --arg cmd "${cmd} | head -${max_lines}" '{"command":$cmd}')
|
|
107
|
+
context+="Eagle Mem: '${base_cmd}' output is typically long (${reason}). Piped through head -${max_lines}."
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
fi
|
|
111
|
+
;;
|
|
112
|
+
summary)
|
|
113
|
+
context+="Eagle Mem: '${base_cmd}' is typically noisy (${reason}). Consider piping through tail or checking exit code only."
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
fi
|
|
117
|
+
;;
|
|
118
|
+
|
|
119
|
+
Read)
|
|
120
|
+
fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
|
121
|
+
if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
|
|
122
|
+
|
|
123
|
+
# ─── Read-after-modify detection ──────────────────────
|
|
124
|
+
mod_file="$EAGLE_MEM_DIR/mod-tracker/${session_id}"
|
|
125
|
+
if [ -f "$mod_file" ] && grep -qFx -- "$fp" "$mod_file" 2>/dev/null; then
|
|
126
|
+
context+="Eagle Mem: '$(basename "$fp")' was just edited/written — the diff is already in context from the tool output. "
|
|
127
|
+
fi
|
|
82
128
|
|
|
83
|
-
#
|
|
84
|
-
|
|
129
|
+
# ─── Read dedup tracker (soft nudge) ──────────────────
|
|
130
|
+
tracker_dir="$EAGLE_MEM_DIR/read-tracker"
|
|
131
|
+
mkdir -p "$tracker_dir" 2>/dev/null
|
|
132
|
+
tracker_file="$tracker_dir/${session_id}"
|
|
133
|
+
echo "$fp" >> "$tracker_file"
|
|
134
|
+
read_count=$(grep -cFx -- "$fp" "$tracker_file" 2>/dev/null)
|
|
135
|
+
read_count=${read_count:-0}
|
|
136
|
+
if [ "$read_count" -ge 3 ]; then
|
|
137
|
+
context+="Eagle Mem: '$(basename "$fp")' has been read ${read_count} times this session. Its contents are likely already in context."
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
85
142
|
|
|
86
|
-
|
|
143
|
+
[ -z "$context" ] && [ -z "$updated_input" ] && exit 0
|
|
87
144
|
|
|
88
|
-
if [ -n "$
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
;;
|
|
94
|
-
truncate)
|
|
95
|
-
if [ -n "$max_lines" ] && [ "$max_lines" -gt 0 ] 2>/dev/null; then
|
|
96
|
-
context+="Eagle Mem command hint: '${base_cmd}' produces long output (${reason}). Consider: ${cmd} | head -${max_lines}"
|
|
97
|
-
fi
|
|
98
|
-
;;
|
|
99
|
-
esac
|
|
145
|
+
if [ -n "$updated_input" ]; then
|
|
146
|
+
jq -nc --arg ctx "$context" --argjson ui "$updated_input" \
|
|
147
|
+
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":$ui,"additionalContext":$ctx}}'
|
|
148
|
+
else
|
|
149
|
+
jq -nc --arg ctx "$context" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$ctx}}'
|
|
100
150
|
fi
|
|
101
151
|
|
|
102
|
-
[ -z "$context" ] && exit 0
|
|
103
|
-
|
|
104
|
-
jq -nc --arg ctx "$context" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$ctx}}'
|
|
105
|
-
|
|
106
152
|
exit 0
|
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,29 @@ 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
|
+
|
|
64
|
+
# ─── Cleanup stale tracker files (non-blocking) ─────────────
|
|
65
|
+
find "$EAGLE_MEM_DIR/read-tracker" -type f -mtime +1 -delete 2>/dev/null &
|
|
66
|
+
find "$EAGLE_MEM_DIR/mod-tracker" -type f -mtime +1 -delete 2>/dev/null &
|
|
67
|
+
|
|
43
68
|
# ─── Version check (non-blocking) ────────────────────────────
|
|
44
69
|
|
|
45
70
|
update_notice=""
|
|
@@ -127,38 +152,25 @@ eagle_banner="======================================
|
|
|
127
152
|
======================================"
|
|
128
153
|
|
|
129
154
|
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
155
|
"
|
|
135
156
|
|
|
136
157
|
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
|
-
|
|
158
|
+
context+="
|
|
159
|
+
=== $update_notice ===
|
|
148
160
|
"
|
|
149
161
|
fi
|
|
150
162
|
|
|
151
163
|
# Project overview
|
|
152
164
|
overview=$(eagle_get_overview "$project")
|
|
153
165
|
if [ -n "$overview" ]; then
|
|
154
|
-
context+="
|
|
166
|
+
context+="
|
|
167
|
+
=== Project Overview ===
|
|
155
168
|
$overview
|
|
156
|
-
|
|
157
169
|
"
|
|
158
170
|
else
|
|
159
|
-
context+="
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
context+="
|
|
172
|
+
=== Action Required ===
|
|
173
|
+
No overview exists for '$project'. Run /eagle-mem-overview to build one.
|
|
162
174
|
"
|
|
163
175
|
fi
|
|
164
176
|
|
|
@@ -166,8 +178,8 @@ fi
|
|
|
166
178
|
recent=$(eagle_get_recent_summaries "$project" 5)
|
|
167
179
|
|
|
168
180
|
if [ -n "$recent" ]; then
|
|
169
|
-
context+="
|
|
170
|
-
Recent
|
|
181
|
+
context+="
|
|
182
|
+
=== Recent Sessions ===
|
|
171
183
|
"
|
|
172
184
|
while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
|
|
173
185
|
[ -z "$request" ] && [ -z "$completed" ] && continue
|
|
@@ -202,8 +214,7 @@ memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, up
|
|
|
202
214
|
LIMIT 5;")
|
|
203
215
|
if [ -n "$memories" ]; then
|
|
204
216
|
context+="
|
|
205
|
-
===
|
|
206
|
-
Recent memories for '$project':
|
|
217
|
+
=== Memories ===
|
|
207
218
|
"
|
|
208
219
|
while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
|
|
209
220
|
[ -z "$mname" ] && continue
|
|
@@ -227,8 +238,7 @@ fi
|
|
|
227
238
|
plans=$(eagle_list_claude_plans "$project" 3)
|
|
228
239
|
if [ -n "$plans" ]; then
|
|
229
240
|
context+="
|
|
230
|
-
===
|
|
231
|
-
Recent plans for '$project':
|
|
241
|
+
=== Plans ===
|
|
232
242
|
"
|
|
233
243
|
while IFS='|' read -r ptitle _pproj _fpath _updated; do
|
|
234
244
|
[ -z "$ptitle" ] && continue
|
|
@@ -249,8 +259,7 @@ synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
|
|
|
249
259
|
LIMIT 10;")
|
|
250
260
|
if [ -n "$synced_tasks" ]; then
|
|
251
261
|
context+="
|
|
252
|
-
===
|
|
253
|
-
Tasks for '$project':
|
|
262
|
+
=== Tasks ===
|
|
254
263
|
"
|
|
255
264
|
while IFS='|' read -r tsubject tstatus tblocked; do
|
|
256
265
|
[ -z "$tsubject" ] && continue
|
|
@@ -263,72 +272,37 @@ Tasks for '$project':
|
|
|
263
272
|
done <<< "$synced_tasks"
|
|
264
273
|
fi
|
|
265
274
|
|
|
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:
|
|
275
|
+
# ─── Instructions (full on startup, minimal on compact) ──
|
|
272
276
|
|
|
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>
|
|
277
|
+
if [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
|
|
278
|
+
context+="
|
|
279
|
+
=== Eagle Mem (compact reload) ===
|
|
280
|
+
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.
|
|
281
|
+
"
|
|
282
|
+
else
|
|
283
|
+
context+="
|
|
284
|
+
=== Eagle Mem ===
|
|
285
|
+
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
286
|
|
|
308
|
-
|
|
287
|
+
Emit an <eagle-summary> block before your FINAL response:
|
|
309
288
|
|
|
310
289
|
<eagle-summary>
|
|
311
|
-
request:
|
|
312
|
-
investigated:
|
|
313
|
-
learned:
|
|
314
|
-
completed:
|
|
315
|
-
next_steps:
|
|
290
|
+
request: [what the user asked for]
|
|
291
|
+
investigated: [file paths read or explored]
|
|
292
|
+
learned: [non-obvious discoveries a future session couldn't guess from code]
|
|
293
|
+
completed: [what shipped — be specific]
|
|
294
|
+
next_steps: [concrete actions for next session]
|
|
316
295
|
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.
|
|
296
|
+
- [choice] Why: [reason]
|
|
319
297
|
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.
|
|
298
|
+
- [what failed or surprised — \"X doesn't work because Y\"]
|
|
322
299
|
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]
|
|
300
|
+
- [path] — [role in this work]
|
|
301
|
+
files_read: [file1, file2]
|
|
302
|
+
files_modified: [file1, file2]
|
|
328
303
|
</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
304
|
"
|
|
305
|
+
fi
|
|
332
306
|
|
|
333
307
|
# Output context (plain text stdout = additionalContext for SessionStart)
|
|
334
308
|
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,10 @@ 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
|
+
LENGTH(pattern) DESC
|
|
56
57
|
LIMIT 1;"
|
|
57
58
|
}
|
|
58
59
|
|
package/lib/db-sessions.sh
CHANGED
|
@@ -15,9 +15,9 @@ eagle_upsert_session() {
|
|
|
15
15
|
eagle_db "INSERT INTO sessions (id, project, cwd, model, source, last_activity_at)
|
|
16
16
|
VALUES ('$session_id', '$project', '$cwd', '$model', '$source', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
17
17
|
ON CONFLICT(id) DO UPDATE SET
|
|
18
|
-
cwd = COALESCE(excluded.cwd, sessions.cwd),
|
|
19
|
-
model = COALESCE(excluded.model, sessions.model),
|
|
20
|
-
source = COALESCE(excluded.source, sessions.source),
|
|
18
|
+
cwd = COALESCE(NULLIF(excluded.cwd, ''), sessions.cwd),
|
|
19
|
+
model = COALESCE(NULLIF(excluded.model, ''), sessions.model),
|
|
20
|
+
source = COALESCE(NULLIF(excluded.source, ''), sessions.source),
|
|
21
21
|
status = 'active',
|
|
22
22
|
last_activity_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
23
23
|
}
|
package/lib/hooks.sh
CHANGED
|
@@ -11,7 +11,15 @@ eagle_patch_hook() {
|
|
|
11
11
|
local command="$4"
|
|
12
12
|
local description="${5:-}"
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# Check both command AND matcher to avoid skipping entries with different matchers
|
|
15
|
+
# (e.g. PreToolUse with "Bash" vs "Read" matcher using the same script)
|
|
16
|
+
local match_query
|
|
17
|
+
if [ -n "$matcher" ]; then
|
|
18
|
+
match_query=".hooks.${event}[]? | select(.matcher == \"$matcher\" and (.hooks[]?.command == \"$command\"))"
|
|
19
|
+
else
|
|
20
|
+
match_query=".hooks.${event}[]? | select(.matcher == null and (.hooks[]?.command == \"$command\"))"
|
|
21
|
+
fi
|
|
22
|
+
if jq -e "$match_query" "$settings" &>/dev/null; then
|
|
15
23
|
[ -n "$description" ] && eagle_ok "$description ${DIM}(already registered)${RESET}"
|
|
16
24
|
return 0
|
|
17
25
|
fi
|
package/package.json
CHANGED
package/scripts/curate.sh
CHANGED
|
@@ -222,6 +222,13 @@ If no rules needed, output: NONE"
|
|
|
222
222
|
eagle_log "WARN" "Curator: skipping RULE with invalid strategy '$strategy'"
|
|
223
223
|
continue
|
|
224
224
|
;; esac
|
|
225
|
+
# Guard: reject dangerous LLM-generated patterns that match everything
|
|
226
|
+
# Require at least 2 literal characters (not just wildcards)
|
|
227
|
+
_literal_chars=$(printf '%s' "$pattern" | sed 's/[%_]//g')
|
|
228
|
+
if [ ${#_literal_chars} -lt 2 ]; then
|
|
229
|
+
eagle_log "WARN" "Curator: skipping overly broad pattern '$pattern' (needs >=2 literal chars)"
|
|
230
|
+
continue
|
|
231
|
+
fi
|
|
225
232
|
|
|
226
233
|
[ "$max_lines" = "-" ] && max_lines=""
|
|
227
234
|
|
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
|
package/scripts/install.sh
CHANGED
|
@@ -186,7 +186,11 @@ eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" \
|
|
|
186
186
|
|
|
187
187
|
eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash" \
|
|
188
188
|
"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
|
|
189
|
-
"PreToolUse hook"
|
|
189
|
+
"PreToolUse hook (Bash)"
|
|
190
|
+
|
|
191
|
+
eagle_patch_hook "$SETTINGS" "PreToolUse" "Read" \
|
|
192
|
+
"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
|
|
193
|
+
"PreToolUse hook (Read)"
|
|
190
194
|
|
|
191
195
|
# ─── Install skills ────────────────────────────────────────
|
|
192
196
|
|
package/scripts/update.sh
CHANGED
|
@@ -76,6 +76,7 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
|
|
|
76
76
|
eagle_patch_hook "$SETTINGS" "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
|
|
77
77
|
eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
|
|
78
78
|
eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
|
|
79
|
+
eagle_patch_hook "$SETTINGS" "PreToolUse" "Read" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
|
|
79
80
|
|
|
80
81
|
eagle_ok "Hooks registered"
|
|
81
82
|
fi
|