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/README.md +40 -8
- 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/hooks/post-tool-use.sh +41 -13
- package/hooks/pre-tool-use.sh +106 -14
- package/hooks/session-end.sh +2 -1
- package/hooks/session-start.sh +54 -13
- package/hooks/stop.sh +114 -21
- package/hooks/user-prompt-submit.sh +13 -5
- package/lib/codex-hooks.sh +194 -0
- package/lib/common.sh +341 -0
- package/lib/db-features.sh +222 -0
- package/lib/db-guardrails.sh +2 -1
- package/lib/db-mirrors.sh +24 -15
- package/lib/db-observations.sh +3 -2
- package/lib/db-sessions.sh +6 -2
- package/lib/db-summaries.sh +6 -3
- package/lib/hooks-posttool.sh +8 -6
- package/package.json +7 -3
- package/scripts/curate.sh +35 -25
- package/scripts/feature.sh +70 -2
- package/scripts/guard.sh +4 -1
- package/scripts/help.sh +7 -2
- package/scripts/install.sh +118 -76
- package/scripts/memories.sh +21 -18
- package/scripts/search.sh +36 -28
- package/scripts/uninstall.sh +7 -0
- package/scripts/update.sh +31 -6
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
|
+
}
|
package/lib/db-features.sh
CHANGED
|
@@ -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")
|
package/lib/db-guardrails.sh
CHANGED
|
@@ -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
|
|
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
|
|