eagle-mem 4.6.1 → 4.7.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/lib/db-mirrors.sh CHANGED
@@ -9,6 +9,7 @@ eagle_capture_claude_memory() {
9
9
  local file_path="$1"
10
10
  local session_id="${2:-}"
11
11
  local project="${3:-}"
12
+ local agent="${4:-$(eagle_agent_source)}"
12
13
 
13
14
  [ ! -f "$file_path" ] && return 0
14
15
 
@@ -28,7 +29,7 @@ eagle_capture_claude_memory() {
28
29
  morigin=$(_fm_field "originSessionId")
29
30
  [ -z "$morigin" ] && morigin="$session_id"
30
31
 
31
- local fp_sql proj_sql name_sql desc_sql type_sql content_sql hash_sql origin_sql
32
+ local fp_sql proj_sql name_sql desc_sql type_sql content_sql hash_sql origin_sql agent_sql
32
33
  fp_sql=$(eagle_sql_escape "$file_path")
33
34
  proj_sql=$(eagle_sql_escape "$project")
34
35
  name_sql=$(eagle_sql_escape "$mname")
@@ -37,10 +38,11 @@ eagle_capture_claude_memory() {
37
38
  content_sql=$(eagle_sql_escape "$body")
38
39
  hash_sql=$(eagle_sql_escape "$chash")
39
40
  origin_sql=$(eagle_sql_escape "$morigin")
41
+ agent_sql=$(eagle_sql_escape "$agent")
40
42
 
41
43
  eagle_db_pipe <<SQL
42
- INSERT INTO claude_memories (project, file_path, memory_name, description, memory_type, content, content_hash, origin_session_id)
43
- VALUES ('$proj_sql', '$fp_sql', '$name_sql', '$desc_sql', '$type_sql', '$content_sql', '$hash_sql', '$origin_sql')
44
+ INSERT INTO claude_memories (project, file_path, memory_name, description, memory_type, content, content_hash, origin_session_id, origin_agent)
45
+ VALUES ('$proj_sql', '$fp_sql', '$name_sql', '$desc_sql', '$type_sql', '$content_sql', '$hash_sql', '$origin_sql', '$agent_sql')
44
46
  ON CONFLICT(file_path) DO UPDATE SET
45
47
  memory_name = excluded.memory_name,
46
48
  description = excluded.description,
@@ -48,6 +50,7 @@ ON CONFLICT(file_path) DO UPDATE SET
48
50
  content = excluded.content,
49
51
  content_hash = excluded.content_hash,
50
52
  origin_session_id = excluded.origin_session_id,
53
+ origin_agent = excluded.origin_agent,
51
54
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
52
55
  WHERE claude_memories.content_hash != excluded.content_hash;
53
56
  SQL
@@ -71,7 +74,7 @@ eagle_search_claude_memories() {
71
74
 
72
75
  eagle_db "SELECT m.memory_name, m.memory_type, m.description,
73
76
  replace(substr(m.content, 1, 200), char(10), ' '),
74
- m.file_path, m.updated_at
77
+ m.file_path, m.updated_at, m.origin_agent
75
78
  FROM claude_memories m
76
79
  JOIN claude_memories_fts f ON f.rowid = m.id
77
80
  WHERE claude_memories_fts MATCH '$query'
@@ -90,7 +93,7 @@ eagle_list_claude_memories() {
90
93
  where_clause="WHERE project = '$project'"
91
94
  fi
92
95
 
93
- eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at
96
+ eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at, origin_agent
94
97
  FROM claude_memories
95
98
  $where_clause
96
99
  ORDER BY updated_at DESC
@@ -101,6 +104,7 @@ eagle_capture_claude_plan() {
101
104
  local file_path="$1"
102
105
  local session_id="${2:-}"
103
106
  local project="${3:-}"
107
+ local agent="${4:-$(eagle_agent_source)}"
104
108
 
105
109
  [ ! -f "$file_path" ] && return 0
106
110
 
@@ -111,22 +115,24 @@ eagle_capture_claude_plan() {
111
115
  title=$(awk '/^# /{print; exit}' "$file_path" | sed 's/^# //')
112
116
  content=$(cat "$file_path")
113
117
 
114
- local fp_sql proj_sql title_sql content_sql hash_sql origin_sql
118
+ local fp_sql proj_sql title_sql content_sql hash_sql origin_sql agent_sql
115
119
  fp_sql=$(eagle_sql_escape "$file_path")
116
120
  proj_sql=$(eagle_sql_escape "$project")
117
121
  title_sql=$(eagle_sql_escape "$title")
118
122
  content_sql=$(eagle_sql_escape "$content")
119
123
  hash_sql=$(eagle_sql_escape "$chash")
120
124
  origin_sql=$(eagle_sql_escape "$session_id")
125
+ agent_sql=$(eagle_sql_escape "$agent")
121
126
 
122
127
  eagle_db_pipe <<SQL
123
- INSERT INTO claude_plans (project, file_path, title, content, content_hash, origin_session_id)
124
- VALUES ('$proj_sql', '$fp_sql', '$title_sql', '$content_sql', '$hash_sql', '$origin_sql')
128
+ INSERT INTO claude_plans (project, file_path, title, content, content_hash, origin_session_id, origin_agent)
129
+ VALUES ('$proj_sql', '$fp_sql', '$title_sql', '$content_sql', '$hash_sql', '$origin_sql', '$agent_sql')
125
130
  ON CONFLICT(file_path) DO UPDATE SET
126
131
  title = excluded.title,
127
132
  content = excluded.content,
128
133
  content_hash = excluded.content_hash,
129
134
  origin_session_id = COALESCE(NULLIF(excluded.origin_session_id, ''), claude_plans.origin_session_id),
135
+ origin_agent = COALESCE(NULLIF(excluded.origin_agent, ''), claude_plans.origin_agent),
130
136
  project = CASE WHEN excluded.project != '' THEN excluded.project ELSE claude_plans.project END,
131
137
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
132
138
  WHERE claude_plans.content_hash != excluded.content_hash;
@@ -151,7 +157,7 @@ eagle_search_claude_plans() {
151
157
 
152
158
  eagle_db "SELECT p.title, p.project,
153
159
  replace(substr(p.content, 1, 200), char(10), ' '),
154
- p.file_path, p.updated_at
160
+ p.file_path, p.updated_at, p.origin_agent
155
161
  FROM claude_plans p
156
162
  JOIN claude_plans_fts f ON f.rowid = p.id
157
163
  WHERE claude_plans_fts MATCH '$query'
@@ -170,7 +176,7 @@ eagle_list_claude_plans() {
170
176
  where_clause="WHERE project = '$project'"
171
177
  fi
172
178
 
173
- eagle_db "SELECT title, project, file_path, updated_at
179
+ eagle_db "SELECT title, project, file_path, updated_at, origin_agent
174
180
  FROM claude_plans
175
181
  $where_clause
176
182
  ORDER BY updated_at DESC
@@ -181,6 +187,7 @@ eagle_capture_claude_task() {
181
187
  local file_path="$1"
182
188
  local session_id="${2:-}"
183
189
  local project="${3:-}"
190
+ local agent="${4:-$(eagle_agent_source)}"
184
191
 
185
192
  [ ! -f "$file_path" ] && return 0
186
193
 
@@ -201,7 +208,7 @@ eagle_capture_claude_task() {
201
208
 
202
209
  [ -z "$task_id" ] && return 0
203
210
 
204
- local fp_sql proj_sql sid_sql tid_sql subj_sql desc_sql af_sql status_sql blocks_sql bb_sql hash_sql
211
+ local fp_sql proj_sql sid_sql tid_sql subj_sql desc_sql af_sql status_sql blocks_sql bb_sql hash_sql agent_sql
205
212
  fp_sql=$(eagle_sql_escape "$file_path")
206
213
  proj_sql=$(eagle_sql_escape "$project")
207
214
  sid_sql=$(eagle_sql_escape "$session_id")
@@ -213,10 +220,11 @@ eagle_capture_claude_task() {
213
220
  blocks_sql=$(eagle_sql_escape "$blocks")
214
221
  bb_sql=$(eagle_sql_escape "$blocked_by")
215
222
  hash_sql=$(eagle_sql_escape "$chash")
223
+ agent_sql=$(eagle_sql_escape "$agent")
216
224
 
217
225
  eagle_db_pipe <<SQL
218
- INSERT INTO claude_tasks (project, source_session_id, source_task_id, file_path, subject, description, active_form, status, blocks, blocked_by, content_hash)
219
- VALUES ('$proj_sql', '$sid_sql', '$tid_sql', '$fp_sql', '$subj_sql', '$desc_sql', '$af_sql', '$status_sql', '$blocks_sql', '$bb_sql', '$hash_sql')
226
+ INSERT INTO claude_tasks (project, source_session_id, source_task_id, file_path, subject, description, active_form, status, blocks, blocked_by, content_hash, origin_agent)
227
+ VALUES ('$proj_sql', '$sid_sql', '$tid_sql', '$fp_sql', '$subj_sql', '$desc_sql', '$af_sql', '$status_sql', '$blocks_sql', '$bb_sql', '$hash_sql', '$agent_sql')
220
228
  ON CONFLICT(file_path) DO UPDATE SET
221
229
  subject = excluded.subject,
222
230
  description = excluded.description,
@@ -225,6 +233,7 @@ ON CONFLICT(file_path) DO UPDATE SET
225
233
  blocks = excluded.blocks,
226
234
  blocked_by = excluded.blocked_by,
227
235
  content_hash = excluded.content_hash,
236
+ origin_agent = excluded.origin_agent,
228
237
  project = CASE WHEN excluded.project != '' THEN excluded.project ELSE claude_tasks.project END,
229
238
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
230
239
  WHERE claude_tasks.content_hash != excluded.content_hash;
@@ -241,7 +250,7 @@ eagle_list_claude_tasks() {
241
250
  where_clause="WHERE project = '$project'"
242
251
  fi
243
252
 
244
- eagle_db "SELECT subject, status, source_session_id, source_task_id, updated_at
253
+ eagle_db "SELECT subject, status, source_session_id, source_task_id, updated_at, origin_agent
245
254
  FROM claude_tasks
246
255
  $where_clause
247
256
  ORDER BY updated_at DESC
@@ -266,7 +275,7 @@ eagle_search_claude_tasks() {
266
275
 
267
276
  eagle_db "SELECT t.subject, t.status,
268
277
  replace(substr(t.description, 1, 200), char(10), ' '),
269
- t.source_session_id, t.source_task_id, t.updated_at
278
+ t.source_session_id, t.source_task_id, t.updated_at, t.origin_agent
270
279
  FROM claude_tasks t
271
280
  JOIN claude_tasks_fts f ON f.rowid = t.id
272
281
  WHERE claude_tasks_fts MATCH '$query'
@@ -15,6 +15,7 @@ eagle_insert_observation() {
15
15
  local output_bytes="${7:-}"
16
16
  local output_lines="${8:-}"
17
17
  local command_category; command_category=$(eagle_sql_escape "${9:-}")
18
+ local agent; agent=$(eagle_sql_escape "${10:-$(eagle_agent_source)}")
18
19
 
19
20
  local extra_cols=""
20
21
  local extra_vals=""
@@ -23,8 +24,8 @@ eagle_insert_observation() {
23
24
  extra_vals=", $(eagle_sql_int "$output_bytes"), $(eagle_sql_int "$output_lines"), '$command_category'"
24
25
  fi
25
26
 
26
- eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified${extra_cols})
27
- SELECT '$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'${extra_vals}
27
+ eagle_db "INSERT INTO observations (session_id, project, agent, tool_name, tool_input_summary, files_read, files_modified${extra_cols})
28
+ SELECT '$session_id', '$project', '$agent', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'${extra_vals}
28
29
  WHERE NOT EXISTS (
29
30
  SELECT 1 FROM observations
30
31
  WHERE session_id = '$session_id'
@@ -11,13 +11,15 @@ eagle_upsert_session() {
11
11
  local cwd; cwd=$(eagle_sql_escape "${3:-}")
12
12
  local model; model=$(eagle_sql_escape "${4:-}")
13
13
  local source; source=$(eagle_sql_escape "${5:-}")
14
+ local agent; agent=$(eagle_sql_escape "${6:-$(eagle_agent_source)}")
14
15
 
15
- eagle_db "INSERT INTO sessions (id, project, cwd, model, source, last_activity_at)
16
- VALUES ('$session_id', '$project', '$cwd', '$model', '$source', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
16
+ eagle_db "INSERT INTO sessions (id, project, cwd, model, source, agent, last_activity_at)
17
+ VALUES ('$session_id', '$project', '$cwd', '$model', '$source', '$agent', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
17
18
  ON CONFLICT(id) DO UPDATE SET
18
19
  cwd = COALESCE(NULLIF(excluded.cwd, ''), sessions.cwd),
19
20
  model = COALESCE(NULLIF(excluded.model, ''), sessions.model),
20
21
  source = COALESCE(NULLIF(excluded.source, ''), sessions.source),
22
+ agent = COALESCE(NULLIF(excluded.agent, ''), sessions.agent),
21
23
  status = 'active',
22
24
  last_activity_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
23
25
  }
@@ -43,6 +45,8 @@ eagle_get_project_stats() {
43
45
  local project; project=$(eagle_sql_escape "$1")
44
46
  eagle_db_pipe <<SQL
45
47
  SELECT 'sessions|' || COUNT(*) FROM sessions WHERE project = '$project';
48
+ SELECT 'sessions_claude|' || COUNT(*) FROM sessions WHERE project = '$project' AND agent = 'claude-code';
49
+ SELECT 'sessions_codex|' || COUNT(*) FROM sessions WHERE project = '$project' AND agent = 'codex';
46
50
  SELECT 'summaries|' || COUNT(*) FROM summaries WHERE project = '$project';
47
51
  SELECT 'with_summaries|' || COUNT(*) FROM summaries WHERE project = '$project' AND request IS NOT NULL AND request != '';
48
52
  SELECT 'memories|' || COUNT(*) FROM claude_memories WHERE project = '$project';
@@ -19,12 +19,14 @@ eagle_insert_summary() {
19
19
  local decisions; decisions=$(eagle_sql_escape "${11:-}")
20
20
  local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
21
21
  local key_files; key_files=$(eagle_sql_escape "${13:-}")
22
+ local agent; agent=$(eagle_sql_escape "${14:-$(eagle_agent_source)}")
22
23
 
23
24
  eagle_db_pipe <<SQL
24
- INSERT INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files)
25
+ INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files)
25
26
  VALUES (
26
27
  '$session_id',
27
28
  '$project',
29
+ '$agent',
28
30
  '$request',
29
31
  '$investigated',
30
32
  '$learned',
@@ -39,6 +41,7 @@ VALUES (
39
41
  )
40
42
  ON CONFLICT(session_id) DO UPDATE SET
41
43
  project = excluded.project,
44
+ agent = COALESCE(NULLIF(excluded.agent, ''), summaries.agent),
42
45
  request = COALESCE(NULLIF(excluded.request, ''), summaries.request),
43
46
  investigated = COALESCE(NULLIF(excluded.investigated, ''), summaries.investigated),
44
47
  learned = COALESCE(NULLIF(excluded.learned, ''), summaries.learned),
@@ -57,7 +60,7 @@ eagle_get_recent_summaries() {
57
60
  local project; project=$(eagle_sql_escape "$1")
58
61
  local limit; limit=$(eagle_sql_int "${2:-5}")
59
62
 
60
- eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.decisions, s.gotchas, s.key_files
63
+ eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.decisions, s.gotchas, s.key_files, s.agent
61
64
  FROM summaries s
62
65
  WHERE s.project = '$project'
63
66
  AND s.request NOT LIKE '%<local-command-caveat>%'
@@ -77,7 +80,7 @@ eagle_search_summaries() {
77
80
  where_clause="AND s.project = '$project'"
78
81
  fi
79
82
 
80
- eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.project, s.decisions, s.gotchas, s.key_files
83
+ eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.project, s.decisions, s.gotchas, s.key_files, s.agent
81
84
  FROM summaries s
82
85
  JOIN summaries_fts f ON f.rowid = s.id
83
86
  WHERE summaries_fts MATCH '$query'
@@ -8,9 +8,10 @@ _EAGLE_HOOKS_POSTTOOL_LOADED=1
8
8
 
9
9
  eagle_posttool_mirror_writes() {
10
10
  local tool_name="$1" fp="$2" session_id="$3" project="$4"
11
+ local agent="${5:-$(eagle_agent_source)}"
11
12
 
12
13
  case "$tool_name" in
13
- Write|Edit)
14
+ Write|Edit|apply_patch)
14
15
  if [ -n "$fp" ]; then
15
16
  case "$fp" in
16
17
  *..*) ;; # path traversal — skip
@@ -18,12 +19,12 @@ eagle_posttool_mirror_writes() {
18
19
  local mem_base
19
20
  mem_base=$(basename "$fp")
20
21
  if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
21
- eagle_capture_claude_memory "$fp" "$session_id" "$project"
22
+ eagle_capture_claude_memory "$fp" "$session_id" "$project" "$agent"
22
23
  fi
23
24
  ;;
24
25
  "$EAGLE_CLAUDE_PLANS_DIR/"*.md)
25
26
  if [ -f "$fp" ]; then
26
- eagle_capture_claude_plan "$fp" "$session_id" "$project"
27
+ eagle_capture_claude_plan "$fp" "$session_id" "$project" "$agent"
27
28
  fi
28
29
  ;;
29
30
  esac
@@ -34,6 +35,7 @@ eagle_posttool_mirror_writes() {
34
35
 
35
36
  eagle_posttool_mirror_tasks() {
36
37
  local tool_name="$1" session_id="$2" project="$3" input="$4"
38
+ local agent="${5:-$(eagle_agent_source)}"
37
39
 
38
40
  case "$tool_name" in
39
41
  TaskCreate|TaskUpdate)
@@ -45,10 +47,10 @@ eagle_posttool_mirror_tasks() {
45
47
  if [ -z "$task_id" ]; then
46
48
  local newest
47
49
  newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
48
- [ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
50
+ [ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project" "$agent"
49
51
  elif eagle_validate_session_id "$task_id"; then
50
52
  local task_json="$task_dir/$task_id.json"
51
- [ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
53
+ [ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project" "$agent"
52
54
  fi
53
55
  fi
54
56
  fi
@@ -60,7 +62,7 @@ eagle_posttool_stale_hint() {
60
62
  local tool_name="$1" fp="$2" project="$3"
61
63
 
62
64
  case "$tool_name" in
63
- Write|Edit)
65
+ Write|Edit|apply_patch)
64
66
  if [ -n "$fp" ]; then
65
67
  local fname fname_stem
66
68
  fname=$(basename "$fp")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.6.1",
4
- "description": "Context that survives /compact for Claude Code — SQLite + FTS5, no daemon, no bloat",
3
+ "version": "4.7.0",
4
+ "description": "Context that survives /compact for Claude Code and Codex — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
7
7
  },
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "keywords": [
17
17
  "claude-code",
18
+ "codex",
18
19
  "memory",
19
20
  "sqlite",
20
21
  "fts5",
@@ -27,5 +28,8 @@
27
28
  "type": "git",
28
29
  "url": "git+https://github.com/eagleisbatman/eagle-mem.git"
29
30
  },
30
- "homepage": "https://github.com/eagleisbatman/eagle-mem#readme"
31
+ "homepage": "https://eagleisbatman.github.io/eagle-mem/",
32
+ "bugs": {
33
+ "url": "https://github.com/eagleisbatman/eagle-mem/issues"
34
+ }
31
35
  }
package/scripts/curate.sh CHANGED
@@ -398,46 +398,56 @@ if [ -n "$co_edit_data" ]; then
398
398
  eagle_info " Co-edit pairs found:"
399
399
  fi
400
400
 
401
- co_edit_count=0
402
- declare -A co_map
403
-
404
- while IFS='|' read -r f1 f2 co_sessions; do
405
- [ -z "$f1" ] || [ -z "$f2" ] && continue
406
-
407
- # Build comma-separated partner list per file (both directions)
408
- if [ -n "${co_map[$f1]+x}" ]; then
409
- co_map[$f1]="${co_map[$f1]},$f2"
410
- else
411
- co_map[$f1]="$f2"
412
- fi
413
- if [ -n "${co_map[$f2]+x}" ]; then
414
- co_map[$f2]="${co_map[$f2]},$f1"
415
- else
416
- co_map[$f2]="$f1"
417
- fi
418
- co_edit_count=$((co_edit_count + 1))
419
- done <<< "$co_edit_data"
401
+ # Use awk for associative map (Bash 3.2 compatible)
402
+ co_map_output=$(printf '%s\n' "$co_edit_data" | awk -F'|' '
403
+ $1 && $2 {
404
+ m[$1] = (m[$1] ? m[$1] "," : "") $2
405
+ m[$2] = (m[$2] ? m[$2] "," : "") $1
406
+ n++
407
+ }
408
+ END {
409
+ for (f in m) print f "|" m[f]
410
+ print "__co_edit_count__|" n "|" length(m) > "/dev/stderr"
411
+ }
412
+ ' 2>&1 1>/dev/null)
413
+ # stderr has the count line, stdout has the map lines — reverse:
414
+ co_map_output=$(printf '%s\n' "$co_edit_data" | awk -F'|' '
415
+ $1 && $2 {
416
+ m[$1] = (m[$1] ? m[$1] "," : "") $2
417
+ m[$2] = (m[$2] ? m[$2] "," : "") $1
418
+ n++
419
+ }
420
+ END {
421
+ for (f in m) print f "|" m[f]
422
+ printf "%s\n", "__META__|" n "|" length(m) > "/dev/stderr"
423
+ }
424
+ ' 2>/tmp/eagle_co_edit_meta)
425
+ co_edit_count=$(awk -F'|' '{print $2}' /tmp/eagle_co_edit_meta)
426
+ co_map_file_count=$(awk -F'|' '{print $3}' /tmp/eagle_co_edit_meta)
427
+ rm -f /tmp/eagle_co_edit_meta
420
428
 
421
429
  if [ "$DRY_RUN" -eq 1 ]; then
422
- for f in "${!co_map[@]}"; do
423
- eagle_info " $(basename "$f") ${co_map[$f]}"
430
+ printf '%s\n' "$co_map_output" | while IFS='|' read -r f partners; do
431
+ [ -z "$f" ] && continue
432
+ eagle_info " $(basename "$f") → $partners"
424
433
  done
425
434
  else
426
435
  {
427
436
  echo "BEGIN;"
428
437
  echo "DELETE FROM file_hints WHERE project = '$(eagle_sql_escape "$project")' AND hint_type = 'co_edit';"
429
- for f in "${!co_map[@]}"; do
438
+ printf '%s\n' "$co_map_output" | while IFS='|' read -r f partners; do
439
+ [ -z "$f" ] && continue
430
440
  local_f=$(eagle_sql_escape "$f")
431
- local_v=$(eagle_sql_escape "${co_map[$f]}")
441
+ local_v=$(eagle_sql_escape "$partners")
432
442
  echo "INSERT INTO file_hints (project, hint_type, file_path, hint_value) VALUES ('$(eagle_sql_escape "$project")', 'co_edit', '$local_f', '$local_v') ON CONFLICT(project, hint_type, file_path) DO UPDATE SET hint_value = excluded.hint_value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
433
443
  done
434
444
  echo "COMMIT;"
435
445
  } | eagle_db_pipe
436
446
  _proj_hash=$(printf '%s' "$project" | shasum | cut -c1-8)
437
447
  touch "$EAGLE_MEM_DIR/.co-edit-active.${_proj_hash}"
438
- eagle_log "INFO" "Curator: stored ${#co_map[@]} co-edit hints from $co_edit_count pairs"
448
+ eagle_log "INFO" "Curator: stored $co_map_file_count co-edit hints from $co_edit_count pairs"
439
449
  fi
440
- eagle_ok "$co_edit_count co-edit pairs found (${#co_map[@]} files)"
450
+ eagle_ok "$co_edit_count co-edit pairs found ($co_map_file_count files)"
441
451
  else
442
452
  if [ "$DRY_RUN" -eq 0 ]; then
443
453
  eagle_delete_file_hints "$project" "co_edit"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — Feature management
4
- # eagle-mem feature [list|show|verify|add]
4
+ # eagle-mem feature [list|show|verify|pending|waive|add]
5
5
  # ═══════════════════════════════════════════════════════════
6
6
  set -euo pipefail
7
7
 
@@ -59,8 +59,76 @@ case "$subcommand" in
59
59
  *) notes="$1"; shift ;;
60
60
  esac
61
61
  done
62
+ fid=$(eagle_get_feature_id "$project" "$name")
63
+ if [ -z "$fid" ]; then
64
+ eagle_err "Feature not found: $name"
65
+ exit 1
66
+ fi
62
67
  eagle_verify_feature "$project" "$name" "$notes"
68
+ resolved=$(eagle_resolve_pending_feature_verifications "$project" "$name" "verified" "$notes" | tail -1)
63
69
  eagle_ok "Feature '$name' marked as verified"
70
+ if [ "${resolved:-0}" -gt 0 ] 2>/dev/null; then
71
+ eagle_info "Resolved pending verification records: $resolved"
72
+ fi
73
+ ;;
74
+
75
+ pending)
76
+ results=$(eagle_list_pending_feature_verifications "$project" 50)
77
+ if [ -z "$results" ]; then
78
+ eagle_ok "No pending feature verifications for '$project'"
79
+ exit 0
80
+ fi
81
+
82
+ echo -e " ${BOLD}ID Feature File Trigger Diff${RESET}"
83
+ echo -e " ${DIM}──── ────────────────────────────── ──────────────────────────── ───────── ────────────${RESET}"
84
+ while IFS='|' read -r id feat file reason trigger created smoke fingerprint; do
85
+ [ -z "$id" ] && continue
86
+ feat_display="${feat:0:30}"
87
+ file_display="${file:0:28}"
88
+ trigger_display="${trigger:-hook}"
89
+ fingerprint_display="${fingerprint:-unknown}"
90
+ printf " %-4s %-30s %-28s %-9s %-12s\n" "$id" "$feat_display" "$file_display" "$trigger_display" "$fingerprint_display"
91
+ [ -n "$reason" ] && echo -e " ${DIM}${reason}${RESET}"
92
+ [ -n "$smoke" ] && echo -e " ${CYAN}smoke:${RESET} $smoke"
93
+ [ -n "$created" ] && echo -e " ${DIM}created: $created${RESET}"
94
+ done <<< "$results"
95
+ echo ""
96
+ eagle_info "Verify after testing: eagle-mem feature verify <name> --notes \"what passed\""
97
+ eagle_info "Waive intentionally: eagle-mem feature waive <id> --reason \"why safe\""
98
+ ;;
99
+
100
+ waive)
101
+ id="${1:-}"
102
+ [ -z "$id" ] && { eagle_err "Usage: eagle-mem feature waive <id> --reason <text>"; exit 1; }
103
+ case "$id" in
104
+ *[!0-9]*)
105
+ eagle_err "Invalid ID: '$id' (must be numeric)"
106
+ exit 1
107
+ ;;
108
+ esac
109
+ shift
110
+ reason=""
111
+ while [ $# -gt 0 ]; do
112
+ case "$1" in
113
+ --reason|--notes)
114
+ if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
115
+ eagle_err "$1 requires a value"
116
+ exit 1
117
+ fi
118
+ reason="$2"
119
+ shift 2
120
+ ;;
121
+ *) reason="$1"; shift ;;
122
+ esac
123
+ done
124
+ [ -z "$reason" ] && { eagle_err "Usage: eagle-mem feature waive <id> --reason <text>"; exit 1; }
125
+ waived=$(eagle_waive_pending_feature_verification "$project" "$id" "$reason" | tail -1)
126
+ if [ "${waived:-0}" -gt 0 ] 2>/dev/null; then
127
+ eagle_ok "Pending verification #$id waived"
128
+ else
129
+ eagle_err "No pending verification found with ID $id"
130
+ exit 1
131
+ fi
64
132
  ;;
65
133
 
66
134
  add)
@@ -104,7 +172,7 @@ case "$subcommand" in
104
172
 
105
173
  *)
106
174
  eagle_err "Unknown feature command: $subcommand"
107
- eagle_info "Usage: eagle-mem feature [list|show|verify|add]"
175
+ eagle_info "Usage: eagle-mem feature [list|show|verify|pending|waive|add]"
108
176
  exit 1
109
177
  ;;
110
178
  esac
package/scripts/guard.sh CHANGED
@@ -55,7 +55,10 @@ case "$subcommand" in
55
55
  esac
56
56
  done
57
57
 
58
- eagle_add_guardrail "$project" "$rule" "$file_pattern" "manual"
58
+ if ! eagle_add_guardrail "$project" "$rule" "$file_pattern" "manual"; then
59
+ eagle_err "Failed to add guardrail. Check $EAGLE_MEM_LOG for SQLite details."
60
+ exit 1
61
+ fi
59
62
  eagle_ok "Guardrail added for project: $project"
60
63
  if [ -n "$file_pattern" ]; then
61
64
  eagle_info "File pattern: $file_pattern"
package/scripts/help.sh CHANGED
@@ -13,7 +13,7 @@ version=$(jq -r .version "$PACKAGE_DIR/package.json" 2>/dev/null || echo "unknow
13
13
  eagle_banner
14
14
 
15
15
  echo -e " ${BOLD}Eagle Mem${RESET} ${DIM}v${version}${RESET}"
16
- echo -e " ${DIM}Context that survives /compact for Claude Code${RESET}"
16
+ echo -e " ${DIM}Context that survives /compact for Claude Code and Codex${RESET}"
17
17
  echo ""
18
18
  echo -e " ${BOLD}Commands:${RESET}"
19
19
  echo -e " ${CYAN}install${RESET} First-time setup: hooks, database, skills"
@@ -27,7 +27,7 @@ echo -e " ${CYAN}overview${RESET} Build or view project overview"
27
27
  echo -e " ${CYAN}memories${RESET} View/sync Claude Code memories"
28
28
  echo -e " ${CYAN}tasks${RESET} View mirrored tasks"
29
29
  echo -e " ${CYAN}curate${RESET} Run curator (co-edits, hot files, guardrails)"
30
- echo -e " ${CYAN}feature${RESET} Track and verify features"
30
+ echo -e " ${CYAN}feature${RESET} Track, verify, and unblock features"
31
31
  echo -e " ${CYAN}prune${RESET} Clean old sessions and stale data"
32
32
  echo ""
33
33
  echo -e " ${BOLD}Search modes:${RESET}"
@@ -39,6 +39,11 @@ echo -e " ${DIM}\$${RESET} eagle-mem search --tasks ${DIM}# in-flight
39
39
  echo -e " ${DIM}\$${RESET} eagle-mem search --files ${DIM}# hot files${RESET}"
40
40
  echo -e " ${DIM}\$${RESET} eagle-mem search --stats ${DIM}# project stats${RESET}"
41
41
  echo ""
42
+ echo -e " ${BOLD}Anti-regression:${RESET}"
43
+ echo -e " ${DIM}\$${RESET} eagle-mem feature pending ${DIM}# pending release blockers${RESET}"
44
+ echo -e " ${DIM}\$${RESET} eagle-mem feature verify NAME ${DIM}# verify current diff after testing${RESET}"
45
+ echo -e " ${DIM}\$${RESET} eagle-mem feature waive ID ${DIM}# intentional exception${RESET}"
46
+ echo ""
42
47
  echo -e " ${BOLD}Skills${RESET} ${DIM}(inside Claude Code sessions):${RESET}"
43
48
  echo -e " ${CYAN}/eagle-mem-search${RESET} Search memory and past sessions"
44
49
  echo -e " ${CYAN}/eagle-mem-overview${RESET} Build or update project overview"