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 +25 -0
- package/README.md +3 -1
- package/hooks/post-tool-use.sh +52 -19
- package/hooks/pre-tool-use.sh +1 -2
- package/lib/common.sh +52 -10
- package/lib/db-features.sh +26 -23
- package/lib/provider.sh +9 -2
- package/package.json +1 -1
- package/scripts/curate.sh +16 -17
- package/scripts/help.sh +1 -1
- package/scripts/logs.sh +73 -13
- package/scripts/test.sh +1 -0
- package/skills/eagle-mem-feature/SKILL.md +3 -3
- package/tests/test_curate_graph_memories.sh +8 -0
- package/tests/test_feature_verification_gate.sh +230 -0
- package/tests/test_reliability_guards.sh +108 -0
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
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -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 "$
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
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
|
@@ -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 "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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).
|
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/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
|
-
*)
|
|
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
|
-
*)
|
|
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
package/scripts/curate.sh
CHANGED
|
@@ -39,22 +39,20 @@ EOF
|
|
|
39
39
|
|
|
40
40
|
parse_consolidations_json() {
|
|
41
41
|
local result="$1"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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-
|
|
18
|
+
Usage: eagle-mem logs [list|tail|show|prune] [run-id-or-filename]
|
|
19
19
|
|
|
20
20
|
Commands:
|
|
21
|
-
list
|
|
22
|
-
tail [id|
|
|
23
|
-
show <id|
|
|
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 "$
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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 "$
|
|
42
|
-
printf '%s\n' "$
|
|
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
|
|
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 |
|
|
@@ -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"
|