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 +12 -0
- package/hooks/pre-tool-use.sh +1 -2
- package/lib/common.sh +8 -8
- package/lib/db-features.sh +26 -23
- package/package.json +1 -1
- package/scripts/test.sh +1 -0
- package/skills/eagle-mem-feature/SKILL.md +3 -3
- package/tests/test_feature_verification_gate.sh +230 -0
- package/tests/test_reliability_guards.sh +20 -0
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:
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -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" "$
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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).
|
package/lib/db-features.sh
CHANGED
|
@@ -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
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
345
|
-
local
|
|
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
|
|
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
|
|
364
|
-
|
|
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
|
|
382
|
+
AND $file_match_sql
|
|
380
383
|
ORDER BY f.updated_at DESC
|
|
381
384
|
LIMIT 3;"
|
|
382
385
|
}
|
package/package.json
CHANGED
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
|
|
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
|
|
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"
|