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.
@@ -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);
@@ -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 + triggers auto-curate
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
@@ -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+="=== EAGLE MEM — $update_notice ===
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+="=== EAGLE MEM — Project Overview ===
162
+ context+="
163
+ === Project Overview ===
155
164
  $overview
156
-
157
165
  "
158
166
  else
159
- context+="=== EAGLE MEM — Action Required ===
160
- No overview exists for '$project'. On the user's first prompt, run /eagle-mem-overview to build a structured project briefing. The skill has full instructions for what to read and how to write a rich overview.
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+="=== EAGLE MEM ===
170
- Recent sessions for project '$project':
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
- === EAGLE MEM — Memories ===
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
- === EAGLE MEM — Plans ===
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
- === EAGLE MEM — Tasks ===
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
- # Emit the eagle-summary instruction
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
- $eagle_banner
275
- \`\`\`
276
-
277
- This gives the user visibility into the full context Eagle Mem loaded for this session.
278
-
279
- ANTI-REGRESSION: When Eagle Mem surfaces decision history about a file you are reading (via PostToolUse context), those decisions were made deliberately in past sessions. Do NOT revert or change the implementation approach without explicit user request. If you believe a past decision should change, state why and ask the user before proceeding. This prevents the common regression where Claude 'improves' code back to an older approach that was already rejected.
280
-
281
- SECRET SAFETY: Never include raw API keys, tokens, passwords, or secrets in eagle-summary fields or any text that Eagle Mem stores. Reference secrets by name (e.g., 'the Stripe API key', 'GOOGLE_APPLICATION_CREDENTIALS_JSON') not by value. Eagle Mem redacts common patterns automatically, but prevention is better than redaction.
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
- EXAMPLE this is what a well-written summary looks like:
283
+ Emit an <eagle-summary> block before your FINAL response:
309
284
 
310
285
  <eagle-summary>
311
- request: Add JWT authentication middleware to the API
312
- investigated: src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts
313
- learned: express-jwt v8 changed its API — req.auth replaces req.user. The error handler must check err.name === 'UnauthorizedError', not err.status === 401.
314
- completed: JWT middleware deployed on all /api routes. Token validation, role-based guards, and 401/403 error responses all working. Added JWKS endpoint support for key rotation.
315
- next_steps: Add refresh token rotation; rate-limit the /auth/token endpoint
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
- - Chose RS256 over HS256 for JWT signing. Why: allows key rotation via JWKS without redeploying; HS256 requires shared secret on every service.
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
- - express-jwt v8 is ESM-onlyrequire() fails silently and returns undefined. Must use dynamic import().
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
- - src/middleware/auth.tsJWT validation + role guard middleware
324
- - src/config/env.ts — JWKS_URI and JWT_ISSUER environment config
325
- - src/routes/users.ts — first route to use the new auth guard (reference implementation)
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
- # Parses <eagle-summary> from transcript, saves to DB
6
- # Falls back to heuristic extraction if no summary block
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
- # ─── Try to parse <eagle-summary> from transcript ──────────
41
+ # ─── Guard: skip if summary already exists for this session ──
42
+ # Stop fires every assistant turn. Only process once per session.
41
43
 
42
- summary_block=""
43
- if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
44
- # Extract all text from the last assistant message using jq on JSONL
45
- # Real transcript format: top-level .type == "assistant", content in .message.content[]
46
- text_content=$(jq -rs '
47
- [.[] | select(.type == "assistant")] | last |
48
- if . then
49
- [.message.content[]? | select(.type == "text") | .text] | join("\n")
50
- else "" end
51
- ' "$transcript_path" 2>/dev/null)
52
-
53
- # Strip <private>...</private> blocks before processing (case-insensitive, tolerates attributes/whitespace)
54
- text_content=$(echo "$text_content" | sed -E '/<[Pp][Rr][Ii][Vv][Aa][Tt][Ee][^>]*>/,/<\/[Pp][Rr][Ii][Vv][Aa][Tt][Ee][[:space:]]*>/d')
55
-
56
- # Parse <eagle-summary> block
57
- if [ -n "$text_content" ] && echo "$text_content" | grep -q '<eagle-summary>' 2>/dev/null; then
58
- summary_block=$(echo "$text_content" | sed -n '/<eagle-summary>/,/<\/eagle-summary>/p' | sed '1d;$d')
59
- fi
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
- if [ -n "$summary_block" ]; then
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
- raw_fr=$(parse_field "$summary_block" "files_read")
101
- raw_fm=$(parse_field "$summary_block" "files_modified")
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
- # Convert bracket-list to JSON array (handles special chars safely)
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: parsed eagle-summary block"
133
+ eagle_log "INFO" "Stop: eagle-summary block merged over heuristic data"
114
134
  fi
115
135
 
116
- # ─── Guard: skip fallback work if summary already exists ──
117
- # Stop fires every assistant turn. Without this, the heuristic and LLM
118
- # enrichment blocks fire on turn 2+ — wasting tokens and producing
119
- # empty inserts that get rejected.
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
- if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
131
- eagle_log "INFO" "Stop: no eagle-summary found, using heuristic fallback"
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
- 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)
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
- enrich_result=$(eagle_llm_call "$enrich_prompt" "Extract structured facts from development sessions. Be concise. Only include items with clear evidence." 512 2>/dev/null) || true
172
-
173
- if [ -n "$enrich_result" ]; then
174
- extract_section() {
175
- local result="$1" header="$2"
176
- echo "$result" | awk -v h="$header:" '
177
- $0 == h || $0 ~ "^"h { found=1; next }
178
- found && /^[A-Z_]+:/ { exit }
179
- found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
180
- END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
181
- '
182
- }
183
- decisions=$(extract_section "$enrich_result" "DECISIONS")
184
- gotchas=$(extract_section "$enrich_result" "GOTCHAS")
185
- key_files=$(extract_section "$enrich_result" "KEY_FILES")
186
- [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
187
- fi
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
- # Resolve /private/tmp → /tmp on macOS
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
  }
@@ -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 IS NULL)
53
+ AND (project = '$project' OR project = '')
54
54
  AND ('$cmd' LIKE pattern OR '$cmd' = pattern)
55
- ORDER BY CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END
55
+ ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END
56
56
  LIMIT 1;"
57
57
  }
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Persistent memory for Claude Code — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/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
- # ─── 1. Summary enrichment rate ──────────────────────────
51
+ # ─��─ 1. Summary capture rate (25 pts) ───────────────────
53
52
 
54
- max_score=$((max_score + 30))
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 [ "${total_summaries:-0}" -eq 0 ]; then
60
- enrich_pct=0
60
+ if [ "${total_sessions:-0}" -eq 0 ]; then
61
+ capture_pct=0
61
62
  else
62
- enrich_pct=$((enriched_summaries * 100 / total_summaries))
63
+ capture_pct=$((total_summaries * 100 / total_sessions))
63
64
  fi
64
65
 
65
- if [ "$enrich_pct" -ge 50 ]; then
66
- eagle_ok "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%)"
67
- score=$((score + 30))
68
- elif [ "$enrich_pct" -ge 20 ]; then
69
- eagle_warn "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — aim for 50%+"
70
- score=$((score + 15))
71
- issues+=("Low enrichment rate (${enrich_pct}%). Eagle-summary blocks aren't being emitted reliably.")
72
- elif [ "${total_summaries:-0}" -gt 0 ]; then
73
- eagle_fail "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — self-learning not working"
74
- issues+=("Critical: ${enrich_pct}% enrichment. Decisions/gotchas/key_files are not being captured.")
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. Feature discovery ────────────────────────────────
83
+ # ─── 2. Enrichment rate (25 pts) ────────────────────────
80
84
 
81
- max_score=$((max_score + 20))
85
+ max_score=$((max_score + 25))
82
86
 
83
- feature_count=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
84
- feature_file_count=$(eagle_db "SELECT COUNT(*) FROM feature_files ff JOIN features f ON ff.feature_id = f.id WHERE f.project = '$p_esc' AND f.status = 'active';")
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 [ "${feature_count:-0}" -ge 3 ]; then
87
- eagle_ok "Features tracked: ${feature_count} (${feature_file_count} files mapped)"
88
- score=$((score + 20))
89
- elif [ "${feature_count:-0}" -ge 1 ]; then
90
- eagle_warn "Features tracked: ${feature_count} curator needs more sessions"
91
- score=$((score + 10))
92
- issues+=("Only ${feature_count} features discovered. Run curator more often.")
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
- eagle_fail "Features tracked: 0 — feature graph is empty"
95
- issues+=("No features discovered. Run: eagle-mem curate")
110
+ eagle_dim " No summaries to enrich"
96
111
  fi
97
112
 
98
- # ─── 3. Command intelligence ─────────────────────────────
113
+ # ─── 3. Feature discovery (15 pts) ─────────────────────
99
114
 
100
115
  max_score=$((max_score + 15))
101
116
 
102
- rule_count=$(eagle_db "SELECT COUNT(*) FROM command_rules WHERE (project = '$p_esc' OR project IS NULL) AND enabled = 1;")
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 [ "${rule_count:-0}" -ge 2 ]; then
106
- eagle_ok "Command rules: ${rule_count} active (${obs_with_metrics} observations with metrics)"
119
+ if [ "${feature_count:-0}" -ge 3 ]; then
120
+ eagle_ok "Features: ${feature_count} tracked"
107
121
  score=$((score + 15))
108
- elif [ "${rule_count:-0}" -ge 1 ]; then
109
- eagle_warn "Command rules: ${rule_count} — learning in progress"
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
- eagle_dim " Command metrics: ${obs_with_metrics} observations (need more data)"
116
- score=$((score + 5))
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 "LLM provider: ${provider} (${model})"
137
+ eagle_ok "Provider: ${provider} (${model})"
127
138
  score=$((score + 15))
128
139
  else
129
- eagle_fail "No LLM provider — curator and enrichment extraction disabled"
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. Project data quality ─────────────────────────────
144
+ # ─── 5. Data quality (10 pts) ──────────────────────────
134
145
 
135
146
  max_score=$((max_score + 10))
136
147
 
137
- tmp_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project IN ('tmp', 'private', '');")
138
- total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions;")
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 [ "${total_sessions:-0}" -eq 0 ]; then
152
+ if [ "${all_sessions:-0}" -eq 0 ]; then
141
153
  noise_pct=0
142
154
  else
143
- noise_pct=$((tmp_sessions * 100 / total_sessions))
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: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
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: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
162
+ eagle_warn "Data quality: ${noise_pct}% noise (${noise_sessions}/${all_sessions})"
151
163
  score=$((score + 5))
152
- issues+=("${noise_pct}% of sessions from ephemeral dirs. Skiplist should prevent new ones.")
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 ${tmp_sessions}/${total_sessions} sessions from ephemeral dirs"
155
- issues+=("Heavy ephemeral pollution. Update Eagle Mem to get skiplist protection.")
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 (project = '$p_esc' OR project IS NULL) ORDER BY CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END LIMIT 1;" 2>/dev/null || echo "")
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 but hasn't run yet"
181
+ eagle_warn "Curator: auto-scheduled, hasn't run yet (triggers on session start)"
170
182
  score=$((score + 5))
171
- issues+=("Auto-curate is configured but hasn't run. It triggers at session end.")
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+=("Curator has never run. Try: eagle-mem curate --dry-run")
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}─────────────────────────────────────${RESET}"
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
- enrichment:{total:$total_summaries, enriched:$enriched_summaries},
221
- features:$features, command_rules:$command_rules,
222
- provider:$provider, noise_pct:$noise_pct}' >&3
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