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.
- package/README.md +49 -15
- package/db/023_guardrails.sql +3 -2
- package/db/024_guardrails_unique.sql +46 -0
- package/db/025_pending_feature_verifications.sql +30 -0
- package/db/026_agent_source.sql +18 -0
- package/db/027_feature_verification_fingerprints.sql +9 -0
- package/db/028_agent_artifact_tables.sql +124 -0
- package/hooks/post-tool-use.sh +42 -13
- package/hooks/pre-tool-use.sh +107 -14
- package/hooks/session-end.sh +3 -1
- package/hooks/session-start.sh +64 -15
- package/hooks/stop.sh +115 -21
- package/hooks/user-prompt-submit.sh +14 -5
- package/lib/codex-hooks.sh +194 -0
- package/lib/common.sh +345 -0
- package/lib/db-backfill.sh +3 -3
- package/lib/db-features.sh +222 -0
- package/lib/db-guardrails.sh +2 -1
- package/lib/db-mirrors.sh +79 -43
- package/lib/db-observations.sh +3 -2
- package/lib/db-sessions.sh +11 -7
- package/lib/db-summaries.sh +9 -6
- package/lib/hooks-posttool.sh +8 -6
- package/lib/provider.sh +190 -4
- package/package.json +7 -3
- package/scripts/config.sh +2 -0
- package/scripts/feature.sh +70 -2
- package/scripts/guard.sh +4 -1
- package/scripts/health.sh +5 -1
- package/scripts/help.sh +13 -8
- package/scripts/install.sh +130 -76
- package/scripts/memories.sh +71 -45
- package/scripts/refresh.sh +3 -3
- package/scripts/search.sh +57 -47
- package/scripts/statusline-em.sh +1 -1
- package/scripts/tasks.sh +186 -15
- package/scripts/uninstall.sh +7 -0
- package/scripts/update.sh +51 -7
- package/skills/eagle-mem-memories/SKILL.md +13 -13
- package/skills/eagle-mem-tasks/SKILL.md +21 -15
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -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
|
-
|
|
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=$(
|
|
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
|
-
# ───
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
if [
|
|
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}}'
|
package/hooks/session-end.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
package/hooks/session-start.sh
CHANGED
|
@@ -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;
|
|
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
|
|
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=$(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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"
|