eagle-mem 4.6.2 → 4.7.1

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.
Files changed (40) hide show
  1. package/README.md +49 -15
  2. package/db/023_guardrails.sql +3 -2
  3. package/db/024_guardrails_unique.sql +46 -0
  4. package/db/025_pending_feature_verifications.sql +30 -0
  5. package/db/026_agent_source.sql +18 -0
  6. package/db/027_feature_verification_fingerprints.sql +9 -0
  7. package/db/028_agent_artifact_tables.sql +124 -0
  8. package/hooks/post-tool-use.sh +42 -13
  9. package/hooks/pre-tool-use.sh +107 -14
  10. package/hooks/session-end.sh +3 -1
  11. package/hooks/session-start.sh +64 -15
  12. package/hooks/stop.sh +115 -21
  13. package/hooks/user-prompt-submit.sh +14 -5
  14. package/lib/codex-hooks.sh +194 -0
  15. package/lib/common.sh +345 -0
  16. package/lib/db-backfill.sh +3 -3
  17. package/lib/db-features.sh +222 -0
  18. package/lib/db-guardrails.sh +2 -1
  19. package/lib/db-mirrors.sh +79 -43
  20. package/lib/db-observations.sh +3 -2
  21. package/lib/db-sessions.sh +11 -7
  22. package/lib/db-summaries.sh +9 -6
  23. package/lib/hooks-posttool.sh +8 -6
  24. package/lib/provider.sh +190 -4
  25. package/package.json +7 -3
  26. package/scripts/config.sh +2 -0
  27. package/scripts/feature.sh +70 -2
  28. package/scripts/guard.sh +4 -1
  29. package/scripts/health.sh +5 -1
  30. package/scripts/help.sh +13 -8
  31. package/scripts/install.sh +130 -76
  32. package/scripts/memories.sh +71 -45
  33. package/scripts/refresh.sh +3 -3
  34. package/scripts/search.sh +57 -47
  35. package/scripts/statusline-em.sh +1 -1
  36. package/scripts/tasks.sh +186 -15
  37. package/scripts/uninstall.sh +7 -0
  38. package/scripts/update.sh +51 -7
  39. package/skills/eagle-mem-memories/SKILL.md +13 -13
  40. package/skills/eagle-mem-tasks/SKILL.md +21 -15
@@ -10,6 +10,7 @@
10
10
  # 6. Stuck loop detection (repeated edits to same file)
11
11
  # ═══════════════════════════════════════════════════════════
12
12
  set +e
13
+ [ "${EAGLE_MEM_DISABLE_HOOKS:-}" = "1" ] && exit 0
13
14
 
14
15
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
16
  LIB_DIR="$SCRIPT_DIR/../lib"
@@ -21,10 +22,11 @@ input=$(eagle_read_stdin)
21
22
  [ -z "$input" ] && exit 0
22
23
 
23
24
  tool_name=$(echo "$input" | jq -r '.tool_name // empty')
25
+ agent=$(eagle_agent_source_from_json "$input")
24
26
 
25
27
  case "$tool_name" in
26
- Bash|Read|Edit|Write) ;;
27
- *) exit 0 ;;
28
+ Read|Edit|Write|apply_patch) ;;
29
+ *) eagle_is_shell_tool "$tool_name" || exit 0 ;;
28
30
  esac
29
31
 
30
32
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
@@ -38,27 +40,102 @@ context=""
38
40
  updated_input=""
39
41
 
40
42
  case "$tool_name" in
41
- Bash)
42
- cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
43
+ Bash|exec_command|shell_command|unified_exec)
44
+ cmd=$(eagle_tool_command_from_json "$input")
43
45
  [ -z "$cmd" ] && exit 0
44
46
 
