eagle-mem 4.10.12 → 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,18 @@ 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
+
7
19
  ## v4.10.12 Spectral Review Closure
8
20
 
9
21
  This patch closes the multi-CLI Spectral review findings on v4.10.11:
@@ -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
@@ -1160,10 +1160,10 @@ eagle_is_release_boundary_command() {
1160
1160
  function has_dry_run_flag(line) {
1161
1161
  return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
1162
1162
  }
1163
- function is_eagle_feature_command(line) {
1164
- 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:]]|$)/
1165
1165
  }
1166
- is_eagle_feature_command($0) { next }
1166
+ is_eagle_state_command($0) { next }
1167
1167
  /(^|[[:space:]])gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)/ ||
1168
1168
  /(^|[[:space:]])npm[[:space:]]+publish([[:space:]]|$)/ ||
1169
1169
  /(^|[[:space:]])pnpm[[:space:]]+publish([[:space:]]|$)/ ||
@@ -1185,10 +1185,10 @@ eagle_is_release_boundary_command() {
1185
1185
  function has_dry_run_flag(line) {
1186
1186
  return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
1187
1187
  }
1188
- function is_eagle_feature_command(line) {
1189
- 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:]]|$)/
1190
1190
  }
1191
- is_eagle_feature_command($0) { next }
1191
+ is_eagle_state_command($0) { next }
1192
1192
  /(^|[[:space:]])git[[:space:]]+push([[:space:]]|$)/ {
1193
1193
  if (!has_dry_run_flag($0)) found = 1
1194
1194
  }
@@ -1301,10 +1301,10 @@ eagle_fts_sanitize() {
1301
1301
  printf '%s' "$1" | sed 's/[^A-Za-z0-9_]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
1302
1302
  }
1303
1303
 
1304
- # Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
1304
+ # Escape SQL LIKE wildcards and the escape character so literal filenames match exactly.
1305
1305
  # Apply AFTER eagle_sql_escape, since this only handles LIKE metacharacters.
1306
1306
  eagle_like_escape() {
1307
- printf '%s' "$1" | sed 's/%/\\%/g; s/_/\\_/g'
1307
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/%/\\%/g; s/_/\\_/g'
1308
1308
  }
1309
1309
 
1310
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.12",
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/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 |
@@ -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"
@@ -67,6 +67,26 @@ grep -q "agent_cli unsupported preferred target: grok" "$provider_home/eagle-mem
67
67
  exit 1
68
68
  }
69
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
+ "
89
+
70
90
  # PreToolUse parsing + read scoring: repeated large read after modification should emit scored context.
71
91
  hook_home="$tmp_dir/hook-home"
72
92
  repo="$tmp_dir/repo"