eagle-mem 4.10.11 → 4.10.12

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,19 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.10.12 Spectral Review Closure
8
+
9
+ This patch closes the multi-CLI Spectral review findings on v4.10.11:
10
+
11
+ - **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.
12
+ - **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.
13
+ - **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.
14
+ - **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.
15
+ - **Curator JSON Robustness**: Dream Cycle consolidation parsing now tolerates provider text wrapped around the JSON payload while preserving strict `jq` validation.
16
+ - **Regression Coverage**: Expanded reliability tests for log path rejection, log pruning, mirrored run diagnostics, unsupported agent target logging, and multi-file modification tracking.
17
+
18
+ ---
19
+
7
20
  ## v4.10.11 Reliability Guards and Provider Fallback
8
21
 
9
22
  This patch closes the active reliability items that remained after the Dream Cycle hotfix:
package/README.md CHANGED
@@ -146,7 +146,7 @@ Eagle Mem prevents Claude from repeating past mistakes:
146
146
  | `eagle-mem search` | Search past sessions, memories, and code |
147
147
  | `eagle-mem health` | Diagnose pipeline health and background automation |
148
148
  | `eagle-mem doctor` | Verify installed runtime files, hooks, SQLite/FTS5, statusline, manifest, and drift |
149
- | `eagle-mem logs` | Inspect command-scoped `scan`, `index`, and `curate` run logs |
149
+ | `eagle-mem logs` | Inspect and prune command-scoped `scan`, `index`, and `curate` run logs |
150
150
  | `eagle-mem config` | View or change LLM provider and token-guard settings |
151
151
  | `eagle-mem updates` | View or change auto-update policy |
152
152
  | `eagle-mem guard` | Manage regression guardrails for files |
@@ -349,6 +349,8 @@ Provider preference is local-first: Ollama is auto-detected when running, then E
349
349
 
350
350
  Provider calls use an explicit fallback chain by default. For example, `agent_cli` can try the preferred/current agent first and then fall through to another supported local CLI when available. `eagle-mem config`, `eagle-mem health`, and `eagle-mem curate` display the resolved provider path so failed or unavailable agent CLIs are visible instead of appearing as `unknown`.
351
351
 
352
+ Command-scoped run logs live under `~/.eagle-mem/runs`. Use `eagle-mem logs list`, `eagle-mem logs show <run-id>`, `eagle-mem logs tail <run-id>`, and `eagle-mem logs prune --days 14 --keep 50` to inspect or trim them. Log reads are constrained to the run-log directory.
353
+
352
354
  RTK is configured separately from the LLM provider:
353
355
 