45
- # ─── Feature verification on git push ─────────────────────
47
+ # ─── Enforced feature verification on release boundaries ───
48
+
49
+ release_changed_files=""
50
+ if eagle_is_release_boundary_command "$cmd"; then
51
+ if [ -n "$cwd" ] && [ -d "$cwd" ]; then
52
+ release_changed_files=$(eagle_changed_files_for_release "$cwd")
53
+ eagle_reconcile_current_feature_verifications "$project" "$cwd" "$session_id" "$tool_name" "Release boundary detected for current repository diff" "$release_changed_files" >/dev/null
54
+ fi
55
+
56
+ pending_rows=$(eagle_list_current_pending_feature_verifications "$project" "$cwd" "$release_changed_files" 8 2>/dev/null)
57
+ pending_count=$(printf '%s\n' "$pending_rows" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
58
+ pending_count=${pending_count:-0}
59
+ if [ "$pending_count" -gt 0 ] 2>/dev/null; then
60
+ block_reason="Eagle Mem blocked this release boundary because ${pending_count} feature verification(s) are pending.
61
+
62
+ Run the affected smoke tests, then resolve them with:
63
+ eagle-mem feature verify <name> --notes \"what passed\"
64
+
65
+ For intentional exceptions:
66
+ eagle-mem feature waive <id> --reason \"why this is safe\"
67
+
68
+ Pending checks:"
69
+ while IFS='|' read -r pid pname pfile preason _ptrigger _pcreated psmoke pfingerprint; do
70
+ [ -z "$pid" ] && continue
71
+ block_reason+="
72
+ #${pid} ${pname}"
73
+ [ -n "$pfile" ] && block_reason+=" (${pfile})"
74
+ [ -n "$preason" ] && block_reason+=" — ${preason}"
75
+ [ -n "$psmoke" ] && block_reason+=" | smoke: ${psmoke}"
76
+ [ -n "$pfingerprint" ] && block_reason+=" | diff: ${pfingerprint}"
77
+ done <<< "$pending_rows"
78
+
79
+ jq -nc --arg reason "$block_reason" '{
80
+ "decision":"block",
81
+ "reason":$reason,
82
+ "hookSpecificOutput":{
83
+ "hookEventName":"PreToolUse",
84
+ "permissionDecision":"deny",
85
+ "permissionDecisionReason":$reason
86
+ }
87
+ }'
88
+ exit 0
89
+ fi
90
+ fi
91
+
92
+ # ─── RTK command rewrite / enforcement ─────────────────
93
+
94
+ rtk_cmd=$(eagle_rtk_rewrite_command "$cmd")
95
+ if [ -n "$rtk_cmd" ]; then
96
+ if [ "$agent" = "codex" ] && ! eagle_raw_bash_unlock_active; then
97
+ reason="Eagle Mem token guard blocked raw shell output.
98
+
99
+ Use RTK so large output is compact before it enters context:
100
+ $rtk_cmd
101
+
102
+ Temporary escape hatch for one-off raw output:
103
+ touch $EAGLE_RAW_BASH_UNLOCK"
104
+ jq -nc --arg reason "$reason" '{
105
+ "decision":"block",
106
+ "reason":$reason,
107
+ "hookSpecificOutput":{
108
+ "hookEventName":"PreToolUse",
109
+ "permissionDecision":"deny",
110
+ "permissionDecisionReason":$reason
111
+ }
112
+ }'
113
+ exit 0
114
+ fi
115
+
116
+ if ! eagle_raw_bash_unlock_active; then
117
+ updated_input=$(echo "$input" | jq --arg cmd "$rtk_cmd" '.tool_input + {"command":$cmd}')
118
+ context+="Eagle Mem token guard: rewrote raw shell command through RTK to reduce context load: ${rtk_cmd}. "
119
+ fi
120
+ fi
121
+
122
+ # ─── Feature verification context for non-blocked pushes ───
46
123
 
47
124
  case "$cmd" in
48
125
  *"git push"*|*"gh pr create"*)
49
126
  has_features=$(eagle_count_active_features "$project")
50
127
  if [ "${has_features:-0}" -gt 0 ]; then
51
- changed_files=""
52
- if [ -n "$cwd" ] && [ -d "$cwd" ]; then
53
- changed_files=$(git -C "$cwd" diff --name-only HEAD 2>/dev/null)
54
- [ -z "$changed_files" ] && changed_files=$(git -C "$cwd" diff --cached --name-only 2>/dev/null)
55
- fi
128
+ changed_files="$release_changed_files"
129
+ if [ -z "$changed_files" ] && [ -n "$cwd" ] && [ -d "$cwd" ]; then
130
+ changed_files=$(eagle_changed_files_for_release "$cwd")
131
+ fi
56
132
 
