eagle-mem 4.10.11 → 4.10.13

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/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.10.13 Feature Gate Monorepo Hardening
8
+
9
+ This hotfix closes the feature verification gate false positives found in monorepos with repeated basenames:
10
+
11
+ - **Full-Path Feature Matching**: Feature impact lookup now matches exact paths and path-boundary suffixes instead of broad basename-only `%server.js%` patterns when feature files store full paths.
12
+ - **LIKE Escaping Hardening**: Feature path matching now treats `%`, `_`, and backslashes literally, preventing stored feature paths from becoming accidental SQL `LIKE` wildcards.
13
+ - **Waive Safety**: Waived pending verifications are now scoped to the current change fingerprint, so a future edit to the same feature file reopens verification instead of being permanently bypassed.
14
+ - **Release Guard Precision**: Eagle Mem state commands such as `orchestrate` and `tasks` no longer trip the release-boundary guard just because their descriptive text mentions `npm publish` or `git push`.
15
+ - **Regression Coverage**: Added end-to-end feature gate coverage for monorepo path collisions, literal wildcard characters in paths, PreToolUse `git push` denial output, and same-fingerprint verification/waive behavior.
16
+
17
+ ---
18
+
19
+ ## v4.10.12 Spectral Review Closure
20
+
21
+ This patch closes the multi-CLI Spectral review findings on v4.10.11:
22
+
23
+ - **Run Log Containment**: `eagle-mem logs show|tail` now resolves only run-log IDs, filenames, or absolute paths under `~/.eagle-mem/runs`, preventing arbitrary file reads through the logs subcommand.
24
+ - **Run Log Retention**: Added `eagle-mem logs prune --days N --keep N` plus automatic pruning when command-scoped run logs start, defaulting to logs older than 14 days and retaining the latest 50.
25
+ - **Run Log Diagnostics**: `eagle_log` messages now mirror into the active command run log, so failure log paths include provider and internal diagnostic messages instead of only command stdout/stderr.
26
+ - **PostToolUse Tracker Locking**: Modification tracking now writes every modified file through the same lock path, retries lock acquisition, avoids unlocked appends to the trimmed tracker, and records all files from multi-file `apply_patch` operations.
27
+ - **Curator JSON Robustness**: Dream Cycle consolidation parsing now tolerates provider text wrapped around the JSON payload while preserving strict `jq` validation.
28
+ - **Regression Coverage**: Expanded reliability tests for log path rejection, log pruning, mirrored run diagnostics, unsupported agent target logging, and multi-file modification tracking.
29
+
30
+ ---
31
+
7
32
  ## v4.10.11 Reliability Guards and Provider Fallback
8
33
 
9
34
  This patch closes the active reliability items that remained after the Dream Cycle hotfix:
package/README.md CHANGED
@@ -146,7 +146,7 @@ Eagle Mem prevents Claude from repeating past mistakes:
146
146
  | `eagle-mem search` | Search past sessions, memories, and code |
147
147
  | `eagle-mem health` | Diagnose pipeline health and background automation |
148
148
  | `eagle-mem doctor` | Verify installed runtime files, hooks, SQLite/FTS5, statusline, manifest, and drift |
149
- | `eagle-mem logs` | Inspect command-scoped `scan`, `index`, and `curate` run logs |
149
+ | `eagle-mem logs` | Inspect and prune command-scoped `scan`, `index`, and `curate` run logs |
150
150
  | `eagle-mem config` | View or change LLM provider and token-guard settings |
151
151
  | `eagle-mem updates` | View or change auto-update policy |
152
152
  | `eagle-mem guard` | Manage regression guardrails for files |
@@ -349,6 +349,8 @@ Provider preference is local-first: Ollama is auto-detected when running, then E
349
349
 
350
350
  Provider calls use an explicit fallback chain by default. For example, `agent_cli` can try the preferred/current agent first and then fall through to another supported local CLI when available. `eagle-mem config`, `eagle-mem health`, and `eagle-mem curate` display the resolved provider path so failed or unavailable agent CLIs are visible instead of appearing as `unknown`.
351
351
 
352
+ Command-scoped run logs live under `~/.eagle-mem/runs`. Use `eagle-mem logs list`, `eagle-mem logs show <run-id>`, `eagle-mem logs tail <run-id>`, and `eagle-mem logs prune --days 14 --keep 50` to inspect or trim them. Log reads are constrained to the run-log directory.
353
+
352
354
  RTK is configured separately from the LLM provider:
353
355
 