354
356
  ```bash
@@ -17,6 +17,50 @@ LIB_DIR="$SCRIPT_DIR/../lib"
17
17
  input=$(eagle_read_stdin)
18
18
  [ -z "$input" ] && exit 0
19
19
 
20
+ eagle_track_modified_path() {
21
+ local path="$1" sid="$2"
22
+ [ -n "$path" ] || return 0
23
+ [ -n "$sid" ] && eagle_validate_session_id "$sid" || return 0
24
+
25
+ local mod_dir mod_file mod_lock mod_tmp attempt
26
+ mod_dir="$EAGLE_MEM_DIR/mod-tracker"
27
+ mkdir -p "$mod_dir" 2>/dev/null || return 0
28
+ mod_file="$mod_dir/${sid}"
29
+ mod_lock="${mod_file}.lock"
30
+
31
+ for attempt in 1 2 3 4 5 6 7 8 9 10; do
32
+ if mkdir "$mod_lock" 2>/dev/null; then
33
+ mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || mod_tmp="${mod_file}.$$"
34
+ (
35
+ cat "$mod_file" 2>/dev/null
36
+ for pending_file in "${mod_file}".pending.*; do
37
+ [ -f "$pending_file" ] && cat "$pending_file" 2>/dev/null
38
+ done
39
+ printf '%s\n' "$path"
40
+ ) | tail -3 > "$mod_tmp"
41
+ mv "$mod_tmp" "$mod_file" 2>/dev/null || rm -f "$mod_tmp"
42
+ rm -f "${mod_file}".pending.* 2>/dev/null || true
43
+ rmdir "$mod_lock" 2>/dev/null || true
44
+ return 0
45
+ fi
46
+ sleep 0.05
47
+ done
48
+
49
+ printf '%s\n' "$path" >> "${mod_file}.pending.$$" 2>/dev/null || true
50
+ eagle_log "WARN" "PostToolUse: mod-tracker lock busy; queued pending modified file for session=$sid"
51
+ }
52
+
53
+ eagle_track_edit_history_path() {
54
+ local path="$1" sid="$2"
55
+ [ -n "$path" ] || return 0
56
+ [ -n "$sid" ] && eagle_validate_session_id "$sid" || return 0
57
+
58
+ local edit_dir
59
+ edit_dir="$EAGLE_MEM_DIR/edit-tracker"
60
+ mkdir -p "$edit_dir" 2>/dev/null || return 0
61
+ printf '%s\n' "$path" >> "$edit_dir/${sid}" 2>/dev/null || true
62
+ }
63
+
20
64
  IFS=$'\x1f' read -r session_id cwd tool_name hook_event <<< \
21
65
  "$(echo "$input" | jq -r '[.session_id, .cwd, .tool_name, .hook_event_name] | map(. // "") | join("\u001f")')"
22
66
  agent=$(eagle_agent_source_from_json "$input")
@@ -167,27 +211,16 @@ esac
167
211
 
168
212
  # ─── Track recent Edit/Write targets for Read-after-modify detection ──
169
213
 
170
- if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
214
+ if [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
171
215
  case "$tool_name" in
172
216
  Edit|Write|apply_patch)
173
- mod_dir="$EAGLE_MEM_DIR/mod-tracker"
174
- mkdir -p "$mod_dir" 2>/dev/null
175
- mod_file="$mod_dir/${session_id}"
176
- mod_lock="${mod_file}.lock"
177
- if mkdir "$mod_lock" 2>/dev/null; then
178
- _mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || _mod_tmp="${mod_file}.$$"
179
- (cat "$mod_file" 2>/dev/null; printf '%s\n' "$fp") | tail -3 > "$_mod_tmp"
180
- mv "$_mod_tmp" "$mod_file" 2>/dev/null || rm -f "$_mod_tmp"
181
- rmdir "$mod_lock" 2>/dev/null || true
182
- else
183
- # If another hook is trimming, append is still safer than losing the edit.
184
- printf '%s\n' "$fp" >> "$mod_file"
185
- fi
186
-
187
- # Full edit history for stuck loop detection (not truncated)
188
- edit_dir="$EAGLE_MEM_DIR/edit-tracker"
189
- mkdir -p "$edit_dir" 2>/dev/null
190
- echo "$fp" >> "$edit_dir/${session_id}"
217
+ modified_paths=$(printf '%s' "$files_modified" | jq -r '.[]?' 2>/dev/null)
218
+ [ -n "$modified_paths" ] || modified_paths="$fp"
219
+ while IFS= read -r modified_path; do
220
+ [ -z "$modified_path" ] && continue
221
+ eagle_track_modified_path "$modified_path" "$session_id"
222
+ eagle_track_edit_history_path "$modified_path" "$session_id"
223
+ done <<< "$modified_paths"
191
224
  ;;
192
225
  esac
193
226
  fi
package/lib/common.sh CHANGED
@@ -106,11 +106,17 @@ eagle_require_sqlite_fts5() {
106
106
  eagle_log() {
107
107
  local level="$1"
108
108
  shift
109
+ local ts msg
110
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
111
+ msg="[$ts] [$level] $*"
109
112
  # Ensure log file is owner-only (may contain debug data)
110
113
  if [ ! -f "$EAGLE_MEM_LOG" ]; then
111
114
  touch "$EAGLE_MEM_LOG" 2>/dev/null && chmod 600 "$EAGLE_MEM_LOG" 2>/dev/null
112
115
  fi
113
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
116
+ echo "$msg" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
117
+ if [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] && [ -n "${EAGLE_RUN_LOG:-}" ] && [ "$EAGLE_RUN_LOG" != "$EAGLE_MEM_LOG" ]; then
118
+ echo "$msg" >> "$EAGLE_RUN_LOG" 2>/dev/null || true
119
+ fi
114
120
  }
115
121
 
116
122
  eagle_run_slug() {
@@ -120,6 +126,39 @@ eagle_run_slug() {
120
126
  | cut -c1-48
121
127
  }
122
128
 
129
+ eagle_run_prune_logs() {
130
+ local days="${1:-${EAGLE_RUN_LOG_RETENTION_DAYS:-14}}"
131
+ local keep="${2:-${EAGLE_RUN_LOG_MAX_COUNT:-50}}"
132
+ local rel_log
133
+
134
+ [ -d "$EAGLE_RUNS_DIR" ] || return 0
135
+ case "$days" in ""|*[!0-9]*) days=14 ;; esac
136
+ case "$keep" in ""|*[!0-9]*) keep=50 ;; esac
137
+
138
+ find "$EAGLE_RUNS_DIR" -type f -name '*.log' -print 2>/dev/null \
139
+ | while IFS= read -r stale_log; do
140
+ rel_log="${stale_log#"$EAGLE_RUNS_DIR"/}"
141
+ case "$rel_log" in
142
+ */*) rm -f -- "$stale_log" 2>/dev/null || true ;;
143
+ esac
144
+ done
145
+
146
+ if [ "$days" -gt 0 ]; then
147
+ find "$EAGLE_RUNS_DIR" -type f -name '*.log' -mtime +"$days" -print 2>/dev/null \
148
+ | while IFS= read -r stale_log; do
149
+ rm -f -- "$stale_log" 2>/dev/null || true
150
+ done
151
+ fi
152
+
153
+ if [ "$keep" -gt 0 ]; then
154
+ ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null \
155
+ | awk -v keep="$keep" 'NR > keep' \
156
+ | while IFS= read -r stale_log; do
157
+ rm -f -- "$stale_log" 2>/dev/null || true
158
+ done
159
+ fi
160
+ }
161
+
123
162
  eagle_run_start() {
124
163
  [ "${EAGLE_RUN_ACTIVE:-0}" = "1" ] && return 0
125
164
 
@@ -129,6 +168,7 @@ eagle_run_start() {
129
168
  [ -n "$slug" ] || slug="command"
130
169
 
131
170
  mkdir -p "$EAGLE_RUNS_DIR" "$EAGLE_MEM_DIR" 2>/dev/null || true
171
+ eagle_run_prune_logs >/dev/null 2>&1 || true
132
172
  EAGLE_RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-${slug}-$$"
133
173
  EAGLE_RUN_LOG="$EAGLE_RUNS_DIR/${EAGLE_RUN_ID}.log"
134
174
  EAGLE_RUN_COMMAND="$command_name"
@@ -818,7 +858,9 @@ eagle_project_file_path() {
818
858
  }
819
859
 
820
860
  eagle_extract_apply_patch_files() {
821
- sed -n -E 's/^\*\*\* (Add|Update|Delete) File: //p'
861
+ sed -n -E \
862
+ -e 's/^\*\*\* (Add|Update|Delete) File: //p' \
863
+ -e 's/^\*\*\* Move to: //p'
822
864
  }
823
865
 
824
866
  eagle_agent_source() {
package/lib/provider.sh CHANGED
@@ -409,7 +409,10 @@ _eagle_agent_cli_target_chain() {
409
409
  claude|claude-code|cloud-code) preferred_target="claude-code" ;;
410
410
  current) preferred_target="$current" ;;
411
411
  auto|"") preferred_target="" ;;
412
- *) preferred_target="$preferred" ;;
412
+ *)
413
+ eagle_log "WARN" "agent_cli unsupported preferred target: $preferred"
414
+ preferred_target=""
415
+ ;;
413
416
  esac
414
417
 
415
418
  for candidate in "$preferred_target" "$current" codex claude-code; do
@@ -469,7 +472,11 @@ _eagle_call_agent_cli() {
469
472
  case "$target" in
470
473
  codex) result=$(_eagle_call_codex_cli "$prompt" "$system" "$max_tokens"); rc=$? ;;
471
474
  claude-code) result=$(_eagle_call_claude_cli "$prompt" "$system" "$max_tokens"); rc=$? ;;
472
- *) rc=1; result="" ;;
475
+ *)
476
+ eagle_log "WARN" "agent_cli unsupported target: $target"
477
+ rc=1
478
+ result=""
479
+ ;;
473
480
  esac
474
481
  if [ "$rc" -eq 0 ] && [ -n "$result" ]; then
475
482
  [ "$tried" -gt 1 ] && eagle_log "INFO" "agent_cli fallback succeeded with $target"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.11",
3
+ "version": "4.10.12",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, Grok, and Google Antigravity",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -39,22 +39,20 @@ EOF
39
39
 
40
40
  parse_consolidations_json() {
41
41
  local result="$1"
42
- local trimmed json_payload
43
-
44
- trimmed=$(printf '%s' "$result" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
45
- case "$trimmed" in
46
- ""|NONE|none|null)
47
- printf '[]\n'
48
- return 0
49
- ;;
50
- esac
51
-
52
- json_payload=$(printf '%s' "$result" \
53
- | sed -e '1s/^[[:space:]]*```json[[:space:]]*$//' \
54
- -e '1s/^[[:space:]]*```[[:space:]]*$//' \
55
- -e '$s/^[[:space:]]*```[[:space:]]*$//')
56
-
57
- printf '%s' "$json_payload" | jq -c '
42
+ printf '%s' "$result" | jq -Rrs -c '
43
+ def text_trim: gsub("^\\s+|\\s+$"; "");
44
+ def parse_payload:
45
+ gsub("\r"; "")
46
+ | gsub("^\\s*```json\\s*\\n"; "")
47
+ | gsub("^\\s*```\\s*\\n"; "")
48
+ | gsub("\\n\\s*```\\s*$"; "")
49
+ | text_trim
50
+ | if . == "" or . == "NONE" or . == "none" or . == "null" then []
51
+ else
52
+ try fromjson catch (
53
+ ([match("(?s)(\\{.*\\}|\\[.*\\])")? | .string][0] // "[]") | fromjson
54
+ )
55
+ end;
58
56
  def trim: gsub("^\\s+|\\s+$"; "");
59
57
  def names:
60
58
  if type == "array" then map(tostring | trim) | map(select(length > 0))
@@ -66,7 +64,8 @@ parse_consolidations_json() {
66
64
  elif type == "object" then (.consolidations // .items // .instructions // [])
67
65
  else []
68
66
  end;
69
- root
67
+ parse_payload
68
+ | root
70
69
  | map({
71
70
  source_names: ((.source_names // .sourceNames // .source_memories // .sourceMemories // .original_names // .originalNames // .originals // .names) | names),
72
71
  new_name: ((.new_name // .newName // .name // .title // "") | tostring | trim),
package/scripts/help.sh CHANGED
@@ -23,7 +23,7 @@ echo -e " ${CYAN}uninstall${RESET} Remove hooks and optionally delete data"
23
23
  echo -e " ${CYAN}search${RESET} Search past sessions, memories, and code"
24
24
  echo -e " ${CYAN}health${RESET} Diagnose pipeline health and background automation"
25
25
  echo -e " ${CYAN}doctor${RESET} Show install footprint, hooks, SQLite, manifest, and runtime drift"
26
- echo -e " ${CYAN}logs${RESET} Inspect command-scoped scan/index/curate logs"
26
+ echo -e " ${CYAN}logs${RESET} Inspect/prune command-scoped scan/index/curate logs"
27
27
  echo -e " ${CYAN}updates${RESET} Auto-update status and policy"
28
28
  echo -e " ${CYAN}overview${RESET} Build or view project overview"
29
29
  echo -e " ${CYAN}session${RESET} Save a manual session summary"
package/scripts/logs.sh CHANGED
@@ -15,36 +15,60 @@ cmd="${1:-list}"
15
15
 
16
16
  show_help() {
17
17
  cat <<EOF
18
- Usage: eagle-mem logs [list|tail|show] [run-id-or-path]
18
+ Usage: eagle-mem logs [list|tail|show|prune] [run-id-or-filename]
19
19
 
20
20
  Commands:
21
- list Show recent command-scoped run logs
22
- tail [id|path] Tail a run log, or the latest run log when omitted
23
- show <id|path> Print a run log
21
+ list Show recent command-scoped run logs
22
+ tail [id|filename] Tail a run log, or the latest run log when omitted
23
+ show <id|filename> Print a run log
24
+ prune [--days N] [--keep N]
25
+ Delete old run logs (defaults: 14 days, latest 50)
24
26
  EOF
25
27
  }
26
28
 
27
29
  resolve_log_path() {
28
30
  local ref="${1:-}"
31
+ local runs_root="${EAGLE_RUNS_DIR%/}" rel_ref
29
32
  if [ -z "$ref" ]; then
30
- ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | sed -n '1p'
31
- return 0
32
- fi
33
- if [ -f "$ref" ]; then
34
- printf '%s\n' "$ref"
33
+ ls -t "$runs_root"/*.log 2>/dev/null | while IFS= read -r candidate; do
34
+ [ -L "$candidate" ] && continue
35
+ [ -f "$candidate" ] && printf '%s\n' "$candidate" && break
36
+ done
35
37
  return 0
36
38
  fi
37
- if [ -f "$EAGLE_RUNS_DIR/$ref" ]; then
38
- printf '%s\n' "$EAGLE_RUNS_DIR/$ref"
39
+
40
+ case "$ref" in
41
+ *$'\n'*|*..*) return 1 ;;
42
+ /*)
43
+ rel_ref="${ref#"$runs_root"/}"
44
+ [ "$rel_ref" != "$ref" ] || return 1
45
+ case "$rel_ref" in ""|*/*) return 1 ;; esac
46
+ [ -L "$runs_root/$rel_ref" ] && return 1
47
+ [ -f "$runs_root/$rel_ref" ] && printf '%s\n' "$runs_root/$rel_ref" && return 0
48
+ return 1
49
+ ;;
50
+ */*) return 1 ;;
51
+ esac
52
+
53
+ if [ ! -L "$runs_root/$ref" ] && [ -f "$runs_root/$ref" ]; then
54
+ printf '%s\n' "$runs_root/$ref"
39
55
  return 0
40
56
  fi
41
- if [ -f "$EAGLE_RUNS_DIR/$ref.log" ]; then
42
- printf '%s\n' "$EAGLE_RUNS_DIR/$ref.log"
57
+ if [ ! -L "$runs_root/$ref.log" ] && [ -f "$runs_root/$ref.log" ]; then
58
+ printf '%s\n' "$runs_root/$ref.log"
43
59
  return 0
44
60
  fi
45
61
  return 1
46
62
  }
47
63
 
64
+ run_log_count() {
65
+ [ -d "$EAGLE_RUNS_DIR" ] || {
66
+ printf '0\n'
67
+ return 0
68
+ }
69
+ find "$EAGLE_RUNS_DIR" -type f -name '*.log' -print 2>/dev/null | wc -l | tr -d ' '
70
+ }
71
+
48
72
  case "$cmd" in
49
73
  -h|--help|help)
50
74
  show_help
@@ -57,6 +81,7 @@ case "$cmd" in
57
81
  exit 0
58
82
  fi
59
83
  ls -t "$EAGLE_RUNS_DIR"/*.log 2>/dev/null | head -20 | while IFS= read -r log_path; do
84
+ [ -L "$log_path" ] && continue
60
85
  first_line=$(sed -n '1p' "$log_path" 2>/dev/null)
61
86
  run_id=$(basename "$log_path" .log)
62
87
  printf ' %s %s\n' "$run_id" "$first_line"
@@ -76,6 +101,41 @@ case "$cmd" in
76
101
  }
77
102
  cat "$log_path"
78
103
  ;;
104
+ prune)
105
+ days="${EAGLE_RUN_LOG_RETENTION_DAYS:-14}"
106
+ keep="${EAGLE_RUN_LOG_MAX_COUNT:-50}"
107
+ while [ $# -gt 0 ]; do
108
+ case "$1" in
109
+ --days)
110
+ days="${2:-}"
111
+ shift 2
112
+ ;;
113
+ --keep)
114
+ keep="${2:-}"
115
+ shift 2
116
+ ;;
117
+ *)
118
+ eagle_err "Unknown prune option: $1"
119
+ show_help
120
+ exit 1
121
+ ;;
122
+ esac
123
+ done
124
+ case "$days" in ""|*[!0-9]*)
125
+ eagle_err "Invalid --days value: $days"
126
+ exit 1
127
+ ;;
128
+ esac
129
+ case "$keep" in ""|*[!0-9]*)
130
+ eagle_err "Invalid --keep value: $keep"
131
+ exit 1
132
+ ;;
133
+ esac
134
+ before=$(run_log_count)
135
+ eagle_run_prune_logs "$days" "$keep"
136
+ after=$(run_log_count)
137
+ eagle_ok "Pruned run logs: before=$before after=$after days=$days keep=$keep"
138
+ ;;
79
139
  *)
80
140
  eagle_err "Unknown logs command: $cmd"
81
141
  show_help
@@ -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"
@@ -47,6 +47,25 @@ 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
+ }
50
69
 
51
70
  # PreToolUse parsing + read scoring: repeated large read after modification should emit scored context.
52
71
  hook_home="$tmp_dir/hook-home"
@@ -67,6 +86,10 @@ grep -q 'mod_file}.lock' "$ROOT_DIR/hooks/post-tool-use.sh" || {
67
86
  echo "post-tool-use modification tracker should use a lock directory" >&2
68
87
  exit 1
69
88
  }
89
+ if grep -q '>> "$mod_file"' "$ROOT_DIR/hooks/post-tool-use.sh"; then
90
+ echo "post-tool-use modification tracker should not append to mod_file outside the lock" >&2
91
+ exit 1
92
+ fi
70
93
 
71
94
  # Auto-scan state race: failed background scan must clear the freshness marker.
72
95
  state_home="$tmp_dir/state-home"
@@ -103,5 +126,70 @@ printf '# demo\n' > "$log_repo/README.md"
103
126
  EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/scripts/scan.sh" "$log_repo" >/dev/null
104
127
  log_list=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs list)
105
128
  assert_contains "$log_list" "command=scan" "logs list did not show the scan command run"
129
+ run_path=$(ls -t "$log_home/runs"/*.log 2>/dev/null | sed -n '1p')
130
+ run_id=$(basename "$run_path" .log)
131
+ run_show=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show "$run_id")
132
+ assert_contains "$run_show" "run_start" "logs show by run id did not print the run log"
133
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show /etc/hosts >/dev/null 2>&1; then
134
+ echo "logs show should reject absolute paths outside the run log directory" >&2
135
+ exit 1
136
+ fi
137
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs tail ../memory.db >/dev/null 2>&1; then
138
+ echo "logs tail should reject traversal outside the run log directory" >&2
139
+ exit 1
140
+ fi
141
+ mkdir -p "$log_home/runs/nested"
142
+ printf '[nested] [INFO] nested run\n' > "$log_home/runs/nested/nested.log"
143
+ 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
144
+ echo "logs show should reject nested absolute paths inside the run log directory" >&2
145
+ exit 1
146
+ fi
147
+ ln -s /etc/hosts "$log_home/runs/symlink.log"
148
+ if EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs show symlink >/dev/null 2>&1; then
149
+ echo "logs show should reject symlinked run logs" >&2
150
+ exit 1
151
+ fi
152
+ list_with_symlink=$(EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs list)
153
+ case "$list_with_symlink" in
154
+ *symlink*)
155
+ echo "logs list should skip symlinked run logs" >&2
156
+ exit 1
157
+ ;;
158
+ esac
159
+ printf '[old] [INFO] old run\n' > "$log_home/runs/20000101T000000Z-scan-1.log"
160
+ printf '[old] [INFO] old run\n' > "$log_home/runs/20000101T000001Z-scan-2.log"
161
+ EAGLE_MEM_DIR="$log_home" bash "$ROOT_DIR/bin/eagle-mem" logs prune --days 0 --keep 1 >/dev/null
162
+ remaining_logs=$(find "$log_home/runs" -type f -name '*.log' -print | wc -l | tr -d ' ')
163
+ if [ "$remaining_logs" != "1" ]; then
164
+ echo "logs prune --keep 1 should leave exactly one run log" >&2
165
+ exit 1
166
+ fi
167
+
168
+ mirror_home="$tmp_dir/mirror-home"
169
+ mkdir -p "$mirror_home"
170
+ EAGLE_MEM_DIR="$mirror_home" EAGLE_MEM_LOG="$mirror_home/eagle-mem.log" bash -c "
171
+ . '$ROOT_DIR/lib/common.sh'
172
+ eagle_run_start 'mirror-test' 'project-mirror' '$tmp_dir'
173
+ eagle_log 'WARN' 'mirrored run log detail'
174
+ eagle_run_finish 0 0
175
+ " >/dev/null
176
+ mirror_log=$(ls "$mirror_home/runs"/*.log | sed -n '1p')
177
+ grep -q "mirrored run log detail" "$mirror_log" || {
178
+ echo "eagle_log messages should be mirrored into active run logs" >&2
179
+ exit 1
180
+ }
181
+
182
+ post_home="$tmp_dir/post-home"
183
+ post_repo="$tmp_dir/post-repo"
184
+ mkdir -p "$post_home" "$post_repo"
185
+ EAGLE_MEM_DIR="$post_home" "$ROOT_DIR/db/migrate.sh" >/dev/null
186
+ post_session="session_posttool_123"
187
+ 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'
188
+ post_input=$(jq -nc --arg sid "$post_session" --arg cwd "$post_repo" --arg cmd "$patch_cmd" \
189
+ '{tool_name:"apply_patch",session_id:$sid,cwd:$cwd,tool_input:{command:$cmd},tool_response:{}}')
190
+ EAGLE_MEM_DIR="$post_home" EAGLE_MEM_PROJECT="project-post" bash "$ROOT_DIR/hooks/post-tool-use.sh" <<< "$post_input"
191
+ mod_contents=$(cat "$post_home/mod-tracker/$post_session")
192
+ assert_contains "$mod_contents" "beta.txt" "multi-file apply_patch should track later modified files"
193
+ assert_contains "$mod_contents" "gamma.txt" "apply_patch move destinations should be tracked"
106
194
 
107
195
  echo "reliability guard regressions passed"