57
133
  if [ -n "$changed_files" ]; then
58
134
  seen_features=""
59
135
  while IFS= read -r changed_file; do
60
136
  [ -z "$changed_file" ] && continue
61
- fname=$(basename "$changed_file")
137
+ norm_file=$(eagle_project_file_path "$cwd" "$changed_file")
138
+ fname=$(basename "$norm_file")
62
139
 
63
140
  feature_hits=$(eagle_find_feature_for_push "$project" "$fname")
64
141
 
@@ -91,6 +168,7 @@ ${context}================"
91
168
 
92
169
  # ─── Command output filtering (learned rules) ─────────────
93
170
 
171
+ if [ -z "$updated_input" ]; then
94
172
  base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
95
173
  rule=$(eagle_get_command_rule "$project" "$base_cmd" "$cmd")
96
174
 
@@ -117,11 +195,18 @@ ${context}================"
117
195
  ;;
118
196
  esac
119
197
  fi
198
+ fi
120
199
  ;;
121
200
 
122
- Edit|Write)
123
- fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
124
- if [ -n "$fp" ]; then
201
+ Edit|Write|apply_patch)
202
+ target_files=$(echo "$input" | jq -r '.tool_input.file_path // empty')
203
+ if [ "$tool_name" = "apply_patch" ]; then
204
+ patch_cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
205
+ target_files=$(printf '%s\n' "$patch_cmd" | eagle_extract_apply_patch_files | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++')
206
+ fi
207
+ if [ -n "$target_files" ]; then
208
+ while IFS= read -r fp; do
209
+ [ -z "$fp" ] && continue
125
210
  # ─── Guardrail + decision/gotcha surfacing ────────
126
211
  fname=$(basename "$fp")
127
212
  fname_stem="${fname%.*}"
@@ -188,6 +273,7 @@ Edit|Write)
188
273
  partners=${partners%, }
189
274
  context+="Eagle Mem recall: when you change '$(basename "$fp")' you usually also touch: $partners"
190
275
  fi
276
+ done <<< "$target_files"
191
277
  fi
192
278
  ;;
193
279
 
@@ -217,6 +303,13 @@ esac
217
303
 
218
304
  [ -z "$context" ] && [ -z "$updated_input" ] && exit 0
219
305
 
306
+ if [ "$agent" = "codex" ]; then
307
+ # Codex PreToolUse currently supports deny decisions, but not advisory
308
+ # additionalContext or updatedInput. Deny paths above already returned JSON;
309
+ # non-blocking reminders are delivered through SessionStart/UserPromptSubmit.
310
+ exit 0
311
+ fi
312
+
220
313
  if [ -n "$updated_input" ]; then
221
314
  jq -nc --arg ctx "$context" --argjson ui "$updated_input" \
222
315
  '{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":$ui,"additionalContext":$ctx}}'
@@ -5,6 +5,7 @@
5
5
  # Marks the session as completed
6
6
  # ═══════════════════════════════════════════════════════════
7
7
  set +e
8
+ [ "${EAGLE_MEM_DISABLE_HOOKS:-}" = "1" ] && exit 0
8
9
 
9
10
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
11
  LIB_DIR="$SCRIPT_DIR/../lib"
@@ -16,6 +17,7 @@ input=$(eagle_read_stdin)
16
17
  [ -z "$input" ] && exit 0
17
18
 
18
19
  session_id=$(echo "$input" | jq -r '.session_id // empty')
20
+ agent=$(eagle_agent_source_from_json "$input")
19
21
  [ -z "$session_id" ] && exit 0
20
22
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
21
23
 
@@ -30,7 +32,7 @@ if eagle_validate_session_id "$session_id"; then
30
32
  if [ -d "$task_dir" ]; then
