eagle-mem 4.6.1 → 4.7.0

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/lib/common.sh CHANGED
@@ -12,6 +12,11 @@ EAGLE_SKILLS_DIR="$HOME/.claude/skills"
12
12
  EAGLE_CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"
13
13
  EAGLE_CLAUDE_PLANS_DIR="$HOME/.claude/plans"
14
14
  EAGLE_CLAUDE_TASKS_DIR="$HOME/.claude/tasks"
15
+ EAGLE_CODEX_DIR="${EAGLE_CODEX_DIR:-$HOME/.codex}"
16
+ EAGLE_CODEX_CONFIG="${EAGLE_CODEX_CONFIG:-$EAGLE_CODEX_DIR/config.toml}"
17
+ EAGLE_CODEX_HOOKS="${EAGLE_CODEX_HOOKS:-$EAGLE_CODEX_DIR/hooks.json}"
18
+ EAGLE_CODEX_AGENTS_MD="${EAGLE_CODEX_AGENTS_MD:-$EAGLE_CODEX_DIR/AGENTS.md}"
19
+ EAGLE_RAW_BASH_UNLOCK="${EAGLE_RAW_BASH_UNLOCK:-/tmp/eagle-mem-raw-bash-unlock}"
15
20
 
16
21
  eagle_log() {
17
22
  local level="$1"
@@ -63,6 +68,289 @@ eagle_project_from_cwd() {
63
68
  fi
64
69
  }
65
70
 
71
+ eagle_project_file_path() {
72
+ local cwd="${1:-$(pwd)}"
73
+ local file_path="${2:-}"
74
+
75
+ [ -z "$file_path" ] && return 0
76
+
77
+ case "$file_path" in
78
+ ./*) file_path="${file_path#./}" ;;
79
+ esac
80
+
81
+ case "$file_path" in
82
+ /*)
83
+ local git_root
84
+ git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
85
+ if [ -n "$git_root" ]; then
86
+ case "$file_path" in
87
+ "$git_root"/*)
88
+ printf '%s\n' "${file_path#$git_root/}"
89
+ return 0
90
+ ;;
91
+ esac
92
+ fi
93
+ case "$file_path" in
94
+ "$cwd"/*)
95
+ printf '%s\n' "${file_path#$cwd/}"
96
+ return 0
97
+ ;;
98
+ esac
99
+ ;;
100
+ esac
101
+
102
+ printf '%s\n' "$file_path"
103
+ }
104
+
105
+ eagle_extract_apply_patch_files() {
106
+ sed -n -E 's/^\*\*\* (Add|Update|Delete) File: //p'
107
+ }
108
+
109
+ eagle_agent_source() {
110
+ local agent="${EAGLE_AGENT_SOURCE:-${EAGLE_AGENT:-}}"
111
+ case "$agent" in
112
+ codex|openai-codex) echo "codex" ;;
113
+ claude|claude-code|cloud-code) echo "claude-code" ;;
114
+ *) echo "claude-code" ;;
115
+ esac
116
+ }
117
+
118
+ eagle_agent_source_from_json() {
119
+ local input="${1:-}"
120
+ local configured="${EAGLE_AGENT_SOURCE:-${EAGLE_AGENT:-}}"
121
+ if [ -n "$configured" ]; then
122
+ eagle_agent_source
123
+ return
124
+ fi
125
+
126
+ local transcript_path turn_id tool_name
127
+ transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
128
+ turn_id=$(printf '%s' "$input" | jq -r '.turn_id // empty' 2>/dev/null)
129
+ tool_name=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)
130
+
131
+ case "$transcript_path" in
132
+ "$HOME/.codex/"*|*/.codex/*) echo "codex"; return ;;
133
+ "$HOME/.claude/"*|*/.claude/*) echo "claude-code"; return ;;
134
+ esac
135
+ [ -n "$turn_id" ] && { echo "codex"; return; }
136
+ [ "$tool_name" = "apply_patch" ] && { echo "codex"; return; }
137
+
138
+ echo "claude-code"
139
+ }
140
+
141
+ eagle_agent_label() {
142
+ case "${1:-$(eagle_agent_source)}" in
143
+ codex) echo "Codex" ;;
144
+ *) echo "Claude Code" ;;
145
+ esac
146
+ }
147
+
148
+ eagle_is_shell_tool() {
149
+ case "${1:-}" in
150
+ Bash|exec_command|shell_command|unified_exec) return 0 ;;
151
+ *) return 1 ;;
152
+ esac
153
+ }
154
+
155
+ eagle_tool_command_from_json() {
156
+ local input="${1:-}"
157
+ printf '%s' "$input" | jq -r '
158
+ .tool_input.command
159
+ // .tool_input.cmd
160
+ // .tool_input.shell_command
161
+ // .tool_input.command_line
162
+ // .tool_input.cmdline
163
+ // (if (.tool_input.argv? | type) == "array" then (.tool_input.argv | join(" ")) else empty end)
164
+ // empty
165
+ ' 2>/dev/null
166
+ }
167
+
168
+ eagle_emit_context_for_agent() {
169
+ local agent="${1:-$(eagle_agent_source)}"
170
+ local hook_event="${2:-}"
171
+ local context="${3:-}"
172
+
173
+ [ -z "$context" ] && return 0
174
+
175
+ if [ "$agent" = "codex" ]; then
176
+ jq -cn \
177
+ --arg event "$hook_event" \
178
+ --arg context "$context" \
179
+ '{
180
+ hookSpecificOutput: {
181
+ hookEventName: $event,
182
+ additionalContext: $context
183
+ }
184
+ }'
185
+ return 0
186
+ fi
187
+
188
+ printf '%s\n' "$context"
189
+ }
190
+
191
+ eagle_rtk_rewrite_command() {
192
+ local cmd="$1"
193
+ command -v rtk >/dev/null 2>&1 || return 1
194
+
195
+ case "$cmd" in
196
+ ""|rtk\ *|*" rtk "*|*"eagle-mem "*|*"git push"*|*"gh pr create"*|*"npm publish"*|*"pnpm publish"*|*"yarn npm publish"*|*"bun publish"*)
197
+ return 1
198
+ ;;
199
+ esac
200
+
201
+ local rewritten
202
+ rewritten=$(rtk rewrite "$cmd" 2>/dev/null | head -1)
203
+ [ -z "$rewritten" ] && return 1
204
+ [ "$rewritten" = "$cmd" ] && return 1
205
+ printf '%s\n' "$rewritten"
206
+ }
207
+
208
+ eagle_raw_bash_unlock_active() {
209
+ [ -f "$EAGLE_RAW_BASH_UNLOCK" ] || return 1
210
+ local now mtime age
211
+ now=$(date +%s)
212
+ mtime=$(stat -f %m "$EAGLE_RAW_BASH_UNLOCK" 2>/dev/null || stat -c %Y "$EAGLE_RAW_BASH_UNLOCK" 2>/dev/null || echo 0)
213
+ age=$((now - mtime))
214
+ [ "$age" -lt 600 ]
215
+ }
216
+
217
+ eagle_sha256_stream() {
218
+ if command -v shasum >/dev/null 2>&1; then
219
+ shasum -a 256 | awk '{print $1}'
220
+ else
221
+ sha256sum | awk '{print $1}'
222
+ fi
223
+ }
224
+
225
+ eagle_is_release_boundary_command() {
226
+ local cmd="$1"
227
+
228
+ if printf '%s\n' "$cmd" \
229
+ | tr '\n' ';' \
230
+ | sed -E 's/(&&|[|][|]|;)/\
231
+ /g' \
232
+ | awk '
233
+ function has_dry_run_flag(line) {
234
+ return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
235
+ }
236
+ /(^|[[:space:]])gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)/ ||
237
+ /(^|[[:space:]])npm[[:space:]]+publish([[:space:]]|$)/ ||
238
+ /(^|[[:space:]])pnpm[[:space:]]+publish([[:space:]]|$)/ ||
239
+ /(^|[[:space:]])yarn[[:space:]]+npm[[:space:]]+publish([[:space:]]|$)/ ||
240
+ /(^|[[:space:]])bun[[:space:]]+publish([[:space:]]|$)/ {
241
+ if (!has_dry_run_flag($0)) found = 1
242
+ }
243
+ END { exit(found ? 0 : 1) }
244
+ '
245
+ then
246
+ return 0
247
+ fi
248
+
249
+ if printf '%s\n' "$cmd" \
250
+ | tr '\n' ';' \
251
+ | sed -E 's/(&&|[|][|]|;)/\
252
+ /g' \
253
+ | awk '
254
+ function has_dry_run_flag(line) {
255
+ return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
256
+ }
257
+ /(^|[[:space:]])git[[:space:]]+push([[:space:]]|$)/ {
258
+ if (!has_dry_run_flag($0)) found = 1
259
+ }
260
+ END { exit(found ? 0 : 1) }
261
+ '
262
+ then
263
+ return 0
264
+ fi
265
+
266
+ return 1
267
+ }
268
+
269
+ eagle_changed_files_for_release() {
270
+ local cwd="${1:-$(pwd)}"
271
+ [ -d "$cwd" ] || return 0
272
+
273
+ {
274
+ git -C "$cwd" diff --name-only HEAD 2>/dev/null
275
+ git -C "$cwd" diff --cached --name-only 2>/dev/null
276
+ local default_branch
277
+ default_branch=$(git -C "$cwd" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
278
+ if [ -n "$default_branch" ] && git -C "$cwd" rev-parse --verify "origin/$default_branch" >/dev/null 2>&1; then
279
+ git -C "$cwd" diff --name-only "origin/$default_branch...HEAD" 2>/dev/null
280
+ fi
281
+ if git -C "$cwd" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
282
+ git -C "$cwd" diff --name-only '@{upstream}...HEAD' 2>/dev/null
283
+ fi
284
+ if ! git -C "$cwd" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
285
+ if ! git -C "$cwd" symbolic-ref --quiet --short refs/remotes/origin/HEAD >/dev/null 2>&1; then
286
+ if git -C "$cwd" rev-parse --verify HEAD~1 >/dev/null 2>&1; then
287
+ git -C "$cwd" diff --name-only HEAD~1..HEAD 2>/dev/null
288
+ fi
289
+ fi
290
+ fi
291
+ } | sed '/^[[:space:]]*$/d' | sort -u
292
+ }
293
+
294
+ eagle_change_fingerprint_for_file() {
295
+ local cwd="${1:-$(pwd)}"
296
+ local file_path="${2:-}"
297
+ [ -z "$file_path" ] && return 0
298
+ [ -d "$cwd" ] || return 0
299
+
300
+ local git_root
301
+ git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
302
+ [ -z "$git_root" ] && return 0
303
+
304
+ local rel_path="$file_path"
305
+ case "$rel_path" in
306
+ ./*) rel_path="${rel_path#./}" ;;
307
+ /*)
308
+ case "$rel_path" in
309
+ "$git_root"/*) rel_path="${rel_path#$git_root/}" ;;
310
+ "$cwd"/*) rel_path="${rel_path#$cwd/}" ;;
311
+ esac
312
+ ;;
313
+ esac
314
+
315
+ local base_ref=""
316
+ local default_branch
317
+ default_branch=$(git -C "$git_root" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
318
+ if [ -n "$default_branch" ] && git -C "$git_root" rev-parse --verify "origin/$default_branch" >/dev/null 2>&1; then
319
+ base_ref=$(git -C "$git_root" merge-base HEAD "origin/$default_branch" 2>/dev/null)
320
+ fi
321
+
322
+ if [ -z "$base_ref" ] && git -C "$git_root" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
323
+ base_ref=$(git -C "$git_root" merge-base HEAD '@{upstream}' 2>/dev/null)
324
+ fi
325
+
326
+ if [ -z "$base_ref" ]; then
327
+ if ! git -C "$git_root" diff --quiet HEAD -- "$rel_path" 2>/dev/null \
328
+ || ! git -C "$git_root" diff --cached --quiet HEAD -- "$rel_path" 2>/dev/null \
329
+ || git -C "$git_root" ls-files --others --exclude-standard -- "$rel_path" 2>/dev/null | grep -qxF "$rel_path"
330
+ then
331
+ base_ref="HEAD"
332
+ elif git -C "$git_root" rev-parse --verify HEAD~1 >/dev/null 2>&1; then
333
+ base_ref="HEAD~1"
334
+ else
335
+ base_ref="HEAD"
336
+ fi
337
+ fi
338
+
339
+ local base_blob="missing"
340
+ if [ -n "$base_ref" ] && git -C "$git_root" cat-file -e "$base_ref:$rel_path" 2>/dev/null; then
341
+ base_blob=$(git -C "$git_root" rev-parse "$base_ref:$rel_path" 2>/dev/null)
342
+ fi
343
+
344
+ local final_blob="missing"
345
+ if [ -f "$git_root/$rel_path" ]; then
346
+ final_blob=$(git -C "$git_root" hash-object -- "$rel_path" 2>/dev/null)
347
+ elif git -C "$git_root" cat-file -e "HEAD:$rel_path" 2>/dev/null; then
348
+ final_blob=$(git -C "$git_root" rev-parse "HEAD:$rel_path" 2>/dev/null)
349
+ fi
350
+
351
+ printf 'file:%s\nbase:%s\nfinal:%s\n' "$rel_path" "$base_blob" "$final_blob" | eagle_sha256_stream
352
+ }
353
+
66
354
  eagle_sql_escape() {
67
355
  printf '%s' "$1" | sed "s/'/''/g"
68
356
  }
@@ -189,6 +477,9 @@ next_steps: [concrete actions]
189
477
  key_files: [path — role]
190
478
  files_read: [path, ...]
191
479
  files_modified: [path, ...]
480
+ affected_features: [feature, ...]
481
+ verified_features: [feature, ...]
482
+ regression_risks: [risk, ...]
192
483
  </eagle-summary>
193
484
  ```
194
485
 
@@ -198,6 +489,7 @@ files_modified: [path, ...]
198
489
  - Emit `<eagle-summary>` before your final text response, every session
199
490
  - When Eagle Mem injects context at SessionStart, attribute it: "Eagle Mem recalls:"
200
491
  - Do not revert decisions surfaced by PostToolUse without asking the user
492
+ - If Eagle Mem reports pending feature verification, verify or waive it before push/PR/publish
201
493
  - Never put raw secrets in the summary — Eagle Mem redacts but defense in depth
202
494
  - If you contradict a loaded memory, update the memory file
203
495
  EAGLE_MD
@@ -233,3 +525,52 @@ eagle_patch_claude_md() {
233
525
 
234
526
  _eagle_claude_md_section >> "$claude_md"
235
527
  }
528
+
529
+ _eagle_codex_agents_section() {
530
+ cat << 'EAGLE_AGENTS'
531
+
532
+ ---
533
+
534
+ ## Eagle Mem — Persistent Memory
535
+
536
+ Eagle Mem hooks are active for Codex in this project. SessionStart and UserPromptSubmit inject project recall from the shared Eagle Mem database at `~/.eagle-mem/memory.db`. PostToolUse records observations and marks affected features for verification.
537
+
538
+ **Rule:** Before your final response in every session, emit an `<eagle-summary>` block so the Stop hook can capture a rich summary.
539
+
540
+ ```
541
+ <eagle-summary>
542
+ request: [what user asked]
543
+ completed: [what shipped]
544
+ learned: [non-obvious discoveries]
545
+ decisions: [choice — why]
546
+ gotchas: [what surprised]
547
+ next_steps: [concrete actions]
548
+ key_files: [path — role]
549
+ files_read: [path, ...]
550
+ files_modified: [path, ...]
551
+ affected_features: [feature, ...]
552
+ verified_features: [feature, ...]
553
+ regression_risks: [risk, ...]
554
+ </eagle-summary>
555
+ ```
556
+
557
+ **How to apply:**
558
+ - Attribute recalled context as "Eagle Mem recalls:" when it is injected
559
+ - Do not revert Eagle Mem-surfaced decisions without asking the user
560
+ - If Eagle Mem reports pending feature verification, verify or waive it before push/PR/publish
561
+ - Never put raw secrets in summaries
562
+ EAGLE_AGENTS
563
+ }
564
+
565
+ eagle_patch_codex_agents_md() {
566
+ local agents_md="$EAGLE_CODEX_AGENTS_MD"
567
+ local marker="## Eagle Mem — Persistent Memory"
568
+
569
+ mkdir -p "$(dirname "$agents_md")"
570
+
571
+ if [ -f "$agents_md" ] && grep -qF "$marker" "$agents_md" 2>/dev/null; then
572
+ return 1
573
+ fi
574
+
575
+ _eagle_codex_agents_section >> "$agents_md"
576
+ }
@@ -58,6 +58,228 @@ eagle_verify_feature() {
58
58
  WHERE project = '$project' AND name = '$name';"
59
59
  }
60
60
 
61
+ eagle_find_feature_impacts_for_file() {
62
+ local project; project=$(eagle_sql_escape "$1")
63
+ local file_path="$2"
64
+ local fname; fname=$(basename "$file_path")
65
+ local file_esc; file_esc=$(eagle_sql_escape "$file_path")
66
+ local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
67
+ local file_like; file_like=$(eagle_like_escape "$file_esc")
68
+ local fname_like; fname_like=$(eagle_like_escape "$fname_esc")
69
+
70
+ eagle_db "SELECT DISTINCT f.id, f.name, f.description, f.last_verified_at,
71
+ ff.file_path,
72
+ (SELECT GROUP_CONCAT(fst.command, '; ')
73
+ FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke_tests
74
+ FROM features f
75
+ JOIN feature_files ff ON ff.feature_id = f.id
76
+ WHERE f.project = '$project'
77
+ 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
+ )
85
+ ORDER BY f.updated_at DESC
86
+ LIMIT 10;"
87
+ }
88
+
89
+ eagle_record_pending_feature_verifications() {
90
+ local project="$1"
91
+ local file_path="$2"
92
+ local session_id="${3:-}"
93
+ local trigger_tool="${4:-}"
94
+ local reason="${5:-File changed}"
95
+ local change_fingerprint="${6:-}"
96
+
97
+ local impacts
98
+ impacts=$(eagle_find_feature_impacts_for_file "$project" "$file_path")
99
+ [ -z "$impacts" ] && return 0
100
+
101
+ local p_esc; p_esc=$(eagle_sql_escape "$project")
102
+ local fp_esc; fp_esc=$(eagle_sql_escape "$file_path")
103
+ local sid_esc; sid_esc=$(eagle_sql_escape "$session_id")
104
+ local tool_esc; tool_esc=$(eagle_sql_escape "$trigger_tool")
105
+ local reason_esc; reason_esc=$(eagle_sql_escape "$reason")
106
+ local fp_hash_esc; fp_hash_esc=$(eagle_sql_escape "$change_fingerprint")
107
+
108
+ while IFS='|' read -r feature_id feature_name _desc _verified _matched_file _smoke; do
109
+ [ -z "$feature_id" ] && continue
110
+ local fid; fid=$(eagle_sql_int "$feature_id")
111
+ local name_esc; name_esc=$(eagle_sql_escape "$feature_name")
112
+
113
+ if [ -n "$change_fingerprint" ]; then
114
+ already_resolved=$(eagle_db "SELECT 1 FROM pending_feature_verifications
115
+ WHERE project = '$p_esc'
116
+ AND feature_id = $fid
117
+ AND file_path = '$fp_esc'
118
+ AND change_fingerprint = '$fp_hash_esc'
119
+ AND status IN ('verified', 'waived')
120
+ LIMIT 1;")
121
+ [ -n "$already_resolved" ] && continue
122
+ fi
123
+
124
+ eagle_db "INSERT INTO pending_feature_verifications
125
+ (project, feature_id, feature_name, file_path, reason, source_session_id, trigger_tool, change_fingerprint)
126
+ VALUES ('$p_esc', $fid, '$name_esc', '$fp_esc', '$reason_esc', '$sid_esc', '$tool_esc', '$fp_hash_esc')
127
+ ON CONFLICT(project, feature_id, file_path) WHERE status = 'pending' DO UPDATE SET
128
+ feature_name = excluded.feature_name,
129
+ reason = excluded.reason,
130
+ source_session_id = excluded.source_session_id,
131
+ trigger_tool = excluded.trigger_tool,
132
+ change_fingerprint = excluded.change_fingerprint,
133
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');" >/dev/null
134
+ done <<< "$impacts"
135
+
136
+ printf '%s\n' "$impacts"
137
+ }
138
+
139
+ eagle_record_current_feature_verifications_for_file() {
140
+ local project="$1"
141
+ local cwd="$2"
142
+ local file_path="$3"
143
+ local session_id="${4:-}"
144
+ local trigger_tool="${5:-}"
145
+ local reason="${6:-File changed}"
146
+
147
+ local norm_file
148
+ norm_file=$(eagle_project_file_path "$cwd" "$file_path")
149
+ [ -z "$norm_file" ] && return 0
150
+
151
+ local fingerprint
152
+ fingerprint=$(eagle_change_fingerprint_for_file "$cwd" "$norm_file")
153
+ eagle_record_pending_feature_verifications "$project" "$norm_file" "$session_id" "$trigger_tool" "$reason" "$fingerprint"
154
+ }
155
+
156
+ eagle_reconcile_current_feature_verifications() {
157
+ local project="$1"
158
+ local cwd="$2"
159
+ local session_id="${3:-}"
160
+ local trigger_tool="${4:-}"
161
+ local reason="${5:-Repository change detected}"
162
+ local changed_files="${6:-}"
163
+
164
+ [ -z "$changed_files" ] && return 0
165
+ while IFS= read -r changed_file; do
166
+ [ -z "$changed_file" ] && continue
167
+ eagle_record_current_feature_verifications_for_file "$project" "$cwd" "$changed_file" "$session_id" "$trigger_tool" "$reason" >/dev/null
168
+ done <<< "$changed_files"
169
+ }
170
+
171
+ eagle_list_current_pending_feature_verifications() {
172
+ local project="$1"
173
+ local cwd="$2"
174
+ local changed_files="${3:-}"
175
+ local limit; limit=$(eagle_sql_int "${4:-20}")
176
+ [ "$limit" -eq 0 ] && limit=20
177
+
178
+ [ -z "$changed_files" ] && return 0
179
+
180
+ local p_esc; p_esc=$(eagle_sql_escape "$project")
181
+ local emitted=0
182
+ local seen="|"
183
+
184
+ while IFS= read -r changed_file; do
185
+ [ -z "$changed_file" ] && continue
186
+ [ "$emitted" -ge "$limit" ] && break
187
+
188
+ local norm_file fingerprint impacts fp_esc fp_hash_esc
189
+ norm_file=$(eagle_project_file_path "$cwd" "$changed_file")
190
+ [ -z "$norm_file" ] && continue
191
+ fingerprint=$(eagle_change_fingerprint_for_file "$cwd" "$norm_file")
192
+ impacts=$(eagle_find_feature_impacts_for_file "$project" "$norm_file")
193
+ [ -z "$impacts" ] && continue
194
+
195
+ fp_esc=$(eagle_sql_escape "$norm_file")
196
+ fp_hash_esc=$(eagle_sql_escape "$fingerprint")
197
+
198
+ while IFS='|' read -r feature_id _feature_name _desc _verified _matched_file _smoke; do
199
+ [ -z "$feature_id" ] && continue
200
+ [ "$emitted" -ge "$limit" ] && break
201
+
202
+ local fid row row_id
203
+ fid=$(eagle_sql_int "$feature_id")
204
+ row=$(eagle_db "SELECT p.id, p.feature_name, p.file_path, p.reason, p.trigger_tool, p.created_at,
205
+ COALESCE((SELECT GROUP_CONCAT(fst.command, '; ')
206
+ FROM feature_smoke_tests fst WHERE fst.feature_id = p.feature_id), '') as smoke_tests,
207
+ substr(p.change_fingerprint, 1, 12) as fingerprint
208
+ FROM pending_feature_verifications p
209
+ WHERE p.project = '$p_esc'
210
+ AND p.feature_id = $fid
211
+ AND p.file_path = '$fp_esc'
212
+ AND p.change_fingerprint = '$fp_hash_esc'
213
+ AND p.status = 'pending'
214
+ ORDER BY p.updated_at DESC, p.id DESC
215
+ LIMIT 1;")
216
+ [ -z "$row" ] && continue
217
+ row_id=${row%%|*}
218
+ case "$seen" in *"|$row_id|"*) continue ;; esac
219
+ seen+="$row_id|"
220
+ printf '%s\n' "$row"
221
+ emitted=$((emitted + 1))
222
+ done <<< "$impacts"
223
+ done <<< "$changed_files"
224
+ }
225
+
226
+ eagle_count_pending_feature_verifications() {
227
+ local project; project=$(eagle_sql_escape "$1")
228
+ eagle_db "SELECT COUNT(*) FROM pending_feature_verifications
229
+ WHERE project = '$project' AND status = 'pending';"
230
+ }
231
+
232
+ eagle_list_pending_feature_verifications() {
233
+ local project; project=$(eagle_sql_escape "$1")
234
+ local limit; limit=$(eagle_sql_int "${2:-20}")
235
+
236
+ eagle_db "SELECT p.id, p.feature_name, p.file_path, p.reason, p.trigger_tool, p.created_at,
237
+ COALESCE((SELECT GROUP_CONCAT(fst.command, '; ')
238
+ FROM feature_smoke_tests fst WHERE fst.feature_id = p.feature_id), '') as smoke_tests,
239
+ substr(p.change_fingerprint, 1, 12) as fingerprint
240
+ FROM pending_feature_verifications p
241
+ WHERE p.project = '$project' AND p.status = 'pending'
242
+ ORDER BY p.updated_at DESC, p.id DESC
243
+ LIMIT $limit;"
244
+ }
245
+
246
+ eagle_resolve_pending_feature_verifications() {
247
+ local project; project=$(eagle_sql_escape "$1")
248
+ local name; name=$(eagle_sql_escape "$2")
249
+ local status; status=$(eagle_sql_escape "${3:-verified}")
250
+ local notes; notes=$(eagle_sql_escape "${4:-}")
251
+
252
+ eagle_db_pipe <<SQL
253
+ UPDATE pending_feature_verifications
254
+ SET status = '$status',
255
+ notes = '$notes',
256
+ resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
257
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
258
+ WHERE project = '$project'
259
+ AND feature_name = '$name'
260
+ AND status = 'pending';
261
+ SELECT changes();
262
+ SQL
263
+ }
264
+
265
+ eagle_waive_pending_feature_verification() {
266
+ local project; project=$(eagle_sql_escape "$1")
267
+ local id; id=$(eagle_sql_int "$2")
268
+ local notes; notes=$(eagle_sql_escape "${3:-}")
269
+
270
+ eagle_db_pipe <<SQL
271
+ UPDATE pending_feature_verifications
272
+ SET status = 'waived',
273
+ notes = '$notes',
274
+ resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
275
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
276
+ WHERE project = '$project'
277
+ AND id = $id
278
+ AND status = 'pending';
279
+ SELECT changes();
280
+ SQL
281
+ }
282
+
61
283
  eagle_get_feature_id() {
62
284
  local project; project=$(eagle_sql_escape "$1")
63
285
  local name; name=$(eagle_sql_escape "$2")
@@ -21,7 +21,8 @@ eagle_add_guardrail() {
21
21
  file_pattern=$(eagle_sql_escape "$file_pattern")
22
22
  eagle_db "INSERT INTO guardrails (project, file_pattern, rule, source)
23
23
  VALUES ('$project', '$file_pattern', '$rule', '$source')
24
- ON CONFLICT(project, source, file_pattern, rule) DO UPDATE SET
24
+ ON CONFLICT DO UPDATE SET
25
+ source = excluded.source,
25
26
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
26
27
  }
27
28