354
356
  ```bash
@@ -17,6 +17,50 @@ LIB_DIR="$SCRIPT_DIR/../lib"
17
17
  input=$(eagle_read_stdin)
18
18
  [ -z "$input" ] && exit 0
19
19
 
20
+ eagle_track_modified_path() {
21
+ local path="$1" sid="$2"
22
+ [ -n "$path" ] || return 0
23
+ [ -n "$sid" ] && eagle_validate_session_id "$sid" || return 0
24
+
25
+ local mod_dir mod_file mod_lock mod_tmp attempt
26
+ mod_dir="$EAGLE_MEM_DIR/mod-tracker"
27
+ mkdir -p "$mod_dir" 2>/dev/null || return 0
28
+ mod_file="$mod_dir/${sid}"
29
+ mod_lock="${mod_file}.lock"
30
+
31
+ for attempt in 1 2 3 4 5 6 7 8 9 10; do
32
+ if mkdir "$mod_lock" 2>/dev/null; then
33
+ mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || mod_tmp="${mod_file}.$$"
34
+ (
35
+ cat "$mod_file" 2>/dev/null
36
+ for pending_file in "${mod_file}".pending.*; do
37
+ [ -f "$pending_file" ] && cat "$pending_file" 2>/dev/null
38
+ done
39
+ printf '%s\n' "$path"
40
+ ) | tail -3 > "$mod_tmp"
41
+ mv "$mod_tmp" "$mod_file" 2>/dev/null || rm -f "$mod_tmp"
42
+ rm -f "${mod_file}".pending.* 2>/dev/null || true
43
+ rmdir "$mod_lock" 2>/dev/null || true
44
+ return 0
45
+ fi
46
+ sleep 0.05
47
+ done
48
+
49
+ printf '%s\n' "$path" >> "${mod_file}.pending.$$" 2>/dev/null || true
50
+ eagle_log "WARN" "PostToolUse: mod-tracker lock busy; queued pending modified file for session=$sid"
51
+ }
52
+
53
+ eagle_track_edit_history_path() {
54
+ local path="$1" sid="$2"
55
+ [ -n "$path" ] || return 0
56
+ [ -n "$sid" ] && eagle_validate_session_id "$sid" || return 0
57
+
58
+ local edit_dir
59
+ edit_dir="$EAGLE_MEM_DIR/edit-tracker"
60
+ mkdir -p "$edit_dir" 2>/dev/null || return 0
61
+ printf '%s\n' "$path" >> "$edit_dir/${sid}" 2>/dev/null || true
62
+ }
63
+
20
64
  IFS=$'\x1f' read -r session_id cwd tool_name hook_event <<< \
21
65
  "$(echo "$input" | jq -r '[.session_id, .cwd, .tool_name, .hook_event_name] | map(. // "") | join("\u001f")')"
22
66
  agent=$(eagle_agent_source_from_json "$input")
@@ -167,27 +211,16 @@ esac
167
211
 
168
212
  # ─── Track recent Edit/Write targets for Read-after-modify detection ──
169
213
 
170
- if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
214
+ if [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
171
215
  case "$tool_name" in
172
216
  Edit|Write|apply_patch)
173
- mod_dir="$EAGLE_MEM_DIR/mod-tracker"
174
- mkdir -p "$mod_dir" 2>/dev/null
175
- mod_file="$mod_dir/${session_id}"
176
- mod_lock="${mod_file}.lock"
177
- if mkdir "$mod_lock" 2>/dev/null; then
178
- _mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || _mod_tmp="${mod_file}.$$"
179
- (cat "$mod_file" 2>/dev/null; printf '%s\n' "$fp") | tail -3 > "$_mod_tmp"
180
- mv "$_mod_tmp" "$mod_file" 2>/dev/null || rm -f "$_mod_tmp"
181
- rmdir "$mod_lock" 2>/dev/null || true
182
- else
183
- # If another hook is trimming, append is still safer than losing the edit.
184
- printf '%s\n' "$fp" >> "$mod_file"
185
- fi
186
-
187
- # Full edit history for stuck loop detection (not truncated)
188
- edit_dir="$EAGLE_MEM_DIR/edit-tracker"
189
- mkdir -p "$edit_dir" 2>/dev/null
190
- echo "$fp" >> "$edit_dir/${session_id}"
217
+ modified_paths=$(printf '%s' "$files_modified" | jq -r '.[]?' 2>/dev/null)
218
+ [ -n "$modified_paths" ] || modified_paths="$fp"
219
+ while IFS= read -r modified_path; do
220
+ [ -z "$modified_path" ] && continue
221
+ eagle_track_modified_path "$modified_path" "$session_id"
222
+ eagle_track_edit_history_path "$modified_path" "$session_id"
223
+ done <<< "$modified_paths"
191
224
  ;;
192
225
  esac
193
226
  fi
@@ -150,9 +150,8 @@ One-off developer bypass:
150
150
  while IFS= read -r changed_file; do
151
151
  [ -z "$changed_file" ] && continue
152
152
  norm_file=$(eagle_project_file_path "$cwd" "$changed_file")
153
- fname=$(basename "$norm_file")
154
153
 
155
- feature_hits=$(eagle_find_feature_for_push "$project" "$fname")
154
+ feature_hits=$(eagle_find_feature_for_push "$project" "$norm_file")
156
155
 
157
156
  while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
158
157
  [ -z "$feat_name" ] && continue
package/lib/common.sh CHANGED
@@ -106,11 +106,17 @@ eagle_require_sqlite_fts5() {
106
106
  eagle_log() {
107
107
  local level="$1"
108
108
  shift
109
+ local ts msg
110
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
111
+ msg="[$ts] [$level] $*"
109
112
  # Ensure log file is owner-only (may contain debug data)
110
113
  if [ ! -f "$EAGLE_MEM_LOG" ]; then
111
114
  touch "$EAGLE_MEM_LOG" 2>/dev/null && chmod 600 "$EAGLE_MEM_LOG" 2>/dev/null
112
115
  fi
113
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
116
+ echo "$msg" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
117
+ if [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] && [ -n "${EAGLE_RUN_LOG:-}" ] && [ "$EAGLE_RUN_LOG" != "$EAGLE_MEM_LOG" ]; then
118
+ echo "$msg" >> "$EAGLE_RUN_LOG" 2>/dev/null || true
119
+ fi
114
120
  }
115
121
 
116
122
  eagle_run_slug() {
@@ -120,6 +126,39 @@ eagle_run_slug() {
120
126
  | cut -c1-48
121
127
  }
122
128
 
129
+ eagle_run_prune_logs() {
130
+ local days="${1:-${EAGLE_RUN_LOG_RETENTION_DAYS:-14}}"
131
+ local keep="${2:-${EAGLE_RUN_LOG_MAX_COUNT:-50}}"
132
+ local rel_log
133
+
134
+ [ -d "$EAGLE_RUNS_DIR" ] || return 0
135
+ case "$days" in ""|*[!0-9]*) days=14 ;; esac
136
+ case "$keep" in ""|*[!0-9]*) keep=50 ;; esac
137
+
138
+ find "$EAGLE_RUNS_DIR" -type f -name '*.log' -print 2>/dev/null \
139
+ | while IFS= read -r stale_log; do
140
+ rel_log="${stale_log#"$EAGLE_RUNS_DIR"/}"
141
+ case "$rel_log" in
142
+ */*) rm -f -- "$stale_log" 2>/dev/null || true ;;
143
+ esac
144
+ done
145
+
146
+ if [ "$days" -gt 0 ]; then
147
+ find "$EAGLE_RUNS_DIR" -type f -name '*.log' -mtime +"$days" -print 2>/dev/null \
148
+ | while IFS= read -r stale_log; do
149
+ rm -f -- "$stale_log" 2>/dev/null || true
150
+ done
151
+ fi
152
+
153
+ if [ "$keep" -gt 0 ]; then
154
+ ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null \
155
+ | awk -v keep="$keep" 'NR > keep' \
156
+ | while IFS= read -r stale_log; do
157
+ rm -f -- "$stale_log" 2>/dev/null || true
158
+ done
159
+ fi
160
+ }
161
+
123
162
  eagle_run_start() {
124
163
  [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] && return 0
125
164
 
@@ -129,6 +168,7 @@ eagle_run_start() {
129
168
  [ -n "$slug" ] || slug="command"
130
169
 
131
170
  mkdir -p "$EAGLE_RUNS_DIR" "$EAGLE_MEM_DIR" 2>/dev/null || true
171
+ eagle_run_prune_logs >/dev/null 2>&1 || true
132
172
  EAGLE_RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-${slug}-$$"
133
173
  EAGLE_RUN_LOG="$EAGLE_RUNS_DIR/${EAGLE_RUN_ID}.log"
134
174
  EAGLE_RUN_COMMAND="$command_name"
@@ -818,7 +858,9 @@ eagle_project_file_path() {
818
858
  }
819
859
 
820
860
  eagle_extract_apply_patch_files() {
821
- sed -n -E 's/^\*\*\* (Add|Update|Delete) File: //p'
861
+ sed -n -E \
862
+ -e 's/^\*\*\* (Add|Update|Delete) File: //p' \
863
+ -e 's/^\*\*\* Move to: //p'
822
864
  }
823
865
 
824
866
  eagle_agent_source() {
@@ -1118,10 +1160,10 @@ eagle_is_release_boundary_command() {
1118
1160
  function has_dry_run_flag(line) {
1119
1161
  return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
1120
1162
  }
1121
- function is_eagle_feature_command(line) {
1122
- return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+feature[[:space:]]+(verify|waive|pending|list)([[:space:]]|$)/
1163
+ function is_eagle_state_command(line) {
1164
+ return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+(feature[[:space:]]+(verify|waive|pending|list)|orchestrate|tasks)([[:space:]]|$)/
1123
1165
  }
1124
- is_eagle_feature_command($0) { next }
1166
+ is_eagle_state_command($0) { next }
1125
1167
  /(^|[[:space:]])gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)/ ||
1126
1168
  /(^|[[:space:]])npm[[:space:]]+publish([[:space:]]|$)/ ||
1127
1169
  /(^|[[:space:]])pnpm[[:space:]]+publish([[:space:]]|$)/ ||
@@ -1143,10 +1185,10 @@ eagle_is_release_boundary_command() {
1143
1185
  function has_dry_run_flag(line) {
1144
1186
  return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
1145
1187
  }
1146
- function is_eagle_feature_command(line) {
1147
- return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+feature[[:space:]]+(verify|waive|pending|list)([[:space:]]|$)/
1188
+ function is_eagle_state_command(line) {
1189
+ return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+(feature[[:space:]]+(verify|waive|pending|list)|orchestrate|tasks)([[:space:]]|$)/
1148
1190
  }
1149
- is_eagle_feature_command($0) { next }
1191
+ is_eagle_state_command($0) { next }
1150
1192
  /(^|[[:space:]])git[[:space:]]+push([[:space:]]|$)/ {
1151
1193
  if (!has_dry_run_flag($0)) found = 1
1152
1194
  }
@@ -1259,10 +1301,10 @@ eagle_fts_sanitize() {
1259
1301
  printf '%s' "$1" | sed 's/[^A-Za-z0-9_]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
1260
1302
  }
1261
1303
 
1262
- # Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
1304
+ # Escape SQL LIKE wildcards and the escape character so literal filenames match exactly.
1263
1305
  # Apply AFTER eagle_sql_escape, since this only handles LIKE metacharacters.
1264
1306
  eagle_like_escape() {
1265
- printf '%s' "$1" | sed 's/%/\\%/g; s/_/\\_/g'
1307
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/%/\\%/g; s/_/\\_/g'
1266
1308
  }
1267
1309
 
1268
1310
  # Validate a session ID is safe for use in file paths (no traversal).
@@ -58,14 +58,25 @@ eagle_verify_feature() {
58
58
  WHERE project = '$project' AND name = '$name';"
59
59
  }
60
60
 
61
+ eagle_feature_file_match_ff_sql() {
62
+ local file_path="$1"
63
+ local file_esc; file_esc=$(eagle_sql_escape "$file_path")
64
+ local file_like; file_like=$(eagle_like_escape "$file_esc")
65
+
66
+ cat <<SQL
67
+ (
68
+ ff.file_path = '$file_esc'
69
+ OR ff.file_path LIKE '%/$file_like' ESCAPE '\\'
70
+ OR substr('$file_esc', -length('/' || ff.file_path)) = '/' || ff.file_path
71
+ )
72
+ SQL
73
+ }
74
+
61
75
  eagle_find_feature_impacts_for_file() {
62
76
  local project; project=$(eagle_sql_escape "$1")
63
77
  local file_path="$2"
64
- local fname; fname=$(basename "$file_path")
65
- local file_esc; file_esc=$(eagle_sql_escape "$file_path")
66
- local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
67
- local file_like; file_like=$(eagle_like_escape "$file_esc")
68
- local fname_like; fname_like=$(eagle_like_escape "$fname_esc")
78
+ local file_match_sql
79
+ file_match_sql=$(eagle_feature_file_match_ff_sql "$file_path")
69
80
 
70
81
  eagle_db "SELECT DISTINCT f.id, f.name, f.description, f.last_verified_at,
71
82
  ff.file_path,
@@ -75,13 +86,7 @@ eagle_find_feature_impacts_for_file() {
75
86
  JOIN feature_files ff ON ff.feature_id = f.id
76
87
  WHERE f.project = '$project'
77
88
  AND f.status = 'active'
78
- AND (
79
- ff.file_path = '$file_esc'
80
- OR ff.file_path LIKE '%/$file_like' ESCAPE '\\'
81
- OR '$file_esc' LIKE '%' || ff.file_path ESCAPE '\\'
82
- OR ff.file_path LIKE '%$fname_like' ESCAPE '\\'
83
- OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\'
84
- )
89
+ AND $file_match_sql
85
90
  ORDER BY f.updated_at DESC
86
91
  LIMIT 10;"
87
92
  }
@@ -114,10 +119,8 @@ eagle_record_pending_feature_verifications() {
114
119
  WHERE project = '$p_esc'
115
120
  AND feature_id = $fid
116
121
  AND file_path = '$fp_esc'
117
- AND (
118
- (change_fingerprint = '$fp_hash_esc' AND status = 'verified')
119
- OR status = 'waived'
120
- )
122
+ AND change_fingerprint = '$fp_hash_esc'
123
+ AND status IN ('verified', 'waived')
121
124
  LIMIT 1;")
122
125
  [ -n "$already_resolved" ] && continue
123
126
 
@@ -341,8 +344,9 @@ eagle_count_active_features() {
341
344
 
342
345
  eagle_find_feature_for_push() {
343
346
  local project; project=$(eagle_sql_escape "$1")
344
- local fname; fname=$(eagle_sql_escape "$2")
345
- local fname_like; fname_like=$(eagle_like_escape "$fname")
347
+ local file_path="$2"
348
+ local file_match_sql
349
+ file_match_sql=$(eagle_feature_file_match_ff_sql "$file_path")
346
350
 
347
351
  eagle_db "SELECT DISTINCT f.name,
348
352
  (SELECT GROUP_CONCAT(fst.command, '; ')
@@ -354,15 +358,14 @@ eagle_find_feature_for_push() {
354
358
  JOIN feature_files ff ON ff.feature_id = f.id
355
359
  WHERE f.project = '$project'
356
360
  AND f.status = 'active'
357
- AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\');"
361
+ AND $file_match_sql;"
358
362
  }
359
363
 
360
364
  eagle_find_features_for_file() {
361
365
  local project; project=$(eagle_sql_escape "$1")
362
366
  local file_path="$2"
363
- local fname; fname=$(basename "$file_path")
364
- local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
365
- local fname_like; fname_like=$(eagle_like_escape "$fname_esc")
367
+ local file_match_sql
368
+ file_match_sql=$(eagle_feature_file_match_ff_sql "$file_path")
366
369
 
367
370
  eagle_db "SELECT f.name, f.description, f.last_verified_at,
368
371
  ff.role,
@@ -376,7 +379,7 @@ eagle_find_features_for_file() {
376
379
  JOIN feature_files ff ON ff.feature_id = f.id
377
380
  WHERE f.project = '$project'
378
381
  AND f.status = 'active'
379
- AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\')
382
+ AND $file_match_sql
380
383
  ORDER BY f.updated_at DESC
381
384
  LIMIT 3;"
382
385
  }
package/lib/provider.sh CHANGED
@@ -409,7 +409,10 @@ _eagle_agent_cli_target_chain() {
409
409
  claude|claude-code|cloud-code) preferred_target="claude-code" ;;
410
410
  current) preferred_target="$current" ;;
411
411
  auto|"") preferred_target="" ;;
412
- *) preferred_target="$preferred" ;;
412
+ *)
413
+ eagle_log "WARN" "agent_cli unsupported preferred target: $preferred"
414
+ preferred_target=""
415
+ ;;
413
416
  esac
414
417
 
415
418
  for candidate in "$preferred_target" "$current" codex claude-code; do
@@ -469,7 +472,11 @@ _eagle_call_agent_cli() {
469
472
  case "$target" in
470
473
  codex) result=$(_eagle_call_codex_cli "$prompt" "$system" "$max_tokens"); rc=$? ;;
471
474
  claude-code) result=$(_eagle_call_claude_cli "$prompt" "$system" "$max_tokens"); rc=$? ;;
472
- *) rc=1; result="" ;;
475
+ *)
476
+ eagle_log "WARN" "agent_cli unsupported target: $target"
477
+ rc=1
478
+ result=""
479
+ ;;
473
480
  esac
474
481
  if [ "$rc" -eq 0 ] && [ -n "$result" ]; then
475
482
  [ "$tried" -gt 1 ] && eagle_log "INFO" "agent_cli fallback succeeded with $target"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.11",
3
+ "version": "4.10.13",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, Grok, and Google Antigravity",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -39,22 +39,20 @@ EOF
39
39
 
40
40
  parse_consolidations_json() {
41
41
  local result="$1"
42
- local trimmed json_payload
43
-
44
- trimmed=$(printf '%s' "$result" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
45
- case "$trimmed" in
46
- ""|NONE|none|null)
47
- printf '[]\n'
48
- return 0
49
- ;;
50
- esac
51
-
52
- json_payload=$(printf '%s' "$result" \
53
- | sed -e '1s/^[[:space:]]*```json[[:space:]]*$//' \
54
- -e '1s/^[[:space:]]*```[[:space:]]*$//' \
55
- -e '$s/^[[:space:]]*```[[:space:]]*$//')
56
-
57
- printf '%s' "$json_payload" | jq -c '
42
+ printf '%s' "$result" | jq -Rrs -c '
43
+ def text_trim: gsub("^\\s+|\\s+$"; "");
44
+ def parse_payload:
45
+ gsub("\r"; "")
46
+ | gsub("^\\s*```json\\s*\\n"; "")
47
+ | gsub("^\\s*```\\s*\\n"; "")
48
+ | gsub("\\n\\s*```\\s*$"; "")
49
+ | text_trim
50
+ | if . == "" or . == "NONE" or . == "none" or . == "null" then []
51
+ else
52
+ try fromjson catch (
53
+ ([match("(?s)(\\{.*\\}|\\[.*\\])")? | .string][0] // "[]") | fromjson
54
+ )
55
+ end;
58
56
  def trim: gsub("^\\s+|\\s+$"; "");
59
57
  def names:
60
58
  if type == "array" then map(tostring | trim) | map(select(length > 0))
@@ -66,7 +64,8 @@ parse_consolidations_json() {
66
64
  elif type == "object" then (.consolidations // .items // .instructions // [])
67
65
  else []
68
66
  end;
69
- root
67
+ parse_payload
68
+ | root
70
69
  | map({
71
70
  source_names: ((.source_names // .sourceNames // .source_memories // .sourceMemories // .original_names // .originalNames // .originals // .names) | names),
72
71
  new_name: ((.new_name // .newName // .name // .title // "") | tostring | trim),
package/scripts/help.sh CHANGED
@@ -23,7 +23,7 @@ echo -e " ${CYAN}uninstall${RESET} Remove hooks and optionally delete data"
23
23
  echo -e " ${CYAN}search${RESET} Search past sessions, memories, and code"
24
24
  echo -e " ${CYAN}health${RESET} Diagnose pipeline health and background automation"
25
25
  echo -e " ${CYAN}doctor${RESET} Show install footprint, hooks, SQLite, manifest, and runtime drift"
26
- echo -e " ${CYAN}logs${RESET} Inspect command-scoped scan/index/curate logs"
26
+ echo -e " ${CYAN}logs${RESET} Inspect/prune command-scoped scan/index/curate logs"
27
27
  echo -e " ${CYAN}updates${RESET} Auto-update status and policy"
28
28
  echo -e " ${CYAN}overview${RESET} Build or view project overview"
29
29
  echo -e " ${CYAN}session${RESET} Save a manual session summary"
package/scripts/logs.sh CHANGED
@@ -15,36 +15,60 @@ cmd="${1:-list}"
15
15
 
16
16
  show_help() {
17
17
  cat <<EOF
18
- Usage: eagle-mem logs [list|tail|show] [run-id-or-path]
18
+ Usage: eagle-mem logs [list|tail|show|prune] [run-id-or-filename]
19
19
 
20
20
  Commands:
21
- list Show recent command-scoped run logs
22
- tail [id|path] Tail a run log, or the latest run log when omitted
23
- show <id|path> Print a run log
21
+ list Show recent command-scoped run logs
22
+ tail [id|filename] Tail a run log, or the latest run log when omitted
23
+ show <id|filename> Print a run log
24
+ prune [--days N] [--keep N]
25
+ Delete old run logs (defaults: 14 days, latest 50)
24
26
  EOF
25
27
  }
26
28
 
27
29
  resolve_log_path() {
28
30
  local ref="${1:-}"
31
+ local runs_root="${EAGLE_RUNS_DIR%/}" rel_ref
29
32
  if [ -z "$ref" ]; then
30
- ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | sed -n '1p'
31
- return 0
32
- fi
33
- if [ -f "$ref" ]; then
34
- printf '%s\n' "$ref"
33
+ ls -t "$runs_root"/*.log 2>/dev/null | while IFS= read -r candidate; do
34
+ [ -L "$candidate" ] && continue
35
+ [ -f "$candidate" ] && printf '%s\n' "$candidate" && break
36
+ done
35
37
  return 0
36
38
  fi
37
- if [ -f "$EAGLE_RUNS_DIR/$ref" ]; then
38
- printf '%s\n' "$EAGLE_RUNS_DIR/$ref"
39
+
40
+ case "$ref" in
41
+ *$'\n'*|*..*) return 1 ;;
42
+ /*)
43
+ rel_ref="${ref#"$runs_root"/}"
44
+ [ "$rel_ref" != "$ref" ] || return 1
45
+ case "$rel_ref" in ""|*/*) return 1 ;; esac
46
+ [ -L "$runs_root/$rel_ref" ] && return 1
47
+ [ -f "$runs_root/$rel_ref" ] && printf '%s\n' "$runs_root/$rel_ref" && return 0
48
+ return 1
49
+ ;;
50
+ */*) return 1 ;;
51
+ esac
52
+
53
+ if [ ! -L "$runs_root/$ref" ] && [ -f "$runs_root/$ref" ]; then
54
+ printf '%s\n' "$runs_root/$ref"
39
55
  return 0
40
56
  fi
41
- if [ -f "$EAGLE_RUNS_DIR/$ref.log" ]; then
42
- printf '%s\n' "$EAGLE_RUNS_DIR/$ref.log"
57
+ if [ ! -L "$runs_root/$ref.log" ] && [ -f "$runs_root/$ref.log" ]; then
58
+ printf '%s\n' "$runs_root/$ref.log"
43
59
  return 0
44
60
  fi
45
61
  return 1
46
62
  }
47
63
 
64
+ run_log_count() {
65
+ [ -d "$EAGLE_RUNS_DIR" ] || {
66
+ printf '0\n'
67
+ return 0
68
+ }
69
+ find "$EAGLE_RUNS_DIR" -type f -name '*.log' -print 2>/dev/null | wc -l | tr -d ' '
70
+ }
71
+
48
72
  case "$cmd" in
49
73
  -h|--help|help)
50
74
  show_help
@@ -57,6 +81,7 @@ case "$cmd" in
57
81
  exit 0
58
82
  fi
59
83
  ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | head -20 | while IFS= read -r log_path; do
84
+ [ -L "$log_path" ] && continue
60
85
  first_line=$(sed -n '1p' "$log_path" 2>/dev/null)
61
86
  run_id=$(basename "$log_path" .log)
62
87
  printf ' %s %s\n' "$run_id" "$first_line"
@@ -76,6 +101,41 @@ case "$cmd" in
76
101
  }
77
102
  cat "$log_path"
78
103
  ;;
104
+ prune)
105
+ days="${EAGLE_RUN_LOG_RETENTION_DAYS:-14}"
106
+ keep="${EAGLE_RUN_LOG_MAX_COUNT:-50}"
107
+ while [ $# -gt 0 ]; do
108
+ case "$1" in
109
+ --days)
110
+ days="${2:-}"
111
+ shift 2
112
+ ;;
113
+ --keep)
114
+ keep="${2:-}"
115
+ shift 2
116
+ ;;
117
+ *)
118
+ eagle_err "Unknown prune option: $1"
119
+ show_help
120
+ exit 1
121
+ ;;
122
+ esac
123
+ done
124
+ case "$days" in ""|*[!0-9]*)
125
+ eagle_err "Invalid --days value: $days"
126
+ exit 1
127
+ ;;
128
+ esac
129
+ case "$keep" in ""|*[!0-9]*)
130
+ eagle_err "Invalid --keep value: $keep"
131
+ exit 1
132
+ ;;
133
+ esac
134
+ before=$(run_log_count)
135
+ eagle_run_prune_logs "$days" "$keep"
136
+ after=$(run_log_count)
137
+ eagle_ok "Pruned run logs: before=$before after=$after days=$days keep=$keep"
138
+ ;;
79
139
  *)
80
140
  eagle_err "Unknown logs command: $cmd"
81
141
  show_help
package/scripts/test.sh CHANGED
@@ -54,6 +54,7 @@ run_check "Code Scan And Index (scan / index syntax)" "bash -n \"$SCRIPTS_DIR/sc
54
54
  run_check "Graph Memory Rebuild (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_graph_memory.sh\""
55
55
  run_check "Dream Cycle Memory Graph Wiring (isolated regression suite)" "bash \"$SCRIPTS_DIR/../tests/test_curate_graph_memories.sh\""
56
56
  run_check "Reliability Guards (provider fallback, logs, autoscan, read scoring)" "bash \"$SCRIPTS_DIR/../tests/test_reliability_guards.sh\""
57
+ run_check "Feature Verification Gate (monorepo path collisions)" "bash \"$SCRIPTS_DIR/../tests/test_feature_verification_gate.sh\""
57
58
 
58
59
  echo ""
59
60
  if [ "$errors" -eq 0 ]; then
@@ -24,7 +24,7 @@ These are semantically different operations:
24
24
 
25
25
  **Verify** = "I tested this exact change and it works." Fingerprint-specific — tied to the current diff hash. If the file changes again, a new pending verification appears.
26
26
 
27
- **Waive** = "I accept changes to this feature+file pair." Fingerprint-agnostic — covers the current change AND all future changes to that file for that feature. Use when the change is known-safe (e.g., comment-only edit, unrelated code path).
27
+ **Waive** = "I accept this current pending change without running the full smoke test." Fingerprint-specific — covers the current file fingerprint only. If that file changes again, Eagle Mem creates a fresh pending verification. Use when the current change is known-safe (e.g., comment-only edit, unrelated code path).
28
28
 
29
29
  **Decision rule:** Did you run the smoke test or manually confirm behavior? Use `verify`. Is the change structurally irrelevant to the feature? Use `waive`.
30
30
 
@@ -72,7 +72,7 @@ Or waive a single pending record by ID:
72
72
  eagle-mem feature waive <id> --reason "unrelated code path"
73
73
  ```
74
74
 
75
- **Prefer waive-by-name** over waive-by-ID. IDs are ephemeral (new edits create new IDs), but names are stable. Waiving by name resolves all pending records for that feature at once.
75
+ **Prefer waive-by-name** over waive-by-ID. IDs are ephemeral (new edits create new IDs), but names are stable. Waiving by name resolves all current pending records for that feature at once.
76
76
 
77
77
  A reason is always required — it's the audit trail for why verification was skipped.
78
78
 
@@ -116,7 +116,7 @@ eagle-mem feature show <name> # files, deps, smoke tests, last verified
116
116
  | `feature show <name>` | Full detail: files, dependencies, smoke tests |
117
117
  | `feature pending` | All unresolved pending verifications |
118
118
  | `feature verify <name>` | Mark feature verified (fingerprint-specific) |
119
- | `feature waive <name\|id>` | Waive verification (fingerprint-agnostic for name) |
119
+ | `feature waive <name\|id>` | Waive current pending verification(s) |
120
120
  | `feature add <name>` | Register a new feature with files/deps/tests |
121
121
  | `--notes "text"` | Attach notes to verify/waive (audit trail) |
122
122
  | `--reason "text"` | Required for waive — explains why safe |
@@ -104,6 +104,14 @@ assert_eq "2" "$(supersedes_edges project-json "Compiled AB JSON")" "consolidate
104
104
  assert_eq "3" "$(memory_graph_nodes project-json)" "curate should keep memory graph nodes idempotent"
105
105
  assert_eq "2" "$(supersedes_edges project-json "Compiled AB JSON")" "curate should keep supersedes edge count idempotent"
106
106
 
107
+ # Wrapped provider output: conversational text around JSON should still parse.
108
+ export EAGLE_CURATE_FAKE_RESPONSE=$'Here is the JSON you requested:\n{"consolidations":[{"source_names":["Wrapped A","Wrapped B"],"new_name":"Wrapped AB","description":"wrapped","value":"--- Compiled Truth ---\\nwrapped truth\\n\\n--- Evidence Trail ---\\n- Wrapped A\\n- Wrapped B"}]}\nDone.'
109
+ insert_memory "project-wrapped" "memory://wrapped-a" "Wrapped A" "First wrapped memory" "Content A"
110
+ insert_memory "project-wrapped" "memory://wrapped-b" "Wrapped B" "Second wrapped memory" "Content B"
111
+ "$EAGLE_BIN" curate -p project-wrapped >/dev/null
112
+ assert_eq "3" "$(memory_graph_nodes project-wrapped)" "wrapped JSON output should still create consolidated memory nodes"
113
+ assert_eq "2" "$(supersedes_edges project-wrapped "Wrapped AB")" "wrapped JSON output should still wire supersedes edges"
114
+
107
115
  # No consolidation: source nodes are still wired, but no supersedes edges are created.
108
116
  export EAGLE_CURATE_FAKE_RESPONSE='{"consolidations":[]}'
109
117
  insert_memory "project-none" "memory://none-a" "Memory None A" "First none memory" "Content A"
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bash
2
+ # Focused feature-verification gate regressions. Runs in an isolated HOME/EAGLE_MEM_DIR.
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+
7
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-feature-gate.XXXXXX")
8
+ trap 'rm -rf "$tmp_dir"' EXIT
9
+
10
+ export HOME="$tmp_dir/home"
11
+ export EAGLE_MEM_DIR="$tmp_dir/eagle-mem"
12
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR"
13
+
14
+ . "$ROOT_DIR/lib/common.sh"
15
+ "$ROOT_DIR/db/migrate.sh" >/dev/null
16
+ . "$ROOT_DIR/lib/db.sh"
17
+
18
+ assert_contains() {
19
+ local haystack="$1" needle="$2" message="$3"
20
+ case "$haystack" in
21
+ *"$needle"*) ;;
22
+ *)
23
+ echo "$message" >&2
24
+ echo "Expected to find: $needle" >&2
25
+ echo "Actual: $haystack" >&2
26
+ exit 1
27
+ ;;
28
+ esac
29
+ }
30
+
31
+ assert_not_contains() {
32
+ local haystack="$1" needle="$2" message="$3"
33
+ case "$haystack" in
34
+ *"$needle"*)
35
+ echo "$message" >&2
36
+ echo "Did not expect to find: $needle" >&2
37
+ echo "Actual: $haystack" >&2
38
+ exit 1
39
+ ;;
40
+ esac
41
+ }
42
+
43
+ assert_not_denied() {
44
+ local output="$1" message="$2"
45
+ case "$output" in
46
+ *'"permissionDecision":"deny"'*)
47
+ echo "$message" >&2
48
+ echo "Actual: $output" >&2
49
+ exit 1
50
+ ;;
51
+ esac
52
+ }
53
+
54
+ feature_id_for() {
55
+ local project="$1" name="$2"
56
+ local id
57
+ id=$(eagle_get_feature_id "$project" "$name")
58
+ [ -n "$id" ] || {
59
+ echo "Missing feature id for $project/$name" >&2
60
+ exit 1
61
+ }
62
+ printf '%s\n' "$id"
63
+ }
64
+
65
+ project="monorepo-collision"
66
+ eagle_upsert_feature "$project" "ussd-farm-profile-menu" "USSD app feature"
67
+ eagle_upsert_feature "$project" "telegram-webhook-processing" "Telegram app feature"
68
+ eagle_upsert_feature "$project" "whatsapp-webhook-processing" "WhatsApp app feature"
69
+
70
+ eagle_add_feature_file "$(feature_id_for "$project" "ussd-farm-profile-menu")" "apps/ussd/src/server.js" "entrypoint"
71
+ eagle_add_feature_file "$(feature_id_for "$project" "telegram-webhook-processing")" "apps/telegram/src/server.js" "entrypoint"
72
+ eagle_add_feature_file "$(feature_id_for "$project" "whatsapp-webhook-processing")" "apps/whatsapp/src/server.js" "entrypoint"
73
+
74
+ impacts=$(eagle_find_feature_impacts_for_file "$project" "apps/ussd/src/server.js")
75
+ assert_contains "$impacts" "ussd-farm-profile-menu" "full-path feature match should find the USSD feature"
76
+ assert_not_contains "$impacts" "telegram-webhook-processing" "full-path feature match should not fall back to Telegram basename"
77
+ assert_not_contains "$impacts" "whatsapp-webhook-processing" "full-path feature match should not fall back to WhatsApp basename"
78
+
79
+ impact_count=$(printf '%s\n' "$impacts" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
80
+ if [ "$impact_count" != "1" ]; then
81
+ echo "expected exactly one full-path feature impact, got $impact_count" >&2
82
+ echo "$impacts" >&2
83
+ exit 1
84
+ fi
85
+
86
+ eagle_record_pending_feature_verifications \
87
+ "$project" \
88
+ "apps/ussd/src/server.js" \
89
+ "session-feature-gate" \
90
+ "test" \
91
+ "Release boundary detected for current repository diff" \
92
+ "fingerprint-ussd" >/dev/null
93
+
94
+ pending_rows=$(eagle_db "SELECT feature_name || '|' || file_path
95
+ FROM pending_feature_verifications
96
+ WHERE project = '$project'
97
+ AND status = 'pending'
98
+ ORDER BY feature_name;")
99
+ assert_contains "$pending_rows" "ussd-farm-profile-menu|apps/ussd/src/server.js" "pending gate should create the matching USSD verification"
100
+ assert_not_contains "$pending_rows" "telegram-webhook-processing" "pending gate should not create Telegram false positives"
101
+ assert_not_contains "$pending_rows" "whatsapp-webhook-processing" "pending gate should not create WhatsApp false positives"
102
+
103
+ waived=$(eagle_resolve_pending_feature_verifications "$project" "ussd-farm-profile-menu" "waived" "current change is safe" | tail -1)
104
+ if [ "${waived:-0}" != "1" ]; then
105
+ echo "expected one pending verification to be waived, got ${waived:-0}" >&2
106
+ exit 1
107
+ fi
108
+ eagle_record_pending_feature_verifications \
109
+ "$project" \
110
+ "apps/ussd/src/server.js" \
111
+ "session-feature-gate" \
112
+ "test" \
113
+ "Release boundary detected for current repository diff" \
114
+ "fingerprint-ussd" >/dev/null
115
+ same_fingerprint_pending=$(eagle_db "SELECT COUNT(*)
116
+ FROM pending_feature_verifications
117
+ WHERE project = '$project'
118
+ AND feature_name = 'ussd-farm-profile-menu'
119
+ AND file_path = 'apps/ussd/src/server.js'
120
+ AND status = 'pending';")
121
+ if [ "$same_fingerprint_pending" != "0" ]; then
122
+ echo "same-fingerprint waiver should suppress the current pending record only" >&2
123
+ exit 1
124
+ fi
125
+ eagle_record_pending_feature_verifications \
126
+ "$project" \
127
+ "apps/ussd/src/server.js" \
128
+ "session-feature-gate" \
129
+ "test" \
130
+ "Release boundary detected for current repository diff" \
131
+ "fingerprint-ussd-2" >/dev/null
132
+ new_fingerprint_pending=$(eagle_db "SELECT COUNT(*)
133
+ FROM pending_feature_verifications
134
+ WHERE project = '$project'
135
+ AND feature_name = 'ussd-farm-profile-menu'
136
+ AND file_path = 'apps/ussd/src/server.js'
137
+ AND status = 'pending';")
138
+ if [ "$new_fingerprint_pending" != "1" ]; then
139
+ echo "new fingerprint should reopen a pending verification after a waiver" >&2
140
+ exit 1
141
+ fi
142
+
143
+ push_context=$(eagle_find_feature_for_push "$project" "apps/ussd/src/server.js")
144
+ assert_contains "$push_context" "ussd-farm-profile-menu" "push feature reminder should find the exact full-path feature"
145
+ assert_not_contains "$push_context" "telegram-webhook-processing" "push feature reminder should not use basename-only full-path matches"
146
+ assert_not_contains "$push_context" "whatsapp-webhook-processing" "push feature reminder should not use basename-only full-path matches"
147
+
148
+ read_context=$(eagle_find_features_for_file "$project" "apps/telegram/src/server.js")
149
+ assert_contains "$read_context" "telegram-webhook-processing" "read feature reminder should find the exact full-path feature"
150
+ assert_not_contains "$read_context" "ussd-farm-profile-menu" "read feature reminder should not use basename-only full-path matches"
151
+ assert_not_contains "$read_context" "whatsapp-webhook-processing" "read feature reminder should not use basename-only full-path matches"
152
+
153
+ legacy_project="bare-filename-compat"
154
+ eagle_upsert_feature "$legacy_project" "legacy-server-feature" "Legacy bare filename feature"
155
+ eagle_add_feature_file "$(feature_id_for "$legacy_project" "legacy-server-feature")" "server.js" "legacy"
156
+ legacy_impacts=$(eagle_find_feature_impacts_for_file "$legacy_project" "apps/ussd/src/server.js")
157
+ assert_contains "$legacy_impacts" "legacy-server-feature" "bare filename feature associations should still match full changed paths"
158
+
159
+ wild_project="literal-like-paths"
160
+ eagle_upsert_feature "$wild_project" "short-boundary-feature" "Boundary fixture"
161
+ eagle_upsert_feature "$wild_project" "long-boundary-feature" "Boundary fixture"
162
+ eagle_upsert_feature "$wild_project" "api-v1-underscore" "LIKE underscore fixture"
163
+ eagle_upsert_feature "$wild_project" "api-xv1" "LIKE underscore fixture"
164
+ eagle_upsert_feature "$wild_project" "api-percent-two" "LIKE percent fixture"
165
+ eagle_upsert_feature "$wild_project" "api-z-two" "LIKE percent fixture"
166
+ eagle_upsert_feature "$wild_project" "backslash-path" "LIKE backslash fixture"
167
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "short-boundary-feature")" "app/server.js" "entrypoint"
168
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "long-boundary-feature")" "myapp/server.js" "entrypoint"
169
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "api-v1-underscore")" "root/apps/api_v1/src/server.js" "entrypoint"
170
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "api-xv1")" "root/apps/apiXv1/src/server.js" "entrypoint"
171
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "api-percent-two")" "root/apps/api%2/src/server.js" "entrypoint"
172
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "api-z-two")" "root/apps/apiZ2/src/server.js" "entrypoint"
173
+ eagle_add_feature_file "$(feature_id_for "$wild_project" "backslash-path")" 'root/apps/api\_v1/src/server.js' "entrypoint"
174
+
175
+ boundary_hits=$(eagle_find_feature_impacts_for_file "$wild_project" "myapp/server.js")
176
+ assert_contains "$boundary_hits" "long-boundary-feature" "boundary match should find myapp/server.js"
177
+ assert_not_contains "$boundary_hits" "short-boundary-feature" "app/server.js should not match myapp/server.js"
178
+
179
+ underscore_hits=$(eagle_find_feature_impacts_for_file "$wild_project" "apps/apiXv1/src/server.js")
180
+ assert_contains "$underscore_hits" "api-xv1" "literal underscore fixture should find apiXv1"
181
+ assert_not_contains "$underscore_hits" "api-v1-underscore" "stored underscore should not act as a LIKE wildcard"
182
+
183
+ percent_hits=$(eagle_find_feature_impacts_for_file "$wild_project" "apps/apiZ2/src/server.js")
184
+ assert_contains "$percent_hits" "api-z-two" "literal percent fixture should find apiZ2"
185
+ assert_not_contains "$percent_hits" "api-percent-two" "stored percent should not act as a LIKE wildcard"
186
+
187
+ backslash_hits=$(eagle_find_feature_impacts_for_file "$wild_project" 'apps/api\_v1/src/server.js')
188
+ assert_contains "$backslash_hits" "backslash-path" "literal backslash path should match itself"
189
+ assert_not_contains "$backslash_hits" "api-v1-underscore" "literal backslash should not turn underscore into a wildcard match"
190
+
191
+ like_escaped=$(eagle_like_escape 'apps\api_v1%/server.js')
192
+ if [ "$like_escaped" != 'apps\\api\_v1\%/server.js' ]; then
193
+ echo "LIKE escape should escape backslash, underscore, and percent" >&2
194
+ echo "Actual: $like_escaped" >&2
195
+ exit 1
196
+ fi
197
+
198
+ hook_project="hook-monorepo-collision"
199
+ hook_repo="$tmp_dir/hook-repo"
200
+ mkdir -p "$hook_repo/apps/ussd/src" "$hook_repo/apps/telegram/src" "$hook_repo/apps/whatsapp/src"
201
+ git -C "$hook_repo" init -q
202
+ git -C "$hook_repo" config user.email "test@example.com"
203
+ git -C "$hook_repo" config user.name "Eagle Mem Test"
204
+ printf 'console.log("ussd v1");\n' > "$hook_repo/apps/ussd/src/server.js"
205
+ printf 'console.log("telegram v1");\n' > "$hook_repo/apps/telegram/src/server.js"
206
+ printf 'console.log("whatsapp v1");\n' > "$hook_repo/apps/whatsapp/src/server.js"
207
+ git -C "$hook_repo" add .
208
+ git -C "$hook_repo" commit -q -m "initial app files"
209
+
210
+ eagle_upsert_feature "$hook_project" "hook-ussd" "USSD hook feature"
211
+ eagle_upsert_feature "$hook_project" "hook-telegram" "Telegram hook feature"
212
+ eagle_upsert_feature "$hook_project" "hook-whatsapp" "WhatsApp hook feature"
213
+ eagle_add_feature_file "$(feature_id_for "$hook_project" "hook-ussd")" "apps/ussd/src/server.js" "entrypoint"
214
+ eagle_add_feature_file "$(feature_id_for "$hook_project" "hook-telegram")" "apps/telegram/src/server.js" "entrypoint"
215
+ eagle_add_feature_file "$(feature_id_for "$hook_project" "hook-whatsapp")" "apps/whatsapp/src/server.js" "entrypoint"
216
+
217
+ printf 'console.log("ussd v2");\n' > "$hook_repo/apps/ussd/src/server.js"
218
+ hook_input=$(jq -nc --arg sid "session-hook-feature-gate" --arg cwd "$hook_repo" \
219
+ '{tool_name:"Bash",session_id:$sid,cwd:$cwd,tool_input:{command:"git push origin main"}}')
220
+ hook_output=$(EAGLE_MEM_PROJECT="$hook_project" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$hook_input")
221
+ assert_contains "$hook_output" '"permissionDecision":"deny"' "pre-tool-use should block git push with a pending matching feature"
222
+ assert_contains "$hook_output" "hook-ussd" "pre-tool-use denial should mention the matching USSD feature"
223
+ assert_not_contains "$hook_output" "hook-telegram" "pre-tool-use denial should not include Telegram false positives"
224
+ assert_not_contains "$hook_output" "hook-whatsapp" "pre-tool-use denial should not include WhatsApp false positives"
225
+
226
+ eagle_resolve_pending_feature_verifications "$hook_project" "hook-ussd" "verified" "verified by hook regression" >/dev/null
227
+ hook_after_verify=$(EAGLE_MEM_PROJECT="$hook_project" bash "$ROOT_DIR/hooks/pre-tool-use.sh" <<< "$hook_input")
228
+ assert_not_denied "$hook_after_verify" "same-fingerprint verification should release the pre-tool-use block"
229
+
230
+ echo "feature verification gate regressions passed"
@@ -47,6 +47,45 @@ provider_result=$(EAGLE_MEM_DIR="$provider_home" PATH="$fake_bin:$PATH" bash -c
47
47
  eagle_llm_call 'say ok' 'system' 20
48
48
  ")
49
49
  assert_contains "$provider_result" "claude fallback ok" "agent_cli fallback did not use Claude after Codex failed"
50
+ cat > "$provider_home/config.toml" <<'TOML'
51
+ [provider]
52
+ type = "agent_cli"
53
+ fallback = "auto"
54
+
55
+ [agent_cli]
56
+ preferred = "grok"
57
+ codex_model = ""
58
+ claude_model = ""
59
+ TOML
60
+ EAGLE_MEM_DIR="$provider_home" PATH="$fake_bin:$PATH" bash -c "
61
+ . '$ROOT_DIR/lib/common.sh'
62
+ . '$ROOT_DIR/lib/provider.sh'
63
+ _eagle_agent_cli_target_chain >/dev/null
64
+ "
65
+ grep -q "agent_cli unsupported preferred target: grok" "$provider_home/eagle-mem.log" || {
66
+ echo "unsupported agent_cli target should be logged" >&2
67
+ exit 1
68
+ }
69
+
70
+ EAGLE_MEM_DIR="$provider_home" bash -c "
71
+ . '$ROOT_DIR/lib/common.sh'
72
+ if eagle_is_release_boundary_command 'eagle-mem orchestrate init \"commit and npm publish\"'; then
73
+ echo 'release guard should ignore Eagle Mem orchestration descriptions' >&2
74
+ exit 1
75
+ fi
76
+ if ! eagle_is_release_boundary_command 'npm publish'; then
77
+ echo 'release guard should detect npm publish' >&2
78
+ exit 1
79
+ fi
80
+ if ! eagle_is_release_boundary_command 'git push origin main'; then
81
+ echo 'release guard should detect git push' >&2
82
+ exit 1
83
+ fi
84
+ if eagle_is_release_boundary_command 'npm publish --dry-run'; then
85
+ echo 'release guard should allow npm publish --dry-run' >&2
86
+ exit 1
87
+ fi
88
+ "
50
89
 
51
90
  # PreToolUse parsing + read scoring: repeated large read after modification should emit scored context.
52
91
  hook_home="$tmp_dir/hook-home"
@@ -67,6 +106,10 @@ grep -q 'mod_file}.lock' "$ROOT_DIR/hooks/post-tool-use.sh" || {
67
106
  echo "post-tool-use modification tracker should use a lock directory" >&2
68
107
  exit 1
69
108
  }
109
+ if grep -q '>> "$mod_file"' "$ROOT_DIR/hooks/post-tool-use.sh"; then
110
+ echo "post-tool-use modification tracker should not append to mod_file outside the lock" >&2
111
+ exit 1
112
+ fi
70
113
 
71
114
  # Auto-scan state race: failed background scan must clear the freshness marker.
72
115
  state_home="$tmp_dir/state-home"
@@ -103,5 +146,70 @@ printf '# demo\n' > "$log_repo/README.md"
103
146
  EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/scripts/scan.sh" "$log_repo" >/dev/null
104
147
  log_list=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs list)
105
148
  assert_contains "$log_list" "command=scan" "logs list did not show the scan command run"
149
+ run_path=$(ls -t "$log_home/runs"/*.log 2>/dev/null | sed -n '1p')
150
+ run_id=$(basename "$run_path" .log)
151
+ run_show=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show "$run_id")
152
+ assert_contains "$run_show" "run_start" "logs show by run id did not print the run log"
153
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show /etc/hosts >/dev/null 2>&1; then
154
+ echo "logs show should reject absolute paths outside the run log directory" >&2
155
+ exit 1
156
+ fi
157
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs tail ../memory.db >/dev/null 2>&1; then
158
+ echo "logs tail should reject traversal outside the run log directory" >&2
159
+ exit 1
160
+ fi
161
+ mkdir -p "$log_home/runs/nested"
162
+ printf '[nested] [INFO] nested run\n' > "$log_home/runs/nested/nested.log"
163
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show "$log_home/runs/nested/nested.log" >/dev/null 2>&1; then
164
+ echo "logs show should reject nested absolute paths inside the run log directory" >&2
165
+ exit 1
166
+ fi
167
+ ln -s /etc/hosts "$log_home/runs/symlink.log"
168
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show symlink >/dev/null 2>&1; then
169
+ echo "logs show should reject symlinked run logs" >&2
170
+ exit 1
171
+ fi
172
+ list_with_symlink=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs list)
173
+ case "$list_with_symlink" in
174
+ *symlink*)
175
+ echo "logs list should skip symlinked run logs" >&2
176
+ exit 1
177
+ ;;
178
+ esac
179
+ printf '[old] [INFO] old run\n' > "$log_home/runs/20000101T000000Z-scan-1.log"
180
+ printf '[old] [INFO] old run\n' > "$log_home/runs/20000101T000001Z-scan-2.log"
181
+ EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs prune --days 0 --keep 1 >/dev/null
182
+ remaining_logs=$(find "$log_home/runs" -type f -name '*.log' -print | wc -l | tr -d ' ')
183
+ if [ "$remaining_logs" != "1" ]; then
184
+ echo "logs prune --keep 1 should leave exactly one run log" >&2
185
+ exit 1
186
+ fi
187
+
188
+ mirror_home="$tmp_dir/mirror-home"
189
+ mkdir -p "$mirror_home"
190
+ EAGLE_MEM_DIR="$mirror_home" EAGLE_MEM_LOG="$mirror_home/eagle-mem.log" bash -c "
191
+ . '$ROOT_DIR/lib/common.sh'
192
+ eagle_run_start 'mirror-test' 'project-mirror' '$tmp_dir'
193
+ eagle_log 'WARN' 'mirrored run log detail'
194
+ eagle_run_finish 0 0
195
+ " >/dev/null
196
+ mirror_log=$(ls "$mirror_home/runs"/*.log | sed -n '1p')
197
+ grep -q "mirrored run log detail" "$mirror_log" || {
198
+ echo "eagle_log messages should be mirrored into active run logs" >&2
199
+ exit 1
200
+ }
201
+
202
+ post_home="$tmp_dir/post-home"
203
+ post_repo="$tmp_dir/post-repo"
204
+ mkdir -p "$post_home" "$post_repo"
205
+ EAGLE_MEM_DIR="$post_home" "$ROOT_DIR/db/migrate.sh" >/dev/null
206
+ post_session="session_posttool_123"
207
+ patch_cmd=$'*** Begin Patch\n*** Update File: alpha.txt\n@@\n-old\n+new\n*** Update File: beta.txt\n@@\n-old\n+new\n*** Update File: old-name.txt\n*** Move to: gamma.txt\n@@\n-old\n+new\n*** End Patch'
208
+ post_input=$(jq -nc --arg sid "$post_session" --arg cwd "$post_repo" --arg cmd "$patch_cmd" \
209
+ '{tool_name:"apply_patch",session_id:$sid,cwd:$cwd,tool_input:{command:$cmd},tool_response:{}}')
210
+ EAGLE_MEM_DIR="$post_home" EAGLE_MEM_PROJECT="project-post" bash "$ROOT_DIR/hooks/post-tool-use.sh" <<< "$post_input"
211
+ mod_contents=$(cat "$post_home/mod-tracker/$post_session")
212
+ assert_contains "$mod_contents" "beta.txt" "multi-file apply_patch should track later modified files"
213
+ assert_contains "$mod_contents" "gamma.txt" "apply_patch move destinations should be tracked"
106
214
 
107
215
  echo "reliability guard regressions passed"