31
33
  for task_file in "$task_dir"/*.json; do
32
34
  [ ! -f "$task_file" ] && continue
33
- eagle_capture_claude_task "$task_file" "$session_id" "$project"
35
+ eagle_capture_agent_task "$task_file" "$session_id" "$project" "$agent"
34
36
  done
35
37
  eagle_log "INFO" "SessionEnd: re-synced tasks from $task_dir"
36
38
  fi
@@ -5,6 +5,7 @@
5
5
  # Injects project memory + pending tasks into Claude's context
6
6
  # ═══════════════════════════════════════════════════════════
7
7
  set +e
8
+ [ "${EAGLE_MEM_DISABLE_HOOKS:-}" = "1" ] && exit 0
8
9
 
9
10
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
11
  LIB_DIR="$SCRIPT_DIR/../lib"
@@ -24,6 +25,8 @@ session_id=$(echo "$input" | jq -r '.session_id // empty')
24
25
  cwd=$(echo "$input" | jq -r '.cwd // empty')
25
26
  source_type=$(echo "$input" | jq -r '.source // empty')
26
27
  model=$(echo "$input" | jq -r '.model // empty')
28
+ agent=$(eagle_agent_source_from_json "$input")
29
+ agent_label=$(eagle_agent_label "$agent")
27
30
 
28
31
  [ -z "$session_id" ] && exit 0
29
32
 
@@ -32,9 +35,9 @@ project=$(eagle_project_from_cwd "$cwd")
32
35
 
33
36
  p_esc=$(eagle_sql_escape "$project")
34
37
 
35
- eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
38
+ eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type agent=$agent"
36
39
 
37
- eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
40
+ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type" "$agent"
38
41
  eagle_abandon_stale_sessions "$session_id"
39
42
 
40
43
  # ─── Reset turn counter on compact/clear ─────────────────
@@ -90,7 +93,8 @@ fi
90
93
 
91
94
  # ─── Gather stats ────────────────────────────────────────
92
95
 
93
- stat_sessions=0; stat_summaries=0; stat_with_summaries=0; stat_memories=0
96
+ stat_sessions=0; stat_sessions_claude=0; stat_sessions_codex=0
97
+ stat_summaries=0; stat_with_summaries=0; stat_memories=0
94
98
  stat_tasks_pending=0; stat_tasks_progress=0; stat_tasks_done=0
95
99
  stat_chunks=0; stat_observations=0; stat_plans=0
96
100
  stat_last_active="never"; stat_last_summary=""
@@ -98,6 +102,8 @@ stat_last_active="never"; stat_last_summary=""
98
102
  while IFS='|' read -r key val; do
99
103
  case "$key" in
100
104
  sessions) stat_sessions="$val" ;;
105
+ sessions_claude) stat_sessions_claude="$val" ;;
106
+ sessions_codex) stat_sessions_codex="$val" ;;
101
107
  summaries) stat_summaries="$val" ;;
102
108
  with_summaries) stat_with_summaries="$val" ;;
103
109
  memories) stat_memories="$val" ;;
@@ -132,7 +138,12 @@ eagle_banner="======================================
132
138
  Eagle Mem Recall Ready
133
139
  ======================================
134
140
  Project | $project
141
+ Agent | $agent_label
135
142
  Sessions | $stat_sessions ($stat_with_summaries with summaries)"
143
+ if [ "$stat_sessions_codex" -gt 0 ] || [ "$stat_sessions_claude" -gt 0 ]; then
144
+ eagle_banner+="
145
+ Sources | Claude $stat_sessions_claude, Codex $stat_sessions_codex"
146
+ fi
136
147
  [ "$stat_memories" -gt 0 ] && eagle_banner+="
137
148
  Memories | $stat_memories stored"
138
149
  [ "$stat_plans" -gt 0 ] && eagle_banner+="
@@ -156,6 +167,13 @@ $update_notice
156
167
  "
157
168
  fi
158
169
 
170
+ if [ "$agent" = "codex" ] && [ "${stat_with_summaries:-0}" -eq 0 ] 2>/dev/null; then
171
+ context+="
172
+ === Eagle Mem: Codex Capture Warming Up ===
173
+ Codex hooks are active. End important turns with an <eagle-summary> block so future Claude Code and Codex sessions can recall decisions, gotchas, key files, and next steps from this project.
174
+ "
175
+ fi
176
+
159
177
  # ─── Project overview (capped at 500 chars) ──────────────
160
178
 
161
179
  overview=$(eagle_get_overview "$project")
@@ -188,10 +206,13 @@ if [ -n "$recent" ]; then
188
206
  context+="
189
207
  === Eagle Mem: Recent Recall ===
190
208
  "
191
- while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
209
+ while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files summary_agent; do
192
210
  [ -z "$request" ] && [ -z "$completed" ] && continue
211
+ summary_agent_label=$(eagle_agent_label "$summary_agent")
193
212
  context+="
194
213
  --- $created_at ---"
214
+ [ -n "$summary_agent" ] && context+="
215
+ Source: $summary_agent_label"
195
216
  [ -n "$request" ] && context+="
196
217
  Request: $request"
197
218
  [ -n "$completed" ] && context+="
@@ -213,9 +234,9 @@ fi
213
234
 
214
235
  # ─── Memories (skip if none) ─────────────────────────────
215
236
 
216
- memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at,
237
+ memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at, origin_agent,
217
238
  CAST(julianday('now') - julianday(updated_at) AS INTEGER) as days_ago
218
- FROM claude_memories
239
+ FROM agent_memories
219
240
  WHERE project = '$p_esc'
220
241
  ORDER BY updated_at DESC
221
242
  LIMIT 5;")
@@ -223,8 +244,9 @@ if [ -n "$memories" ]; then
223
244
  context+="
224
245
  === Eagle Mem: Stored Memories ===
225
246
  "
226
- while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
247
+ while IFS='|' read -r mname mtype mdesc _fpath _updated morigin days_ago; do
227
248
  [ -z "$mname" ] && continue
249
+ origin_label=$(eagle_agent_label "$morigin")
228
250
  age_label=""
229
251
  if [ -n "$days_ago" ] && [ "$days_ago" -gt 0 ] 2>/dev/null; then
230
252
  if [ "$days_ago" -eq 1 ]; then
@@ -235,28 +257,29 @@ if [ -n "$memories" ]; then
235
257
  else
236
258
  age_label=" (today)"
237
259
  fi
238
- context+=" - [$mtype] $mname: $mdesc$age_label
260
+ context+=" - [$mtype][$origin_label] $mname: $mdesc$age_label
239
261
  "
240
262
  done <<< "$memories"
241
263
  fi
242
264
 
243
265
  # ─── Plans (skip if none) ────────────────────────────────
244
266
 
245
- plans=$(eagle_list_claude_plans "$project" 3)
267
+ plans=$(eagle_list_agent_plans "$project" 3)
246
268
  if [ -n "$plans" ]; then
247
269
  context+="
248
270
  === Eagle Mem: Plans ===
249
271
  "
250
- while IFS='|' read -r ptitle _pproj _fpath _updated; do
272
+ while IFS='|' read -r ptitle _pproj _fpath _updated porigin; do
251
273
  [ -z "$ptitle" ] && continue
252
- context+=" - $ptitle
274
+ origin_label=$(eagle_agent_label "$porigin")
275
+ context+=" - [$origin_label] $ptitle
253
276
  "
254
277
  done <<< "$plans"
255
278
  fi
256
279
 
257
280
  # ─── Tasks (skip if none) ────────────────────────────────
258
281
 
259
- synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
282
+ synced_tasks=$(eagle_db "SELECT subject, status, blocked_by, origin_agent FROM agent_tasks
260
283
  WHERE project = '$p_esc'
261
284
  AND status IN ('in_progress', 'pending')
262
285
  AND updated_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days')
@@ -268,17 +291,40 @@ if [ -n "$synced_tasks" ]; then
268
291
  context+="
269
292
  === Eagle Mem: Tasks ===
270
293
  "
271
- while IFS='|' read -r tsubject tstatus tblocked; do
294
+ while IFS='|' read -r tsubject tstatus tblocked torigin; do
272
295
  [ -z "$tsubject" ] && continue
296
+ origin_label=$(eagle_agent_label "$torigin")
273
297
  block_marker=""
274
298
  if [ "$tblocked" != "[]" ] && [ -n "$tblocked" ]; then
275
299
  block_marker=" (blocked)"
276
300
  fi
277
- context+=" - [$tstatus] $tsubject$block_marker
301
+ context+=" - [$tstatus][$origin_label] $tsubject$block_marker
278
302
  "
279
303
  done <<< "$synced_tasks"
280
304
  fi
281
305
 
306
+ # ─── Pending feature verifications ───────────────────────
307
+
308
+ pending_features=$(eagle_list_pending_feature_verifications "$project" 10 2>/dev/null)
309
+ if [ -n "$pending_features" ]; then
310
+ context+="
311
+ === Eagle Mem: Pending Feature Verification ===
312
+ Release-boundary commands are blocked until these are verified or waived.
313
+ "
314
+ while IFS='|' read -r pf_id pf_name pf_file pf_reason _pf_trigger _pf_created pf_smoke pfingerprint; do
315
+ [ -z "$pf_id" ] && continue
316
+ context+=" - #${pf_id} ${pf_name}"
317
+ [ -n "$pf_file" ] && context+=" (${pf_file})"
318
+ [ -n "$pf_reason" ] && context+=" — ${pf_reason}"
319
+ [ -n "$pf_smoke" ] && context+=" | smoke: ${pf_smoke}"
320
+ [ -n "$pfingerprint" ] && context+=" | diff: ${pfingerprint}"
321
+ context+="
322
+ "
323
+ done <<< "$pending_features"
324
+ context+="Resolve with: eagle-mem feature verify <name> --notes \"what passed\"; or eagle-mem feature waive <id> --reason \"why safe\".
325
+ "
326
+ fi
327
+
282
328
  # ─── Core files (hot file hints from curator) ───────────
283
329
 
284
330
  hot_files=$(eagle_get_hot_files "$project")
@@ -334,12 +380,15 @@ next_steps: [concrete actions]
334
380
  key_files: [path — role]
335
381
  files_read: [path, ...]
336
382
  files_modified: [path, ...]
383
+ affected_features: [feature, ...]
384
+ verified_features: [feature, ...]
385
+ regression_risks: [risk, ...]
337
386
  </eagle-summary>
338
387
  "
339
388
  fi
340
389
 
341
390
  if [ -n "$context" ]; then
342
- echo "$context"
391
+ eagle_emit_context_for_agent "$agent" "SessionStart" "$context"
343
392
  fi
344
393
 
345
394
  exit 0
package/hooks/stop.sh CHANGED
@@ -7,6 +7,7 @@
7
7
  # LLM enrichment fills in decisions/gotchas/key_files
8
8
  # ═══════════════════════════════════════════════════════════
9
9
  set +e
10
+ [ "${EAGLE_MEM_DISABLE_HOOKS:-}" = "1" ] && exit 0
10
11
 
11
12
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
13
  LIB_DIR="$SCRIPT_DIR/../lib"
@@ -23,6 +24,8 @@ input=$(eagle_read_stdin)
23
24
  session_id=$(echo "$input" | jq -r '.session_id // empty')
24
25
  cwd=$(echo "$input" | jq -r '.cwd // empty')
25
26
  transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')
27
+ last_assistant_message=$(echo "$input" | jq -r '.last_assistant_message // empty')
28
+ agent=$(eagle_agent_source_from_json "$input")
26
29
 
27
30
  [ -z "$session_id" ] && exit 0
28
31
 
@@ -33,25 +36,62 @@ agent_type=$(echo "$input" | jq -r '.agent_type // empty')
33
36
  project=$(eagle_project_from_cwd "$cwd")
34
37
  [ -z "$project" ] && exit 0
35
38
 
36
- eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path"
39
+ eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path agent=$agent"
37
40
 
38
41
  # Ensure session exists (may not if SessionStart didn't fire)
39
- eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
40
-
41
- [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ] && exit 0
42
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" "" "$agent"
43
+
44
+ # Reconcile from git diff, not only from edit-tool hooks. This keeps
45
+ # anti-regression agent-agnostic: Claude, Codex, manual edits, and script edits
46
+ # all become visible before a release boundary.
47
+ if [ -n "$cwd" ] && [ -d "$cwd" ]; then
48
+ changed_files=$(eagle_changed_files_for_release "$cwd")
49
+ if [ -n "$changed_files" ]; then
50
+ eagle_reconcile_current_feature_verifications "$project" "$cwd" "$session_id" "Stop" "Repository diff detected at turn end" "$changed_files" >/dev/null
51
+ fi
52
+ fi
42
53
 
43
54
  # ─── Primary: heuristic extraction from transcript ───────────
44
55
 
45
- request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
46
- | grep -v '<local-command-caveat>' \
47
- | grep -v '<system-reminder>' \
48
- | grep -v '<command-name>' \
49
- | grep -v '<command-message>' \
50
- | grep -v '^\[{' \
51
- | head -1 | cut -c1-500)
56
+ request=""
57
+ heuristic_reads=""
58
+ heuristic_writes=""
59
+
60
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
61
+ request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
62
+ | grep -v '<local-command-caveat>' \
63
+ | grep -v '<system-reminder>' \
64
+ | grep -v '<command-name>' \
65
+ | grep -v '<command-message>' \
66
+ | grep -v '^\[{' \
67
+ | head -1 | cut -c1-500)
68
+
69
+ if [ -z "$request" ]; then
70
+ request=$(jq -r '
71
+ select(.type == "response_item" and .payload.role == "user")
72
+ | .payload.content
73
+ | if type == "string" then .
74
+ elif type == "array" then [.[]? | select(.type == "input_text" or .type == "text") | .text] | join(" ")
75
+ else "" end
76
+ ' "$transcript_path" 2>/dev/null \
77
+ | grep -v '<local-command-caveat>' \
78
+ | grep -v '<system-reminder>' \
79
+ | grep -v '<command-name>' \
80
+ | grep -v '<command-message>' \
81
+ | grep -v '^\[{' \
82
+ | head -1 | cut -c1-500)
83
+ fi
84
+
85
+ if [ -z "$request" ]; then
86
+ request=$(jq -r 'select(.type == "event_msg" and .payload.type == "user_message") | .payload.message // empty' "$transcript_path" 2>/dev/null \
87
+ | grep -v '<local-command-caveat>' \
88
+ | grep -v '<system-reminder>' \
89
+ | head -1 | cut -c1-500)
90
+ fi
52
91
 
53
- heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
54
- heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
92
+ heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
93
+ heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
94
+ fi
55
95
 
56
96
  files_read="[]"
57
97
  files_modified="[]"
@@ -70,17 +110,46 @@ notes=""
70
110
  decisions=""
71
111
  gotchas=""
72
112
  key_files=""
113
+ affected_features=""
114
+ verified_features=""
115
+ regression_risks=""
73
116
 
74
117
  eagle_log "INFO" "Stop: heuristic extraction complete"
75
118
 
76
119
  # ─── Bonus: eagle-summary block overrides where present ──────
77
120
 
78
- text_content=$(jq -rs '
79
- [.[] | select(.type == "assistant")] | last |
80
- if . then
81
- [.message.content[]? | select(.type == "text") | .text] | join("\n")
82
- else "" end
83
- ' "$transcript_path" 2>/dev/null)
121
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
122
+ text_content=$(jq -r -s '
123
+ [.[] | select(.type == "assistant")] | last |
124
+ if . then
125
+ [.message.content[]? | select(.type == "text") | .text] | join("\n")
126
+ else "" end
127
+ ' "$transcript_path" 2>/dev/null)
128
+
129
+ if [ -z "$text_content" ]; then
130
+ text_content=$(jq -r -s '
131
+ def content_text:
132
+ if type == "string" then .
133
+ elif type == "array" then
134
+ [.[]? | select(.type == "output_text" or .type == "text") | (.text // empty)] | join("\n")
135
+ else "" end;
136
+
137
+ (
138
+ [.[] | select(.type == "response_item" and .payload.role == "assistant") | (.payload.content | content_text)]
139
+ | map(select(. != ""))
140
+ | last
141
+ ) // (
142
+ [.[] | select(.type == "event_msg" and .payload.type == "agent_message") | (.payload.message // "")]
143
+ | map(select(. != ""))
144
+ | last
145
+ ) // ""
146
+ ' "$transcript_path" 2>/dev/null)
147
+ fi
148
+ fi
149
+
150
+ if [ -z "$text_content" ]; then
151
+ text_content="$last_assistant_message"
152
+ fi
84
153
 
85
154
  # Strip <private>...</private> blocks
86
155
  text_content=$(echo "$text_content" | sed -E '/<[Pp][Rr][Ii][Vv][Aa][Tt][Ee][^>]*>/,/<\/[Pp][Rr][Ii][Vv][Aa][Tt][Ee][[:space:]]*>/d')
@@ -99,7 +168,7 @@ if [ -n "$summary_block" ]; then
99
168
  $0 ~ "^"f":" {
100
169
  sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
101
170
  }
102
- found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
171
+ found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files|affected_features|verified_features|regression_risks):/ { exit }
103
172
  found { val = val " " $0 }
104
173
  END { if (found) print val }
105
174
  '
@@ -114,6 +183,9 @@ if [ -n "$summary_block" ]; then
114
183
  _val=$(parse_field "$summary_block" "decisions"); [ -n "$_val" ] && decisions="$_val"
115
184
  _val=$(parse_field "$summary_block" "gotchas"); [ -n "$_val" ] && gotchas="$_val"
116
185
  _val=$(parse_field "$summary_block" "key_files"); [ -n "$_val" ] && key_files="$_val"
186
+ _val=$(parse_field "$summary_block" "affected_features"); [ -n "$_val" ] && affected_features="$_val"
187
+ _val=$(parse_field "$summary_block" "verified_features"); [ -n "$_val" ] && verified_features="$_val"
188
+ _val=$(parse_field "$summary_block" "regression_risks"); [ -n "$_val" ] && regression_risks="$_val"
117
189
 
118
190
  # Convert bracket-list to JSON array
119
191
  to_json_array() {
@@ -295,11 +367,33 @@ next_steps=$(echo "$next_steps" | eagle_redact)
295
367
  decisions=$(echo "$decisions" | eagle_redact)
296
368
  gotchas=$(echo "$gotchas" | eagle_redact)
297
369
  key_files=$(echo "$key_files" | eagle_redact)
370
+ notes=$(echo "$notes" | eagle_redact)
371
+ affected_features=$(echo "$affected_features" | eagle_redact)
372
+ verified_features=$(echo "$verified_features" | eagle_redact)
373
+ regression_risks=$(echo "$regression_risks" | eagle_redact)
374
+
375
+ regression_notes=""
376
+ [ -n "$affected_features" ] && regression_notes+="affected_features: $affected_features"
377
+ if [ -n "$verified_features" ]; then
378
+ [ -n "$regression_notes" ] && regression_notes+="; "
379
+ regression_notes+="verified_features: $verified_features"
380
+ fi
381
+ if [ -n "$regression_risks" ]; then
382
+ [ -n "$regression_notes" ] && regression_notes+="; "
383
+ regression_notes+="regression_risks: $regression_risks"
384
+ fi
385
+ if [ -n "$regression_notes" ]; then
386
+ if [ -n "$notes" ]; then
387
+ notes="${notes}; ${regression_notes}"
388
+ else
389
+ notes="$regression_notes"
390
+ fi
391
+ fi
298
392
 
299
393
  # ─── Write to database ─────────────────────────────────────
300
394
 
301
395
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
302
- if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"; then
396
+ if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent"; then
303
397
  eagle_log "INFO" "Stop: summary saved for session=$session_id"
304
398
  else
305
399
  eagle_log "ERROR" "Stop: summary insert FAILED for session=$session_id — check DB constraints"