eagle-mem 4.10.13 → 4.11.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +20 -20
  3. package/architecture.html +26 -14
  4. package/bin/eagle-mem +4 -0
  5. package/db/039_recall_events.sql +27 -0
  6. package/db/040_graph_decision_nodes.sql +21 -0
  7. package/db/041_graph_semantic_edge_types.sql +21 -0
  8. package/db/042_orchestration_auto_events.sql +23 -0
  9. package/db/043_eagle_events.sql +22 -0
  10. package/docs/agent-compatibility/README.md +38 -0
  11. package/docs/agent-compatibility/claude-code.md +50 -0
  12. package/docs/agent-compatibility/codex.md +51 -0
  13. package/docs/agent-compatibility/opencode.md +71 -0
  14. package/hooks/post-tool-use.sh +8 -0
  15. package/hooks/pre-tool-use.sh +10 -1
  16. package/hooks/session-end.sh +3 -0
  17. package/hooks/session-start.sh +7 -0
  18. package/hooks/stop.sh +10 -1
  19. package/hooks/user-prompt-submit.sh +79 -6
  20. package/integrations/opencode_eagle_mem_plugin.js +387 -0
  21. package/lib/codex-hooks.sh +13 -6
  22. package/lib/common.sh +63 -0
  23. package/lib/db-events.sh +89 -0
  24. package/lib/db-graph.sh +154 -0
  25. package/lib/db-observations.sh +34 -0
  26. package/lib/db-orchestration.sh +149 -0
  27. package/lib/db.sh +2 -0
  28. package/lib/hooks.sh +12 -7
  29. package/lib/opencode-hooks.sh +105 -0
  30. package/lib/provider.sh +2 -2
  31. package/package.json +5 -2
  32. package/scripts/compaction.sh +108 -8
  33. package/scripts/dashboard.sh +372 -0
  34. package/scripts/doctor.sh +30 -3
  35. package/scripts/health.sh +40 -2
  36. package/scripts/help.sh +10 -2
  37. package/scripts/inspect.sh +285 -0
  38. package/scripts/install.sh +31 -7
  39. package/scripts/memories.sh +13 -0
  40. package/scripts/repair.sh +187 -0
  41. package/scripts/replay.sh +248 -0
  42. package/scripts/search.sh +44 -3
  43. package/scripts/statusline-em.sh +34 -7
  44. package/scripts/tasks.sh +34 -0
  45. package/scripts/test.sh +13 -0
  46. package/scripts/uninstall.sh +9 -0
  47. package/scripts/update.sh +18 -2
  48. package/tests/fixtures/agent-hooks/claude-statusline.json +32 -0
  49. package/tests/fixtures/agent-hooks/claude-user-prompt-submit.json +9 -0
  50. package/tests/fixtures/agent-hooks/codex-pre-tool-use.json +10 -0
  51. package/tests/fixtures/agent-hooks/codex-user-prompt-submit.json +7 -0
  52. package/tests/fixtures/agent-hooks/opencode-chat-message.json +36 -0
  53. package/tests/fixtures/agent-hooks/opencode-session-compacting.json +9 -0
  54. package/tests/fixtures/agent-hooks/opencode-todo-updated.json +13 -0
  55. package/tests/fixtures/agent-hooks/opencode-tool-execute-after.json +15 -0
  56. package/tests/fixtures/agent-hooks/opencode-tool-execute-before.json +12 -0
  57. package/tests/test_agent_compatibility_docs_gate.sh +123 -0
  58. package/tests/test_auto_orchestration_detection.sh +109 -0
  59. package/tests/test_claude_stop_hook_registration.sh +56 -0
  60. package/tests/test_codex_hooks_config.sh +73 -0
  61. package/tests/test_compaction_survival_matrix.sh +237 -0
  62. package/tests/test_dashboard.sh +96 -0
  63. package/tests/test_eagle_events.sh +96 -0
  64. package/tests/test_opencode_hooks_config.sh +56 -0
  65. package/tests/test_opencode_plugin_adapter.sh +202 -0
  66. package/tests/test_recall_observability.sh +144 -0
  67. package/tests/test_repair.sh +63 -0
  68. package/tests/test_rust_migration_plan.sh +75 -0
  69. package/tests/test_trust_surfaces.sh +123 -0
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env bash
2
+ # Regression coverage for the OpenCode local plugin bridge.
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-opencode-plugin.XXXXXX")
7
+ trap 'rm -rf "$tmp_dir"' EXIT
8
+
9
+ export HOME="$tmp_dir/home"
10
+ export EAGLE_MEM_DIR="$tmp_dir/eagle-mem"
11
+ export PROJECT_DIR="$tmp_dir/project"
12
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR/hooks" "$PROJECT_DIR" "$tmp_dir/module"
13
+
14
+ fail() {
15
+ echo "opencode plugin adapter test failed: $*" >&2
16
+ exit 1
17
+ }
18
+
19
+ for required in jq node; do
20
+ command -v "$required" >/dev/null 2>&1 || fail "missing required command: $required"
21
+ done
22
+
23
+ cp "$ROOT_DIR/integrations/opencode_eagle_mem_plugin.js" "$tmp_dir/module/eagle-mem-plugin.js"
24
+ printf '%s\n' '{"type":"module"}' > "$tmp_dir/module/package.json"
25
+ export PLUGIN_PATH="$tmp_dir/module/eagle-mem-plugin.js"
26
+
27
+ cat > "$EAGLE_MEM_DIR/hooks/session-start.sh" <<'HOOK'
28
+ #!/usr/bin/env bash
29
+ set -euo pipefail
30
+ payload=$(cat)
31
+ printf '%s\n' "$payload" | jq -c . >> "$EAGLE_MEM_DIR/session-start.jsonl"
32
+ source=$(printf '%s\n' "$payload" | jq -r '.source // "startup"')
33
+ jq -nc --arg ctx "session context: $source" '{"hookSpecificOutput":{"additionalContext":$ctx}}'
34
+ HOOK
35
+
36
+ cat > "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh" <<'HOOK'
37
+ #!/usr/bin/env bash
38
+ set -euo pipefail
39
+ payload=$(cat)
40
+ printf '%s\n' "$payload" | jq -c . >> "$EAGLE_MEM_DIR/user-prompt-submit.jsonl"
41
+ jq -nc '{"hookSpecificOutput":{"additionalContext":"recall context"}}'
42
+ HOOK
43
+
44
+ cat > "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" <<'HOOK'
45
+ #!/usr/bin/env bash
46
+ set -euo pipefail
47
+ payload=$(cat)
48
+ printf '%s\n' "$payload" | jq -c . >> "$EAGLE_MEM_DIR/pre-tool-use.jsonl"
49
+ command_value=$(printf '%s\n' "$payload" | jq -r '.tool_input.command // ""')
50
+ if [ "$command_value" = "deny" ]; then
51
+ jq -nc '{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"blocked by test"}}'
52
+ elif [ "$command_value" = "raw" ]; then
53
+ jq -nc '{"hookSpecificOutput":{"updatedInput":{"command":"safe"}}}'
54
+ else
55
+ jq -nc '{"hookSpecificOutput":{}}'
56
+ fi
57
+ HOOK
58
+
59
+ cat > "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" <<'HOOK'
60
+ #!/usr/bin/env bash
61
+ set -euo pipefail
62
+ payload=$(cat)
63
+ printf '%s\n' "$payload" | jq -c . >> "$EAGLE_MEM_DIR/post-tool-use.jsonl"
64
+ tool_name=$(printf '%s\n' "$payload" | jq -r '.tool_name // ""')
65
+ if [ "$tool_name" = "Read" ]; then
66
+ jq -nc '{"hookSpecificOutput":{"additionalContext":"post context"}}'
67
+ else
68
+ jq -nc '{"hookSpecificOutput":{}}'
69
+ fi
70
+ HOOK
71
+
72
+ cat > "$EAGLE_MEM_DIR/hooks/stop.sh" <<'HOOK'
73
+ #!/usr/bin/env bash
74
+ set -euo pipefail
75
+ cat | jq -c . >> "$EAGLE_MEM_DIR/stop.jsonl"
76
+ jq -nc '{"hookSpecificOutput":{}}'
77
+ HOOK
78
+
79
+ cat > "$EAGLE_MEM_DIR/hooks/session-end.sh" <<'HOOK'
80
+ #!/usr/bin/env bash
81
+ set -euo pipefail
82
+ cat | jq -c . >> "$EAGLE_MEM_DIR/session-end.jsonl"
83
+ jq -nc '{"hookSpecificOutput":{}}'
84
+ HOOK
85
+
86
+ node --input-type=module <<'NODE'
87
+ import assert from "node:assert/strict";
88
+ import { pathToFileURL } from "node:url";
89
+
90
+ const pluginUrl = pathToFileURL(process.env.PLUGIN_PATH).href;
91
+ const mod = await import(pluginUrl);
92
+ const hooks = await mod.default({
93
+ project: { name: "opencode-fixture-project" },
94
+ directory: process.env.PROJECT_DIR,
95
+ worktree: process.env.PROJECT_DIR,
96
+ });
97
+
98
+ const chatOutput = {
99
+ message: {
100
+ id: "message-user-1",
101
+ sessionID: "session-1",
102
+ role: "user",
103
+ time: { created: 1 },
104
+ agent: "build",
105
+ model: { providerID: "openai", modelID: "gpt-5" },
106
+ },
107
+ parts: [
108
+ {
109
+ id: "part-user-1",
110
+ sessionID: "session-1",
111
+ messageID: "message-user-1",
112
+ type: "text",
113
+ text: "Review auth memory",
114
+ },
115
+ ],
116
+ };
117
+
118
+ await hooks["chat.message"]({ sessionID: "session-1" }, chatOutput);
119
+ assert(chatOutput.parts.some((part) => part.text.includes("session context: startup")));
120
+ assert(chatOutput.parts.some((part) => part.text.includes("recall context")));
121
+
122
+ const beforeOutput = { args: { command: "raw" } };
123
+ await hooks["tool.execute.before"]({ tool: "bash", sessionID: "session-1", callID: "call-1" }, beforeOutput);
124
+ assert.equal(beforeOutput.args.command, "safe");
125
+
126
+ await assert.rejects(
127
+ () => hooks["tool.execute.before"]({ tool: "bash", sessionID: "session-1", callID: "call-2" }, { args: { command: "deny" } }),
128
+ /blocked by test/,
129
+ );
130
+
131
+ const afterOutput = { title: "Read", output: "file body", metadata: {} };
132
+ await hooks["tool.execute.after"](
133
+ { tool: "read", sessionID: "session-1", callID: "call-3", args: { filePath: "/tmp/auth.ts" } },
134
+ afterOutput,
135
+ );
136
+ assert(afterOutput.output.includes("post context"));
137
+ assert.equal(afterOutput.metadata.eagleMemContext, "post context");
138
+
139
+ await hooks.event({
140
+ event: {
141
+ type: "todo.updated",
142
+ properties: {
143
+ sessionID: "session-1",
144
+ todos: [{ content: "Ship OpenCode", status: "in_progress", priority: "high" }],
145
+ },
146
+ },
147
+ });
148
+
149
+ const compactOutput = {};
150
+ await hooks["experimental.session.compacting"]({ sessionID: "session-1" }, compactOutput);
151
+ assert(compactOutput.context.some((entry) => entry.includes("Eagle Mem Compaction Context")));
152
+
153
+ await hooks.event({
154
+ event: {
155
+ type: "message.updated",
156
+ properties: {
157
+ sessionID: "session-1",
158
+ info: { id: "message-assistant-1", sessionID: "session-1", role: "assistant" },
159
+ },
160
+ },
161
+ });
162
+ await hooks.event({
163
+ event: {
164
+ type: "message.part.updated",
165
+ properties: {
166
+ sessionID: "session-1",
167
+ part: { messageID: "message-assistant-1", sessionID: "session-1", type: "text", text: "Assistant done" },
168
+ time: 1,
169
+ },
170
+ },
171
+ });
172
+ await hooks.event({ event: { type: "session.idle", properties: { sessionID: "session-1" } } });
173
+ await hooks.event({ event: { type: "session.deleted", properties: { sessionID: "session-1" } } });
174
+
175
+ const shellOutput = {};
176
+ await hooks["shell.env"]({}, shellOutput);
177
+ assert.equal(shellOutput.env.EAGLE_AGENT_SOURCE, "opencode");
178
+ assert.equal(shellOutput.env.EAGLE_MEM_DIR, process.env.EAGLE_MEM_DIR);
179
+ NODE
180
+
181
+ jq -e 'select(.hook_event_name == "UserPromptSubmit" and .prompt == "Review auth memory" and .agent == "opencode")' "$EAGLE_MEM_DIR/user-prompt-submit.jsonl" >/dev/null \
182
+ || fail "UserPromptSubmit payload did not preserve the raw prompt"
183
+
184
+ jq -e 'select(.hook_event_name == "PreToolUse" and .tool_name == "Bash" and .tool_input.command == "raw" and .agent == "opencode")' "$EAGLE_MEM_DIR/pre-tool-use.jsonl" >/dev/null \
185
+ || fail "PreToolUse payload did not normalize bash command input"
186
+
187
+ jq -e 'select(.hook_event_name == "PostToolUse" and .tool_name == "Read" and .tool_input.file_path == "/tmp/auth.ts")' "$EAGLE_MEM_DIR/post-tool-use.jsonl" >/dev/null \
188
+ || fail "PostToolUse payload did not normalize read filePath input"
189
+
190
+ jq -e 'select(.hook_event_name == "TaskCreated" and .task_subject == "Ship OpenCode")' "$EAGLE_MEM_DIR/post-tool-use.jsonl" >/dev/null \
191
+ || fail "todo.updated did not emit a synthetic TaskCreated payload"
192
+
193
+ jq -e 'select(.tool_name == "TaskUpdate" and .tool_input.status == "in_progress")' "$EAGLE_MEM_DIR/post-tool-use.jsonl" >/dev/null \
194
+ || fail "todo.updated did not emit a TaskUpdate payload"
195
+
196
+ jq -e 'select(.hook_event_name == "Stop" and .last_assistant_message == "Assistant done")' "$EAGLE_MEM_DIR/stop.jsonl" >/dev/null \
197
+ || fail "session.idle did not emit Stop with latest assistant text"
198
+
199
+ jq -e 'select(.hook_event_name == "SessionEnd")' "$EAGLE_MEM_DIR/session-end.jsonl" >/dev/null \
200
+ || fail "session.deleted did not emit SessionEnd"
201
+
202
+ echo "opencode plugin adapter test passed"
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bash
2
+ # UserPromptSubmit should persist recall observability facts: what matched and
3
+ # how much context was injected.
4
+ set -euo pipefail
5
+
6
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
7
+
8
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-recall-observability.XXXXXX")
9
+ trap 'rm -rf "$tmp_dir"' EXIT
10
+
11
+ export HOME="$tmp_dir/home"
12
+ export EAGLE_MEM_DIR="$tmp_dir/eagle-mem"
13
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR"
14
+
15
+ . "$ROOT_DIR/lib/common.sh"
16
+ "$ROOT_DIR/db/migrate.sh" >/dev/null
17
+ . "$ROOT_DIR/lib/db.sh"
18
+
19
+ repo="$tmp_dir/repo"
20
+ mkdir -p "$repo"
21
+ project="project-recall"
22
+
23
+ eagle_upsert_session "seed-summary-session" "$project" "$repo" "test-model" "test" "codex" >/dev/null
24
+ eagle_insert_summary \
25
+ "seed-summary-session" \
26
+ "$project" \
27
+ "Implement oauth token refresh" \
28
+ "Read auth client" \
29
+ "OAuth refresh needs rotating tokens" \
30
+ "Added oauth refresh guard" \
31
+ "" \
32
+ "[]" \
33
+ "[]" \
34
+ "oauth recall note" \
35
+ "Do not regress token refresh" \
36
+ "" \
37
+ "auth/client.ts" \
38
+ "codex" >/dev/null
39
+
40
+ memory_file="$tmp_dir/oauth-memory.md"
41
+ cat > "$memory_file" <<'EOF'
42
+ ---
43
+ name: OAuth Memory
44
+ description: OAuth token refresh decision
45
+ type: decision
46
+ originSessionId: seed-summary-session
47
+ ---
48
+ OAuth token refresh must rotate credentials and preserve retry safety.
49
+ EOF
50
+ eagle_capture_agent_memory "$memory_file" "seed-summary-session" "$project" "codex" >/dev/null
51
+
52
+ eagle_db "INSERT INTO code_chunks (project, file_path, language, start_line, end_line, content, mtime)
53
+ VALUES ('$project', 'auth/client.ts', 'typescript', 1, 20,
54
+ 'export function refreshOauthToken() { /* oauth token refresh */ return rotateCredentials(); }',
55
+ 123);" >/dev/null
56
+
57
+ hook_input=$(jq -nc \
58
+ --arg sid "recall-hook-session" \
59
+ --arg cwd "$repo" \
60
+ --arg prompt "Please review oauth token refresh before editing auth client" \
61
+ '{session_id:$sid, cwd:$cwd, prompt:$prompt}')
62
+
63
+ hook_output=$(EAGLE_MEM_PROJECT="$project" EAGLE_MEM_DIR="$EAGLE_MEM_DIR" bash "$ROOT_DIR/hooks/user-prompt-submit.sh" <<< "$hook_input")
64
+ case "$hook_output" in
65
+ *"Eagle Mem recalls"*|*"Relevant code"*) ;;
66
+ *)
67
+ echo "UserPromptSubmit did not inject recall context" >&2
68
+ echo "$hook_output" >&2
69
+ exit 1
70
+ ;;
71
+ esac
72
+
73
+ event_json=$(eagle_db_json "SELECT session_id, project, agent, fts_query, summary_matches, memory_matches,
74
+ code_matches, summary_refs, memory_refs, code_refs,
75
+ injected_chars, injected_token_estimate, status
76
+ FROM recall_events
77
+ WHERE session_id = 'recall-hook-session'
78
+ ORDER BY id DESC
79
+ LIMIT 1;")
80
+
81
+ printf '%s' "$event_json" | jq -e '
82
+ length == 1
83
+ and .[0].project == "project-recall"
84
+ and .[0].status == "ok"
85
+ and .[0].summary_matches >= 1
86
+ and .[0].memory_matches >= 1
87
+ and .[0].code_matches >= 1
88
+ and ((.[0].summary_refs | fromjson) | length >= 1)
89
+ and ((.[0].memory_refs | fromjson) | length >= 1)
90
+ and ((.[0].code_refs | fromjson) | length >= 1)
91
+ and .[0].injected_chars > 0
92
+ and .[0].injected_token_estimate > 0
93
+ ' >/dev/null || {
94
+ echo "recall event did not capture expected match counts" >&2
95
+ echo "$event_json" >&2
96
+ exit 1
97
+ }
98
+
99
+ inspect_json=$(cd "$repo" && EAGLE_MEM_DIR="$EAGLE_MEM_DIR" "$ROOT_DIR/bin/eagle-mem" inspect recall --project "$project" --json)
100
+ printf '%s' "$inspect_json" | jq -e '
101
+ .status == "ok"
102
+ and .action == "recall"
103
+ and (.events | length >= 1)
104
+ ' >/dev/null || {
105
+ echo "inspect recall --json did not return recall events" >&2
106
+ echo "$inspect_json" >&2
107
+ exit 1
108
+ }
109
+
110
+ inspect_last=$(cd "$repo" && EAGLE_MEM_DIR="$EAGLE_MEM_DIR" "$ROOT_DIR/bin/eagle-mem" inspect recall --project "$project" --last)
111
+ case "$inspect_last" in
112
+ *"Recall Inspector"*summaries=1*memories=1*code=1*) ;;
113
+ *)
114
+ echo "inspect recall --last did not render recall counts" >&2
115
+ echo "$inspect_last" >&2
116
+ exit 1
117
+ ;;
118
+ esac
119
+
120
+ replay_json=$(cd "$repo" && EAGLE_MEM_DIR="$EAGLE_MEM_DIR" "$ROOT_DIR/bin/eagle-mem" replay recall-hook-session --json)
121
+ printf '%s' "$replay_json" | jq -e '
122
+ .status == "ok"
123
+ and .session_id == "recall-hook-session"
124
+ and (.recall_events | length == 1)
125
+ and (.recall_events[0].summary_refs | length >= 1)
126
+ and (.recall_events[0].memory_refs | length >= 1)
127
+ and (.recall_events[0].code_refs | length >= 1)
128
+ ' >/dev/null || {
129
+ echo "replay --json did not return recall refs" >&2
130
+ echo "$replay_json" >&2
131
+ exit 1
132
+ }
133
+
134
+ replay_last=$(cd "$repo" && EAGLE_MEM_DIR="$EAGLE_MEM_DIR" "$ROOT_DIR/bin/eagle-mem" replay --project "$project" --last)
135
+ case "$replay_last" in
136
+ *"Replay"*recall-hook-session*"Retrieved refs"*auth/client.ts*) ;;
137
+ *)
138
+ echo "replay --last did not render retrieved refs" >&2
139
+ echo "$replay_last" >&2
140
+ exit 1
141
+ ;;
142
+ esac
143
+
144
+ echo "recall observability regressions passed"
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # Repair command regressions: DB errors need an actionable and safe recovery path.
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+ EAGLE_BIN="$ROOT_DIR/bin/eagle-mem"
7
+
8
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-repair.XXXXXX")
9
+ trap 'rm -rf "$tmp_dir"' EXIT
10
+
11
+ assert_json() {
12
+ local json="$1" filter="$2" message="$3"
13
+ if ! printf '%s' "$json" | jq -e "$filter" >/dev/null; then
14
+ echo "$message" >&2
15
+ echo "$json" >&2
16
+ exit 1
17
+ fi
18
+ }
19
+
20
+ healthy_home="$tmp_dir/healthy"
21
+ mkdir -p "$healthy_home"
22
+ EAGLE_MEM_DIR="$healthy_home" "$ROOT_DIR/db/migrate.sh" >/dev/null
23
+
24
+ healthy_json=$(EAGLE_MEM_DIR="$healthy_home" "$EAGLE_BIN" repair --json)
25
+ assert_json "$healthy_json" '.status == "ok" and .database.integrity.status == "ok"' "repair should no-op on a healthy DB"
26
+
27
+ corrupt_home="$tmp_dir/corrupt"
28
+ mkdir -p "$corrupt_home"
29
+ printf 'not a sqlite database\n' > "$corrupt_home/memory.db"
30
+ before_hash=$(shasum -a 256 "$corrupt_home/memory.db" | awk '{print $1}')
31
+
32
+ dry_json=$(EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" repair --dry-run --json)
33
+ assert_json "$dry_json" '.status == "needs_repair" and .database.integrity.status == "error"' "repair dry-run should produce an actionable repair plan"
34
+ after_dry_hash=$(shasum -a 256 "$corrupt_home/memory.db" | awk '{print $1}')
35
+ [ "$before_hash" = "$after_dry_hash" ] || {
36
+ echo "repair --dry-run must not modify the DB" >&2
37
+ exit 1
38
+ }
39
+
40
+ set +e
41
+ yes_json=$(EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" repair --yes --json 2>/dev/null)
42
+ yes_rc=$?
43
+ set -e
44
+ [ "$yes_rc" -ne 0 ] || {
45
+ echo "repair --yes should not replace unrecoverable garbage with an empty DB" >&2
46
+ echo "$yes_json" >&2
47
+ exit 1
48
+ }
49
+ assert_json "$yes_json" '.status == "error" and (.message | test("left untouched")) and .backup_path != ""' "repair --yes should back up and keep unrecoverable DB untouched"
50
+
51
+ after_yes_hash=$(shasum -a 256 "$corrupt_home/memory.db" | awk '{print $1}')
52
+ [ "$before_hash" = "$after_yes_hash" ] || {
53
+ echo "unrecoverable repair should leave original DB bytes untouched" >&2
54
+ exit 1
55
+ }
56
+
57
+ backup_count=$(find "$corrupt_home/backups" -type f -name 'memory-*.db' 2>/dev/null | wc -l | tr -d ' ')
58
+ [ "$backup_count" -ge 1 ] || {
59
+ echo "repair --yes should create a backup before recovery attempts" >&2
60
+ exit 1
61
+ }
62
+
63
+ echo "repair regressions passed"
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # Static guard for the compatibility-first Rust migration plan.
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+ PLAN="$ROOT_DIR/MIGRATION.md"
7
+
8
+ fail() {
9
+ echo "rust migration plan check failed: $*" >&2
10
+ exit 1
11
+ }
12
+
13
+ require_contains() {
14
+ local pattern="$1"
15
+ local label="$2"
16
+ if ! grep -Eq "$pattern" "$PLAN"; then
17
+ fail "MIGRATION.md does not mention $label"
18
+ fi
19
+ }
20
+
21
+ [ -f "$PLAN" ] || fail "missing MIGRATION.md"
22
+
23
+ require_contains "~/.eagle-mem/memory\\.db" "the existing user database path"
24
+ require_contains "compatibility wrapper" "the Bash compatibility wrapper"
25
+ require_contains "additive migrations" "additive migrations"
26
+ require_contains "never deleted|Never mutate|Never make destructive" "data preservation"
27
+ require_contains "eagle_events" "structured observability events"
28
+ require_contains "ratatui" "Rust TUI library"
29
+ require_contains "crossterm" "Rust terminal backend"
30
+ require_contains "rusqlite|sqlx" "SQLite Rust library options"
31
+ require_contains "Phase 0" "Phase 0"
32
+ require_contains "Phase 1" "Phase 1"
33
+ require_contains "Phase 2" "Phase 2"
34
+ require_contains "Phase 3" "Phase 3"
35
+ require_contains "Phase 4" "Phase 4"
36
+ require_contains "Phase 5" "Phase 5"
37
+ require_contains "Phase 6" "Phase 6"
38
+ require_contains "Phase 7" "Phase 7"
39
+
40
+ for crate in eagle-core eagle-db eagle-hooks eagle-cli eagle-tui; do
41
+ require_contains "$crate" "crate $crate"
42
+ done
43
+
44
+ for table in \
45
+ sessions observations summaries summaries_fts overviews code_chunks code_chunks_fts \
46
+ agent_memories agent_tasks features feature_files pending_feature_verifications \
47
+ graph_nodes graph_edges orchestrations orchestration_lanes recall_events \
48
+ orchestration_auto_events eagle_meta
49
+ do
50
+ require_contains "\\b$table\\b" "table $table"
51
+ done
52
+
53
+ for command in \
54
+ install update uninstall search health doctor repair inspect replay dashboard logs \
55
+ config updates statusline guard overview graph session sessions memories tasks \
56
+ orchestrate curate feature grok-bootstrap test compaction prune scan index help version
57
+ do
58
+ require_contains "\`$command\`|^(- )?$command$| $command" "command $command"
59
+ done
60
+
61
+ for event in \
62
+ hook_started hook_completed memory_created memory_retrieved context_injected \
63
+ compact_started compact_completed index_started index_completed lane_created \
64
+ task_created task_completed graph_rebuilt dashboard_generated replay_generated
65
+ do
66
+ require_contains "$event" "event $event"
67
+ done
68
+
69
+ require_contains "Rust disabled" "Rust disabled test mode"
70
+ require_contains "Rust enabled" "Rust enabled test mode"
71
+ require_contains "sanitized" "sanitized migrated DB fixtures"
72
+ require_contains "docs/agent-compatibility" "agent compatibility docs integration"
73
+
74
+ echo "rust migration plan check passed"
75
+
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # Trust surface regressions: corrupted DBs must be visible, not mistaken for
3
+ # empty memory state.
4
+ set -euo pipefail
5
+
6
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
7
+ EAGLE_BIN="$ROOT_DIR/bin/eagle-mem"
8
+
9
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-trust-surfaces.XXXXXX")
10
+ trap 'rm -rf "$tmp_dir"' EXIT
11
+
12
+ assert_contains() {
13
+ local haystack="$1" needle="$2" message="$3"
14
+ case "$haystack" in
15
+ *"$needle"*) ;;
16
+ *)
17
+ echo "$message" >&2
18
+ echo "Expected to find: $needle" >&2
19
+ echo "Actual: $haystack" >&2
20
+ exit 1
21
+ ;;
22
+ esac
23
+ }
24
+
25
+ assert_json() {
26
+ local json="$1" filter="$2" message="$3"
27
+ if ! printf '%s' "$json" | jq -e "$filter" >/dev/null; then
28
+ echo "$message" >&2
29
+ echo "$json" >&2
30
+ exit 1
31
+ fi
32
+ }
33
+
34
+ strip_ansi() {
35
+ sed -E $'s/\x1b\\[[0-9;]*m//g'
36
+ }
37
+
38
+ repo="$tmp_dir/repo"
39
+ mkdir -p "$repo"
40
+
41
+ healthy_home="$tmp_dir/healthy"
42
+ mkdir -p "$healthy_home"
43
+ EAGLE_MEM_DIR="$healthy_home" "$ROOT_DIR/db/migrate.sh" >/dev/null
44
+
45
+ doctor_ok=$(EAGLE_MEM_DIR="$healthy_home" "$EAGLE_BIN" doctor --json)
46
+ assert_json "$doctor_ok" '.database.integrity.status == "ok"' "doctor should report healthy DB integrity"
47
+
48
+ compaction_ok=$(cd "$repo" && EAGLE_MEM_DIR="$healthy_home" "$EAGLE_BIN" compaction --json)
49
+ assert_json "$compaction_ok" '.status == "ok" and .database.integrity.status == "ok" and .readiness == "weak"' "compaction --json should emit structured healthy output"
50
+
51
+ stats_ok=$(cd "$repo" && EAGLE_MEM_DIR="$healthy_home" "$EAGLE_BIN" search --stats --json)
52
+ assert_json "$stats_ok" '.status == "ok" and .database.integrity.status == "ok" and (.sessions | type == "number")' "search --stats --json should emit structured healthy output"
53
+
54
+ statusline_ok=$(cd "$repo" && EAGLE_MEM_DIR="$healthy_home" "$EAGLE_BIN" statusline | strip_ansi)
55
+ assert_contains "$statusline_ok" "sessions" "healthy statusline should still show memory counts"
56
+
57
+ corrupt_home="$tmp_dir/corrupt"
58
+ mkdir -p "$corrupt_home"
59
+ printf 'not a sqlite database\n' > "$corrupt_home/memory.db"
60
+
61
+ doctor_bad=$(EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" doctor --json)
62
+ assert_json "$doctor_bad" '.overall == "Needs attention" and .database.integrity.status == "error"' "doctor should mark corrupted DB as needing attention"
63
+
64
+ set +e
65
+ health_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" health --json 2>/dev/null)
66
+ health_rc=$?
67
+ set -e
68
+ [ "$health_rc" -ne 0 ] || { echo "health --json should fail on corrupted DB" >&2; exit 1; }
69
+ assert_json "$health_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "health --json should emit a structured DB integrity error"
70
+
71
+ set +e
72
+ stats_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" search --stats --json 2>/dev/null)
73
+ stats_rc=$?
74
+ set -e
75
+ [ "$stats_rc" -ne 0 ] || { echo "search --stats --json should fail on corrupted DB" >&2; exit 1; }
76
+ assert_json "$stats_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "search --stats --json should emit a structured DB integrity error"
77
+
78
+ set +e
79
+ compaction_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" compaction --json 2>/dev/null)
80
+ compaction_rc=$?
81
+ set -e
82
+ [ "$compaction_rc" -ne 0 ] || { echo "compaction --json should fail on corrupted DB" >&2; exit 1; }
83
+ assert_json "$compaction_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "compaction --json should emit a structured DB integrity error"
84
+
85
+ set +e
86
+ tasks_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" tasks --json 2>/dev/null)
87
+ tasks_rc=$?
88
+ set -e
89
+ [ "$tasks_rc" -ne 0 ] || { echo "tasks --json should fail on corrupted DB" >&2; exit 1; }
90
+ assert_json "$tasks_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "tasks --json should emit a structured DB integrity error"
91
+
92
+ set +e
93
+ graph_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" graph 2>&1)
94
+ graph_rc=$?
95
+ set -e
96
+ [ "$graph_rc" -ne 0 ] || { echo "graph should fail on corrupted DB" >&2; exit 1; }
97
+ assert_contains "$graph_bad" "Database integrity check failed" "graph should explain corrupted DB instead of showing an empty graph"
98
+
99
+ set +e
100
+ inspect_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" inspect recall --json 2>/dev/null)
101
+ inspect_rc=$?
102
+ set -e
103
+ [ "$inspect_rc" -ne 0 ] || { echo "inspect recall --json should fail on corrupted DB" >&2; exit 1; }
104
+ assert_json "$inspect_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "inspect recall --json should emit a structured DB integrity error"
105
+
106
+ set +e
107
+ dashboard_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" dashboard --json 2>/dev/null)
108
+ dashboard_rc=$?
109
+ set -e
110
+ [ "$dashboard_rc" -ne 0 ] || { echo "dashboard --json should fail on corrupted DB" >&2; exit 1; }
111
+ assert_json "$dashboard_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "dashboard --json should emit a structured DB integrity error"
112
+
113
+ set +e
114
+ replay_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" replay --last --json 2>/dev/null)
115
+ replay_rc=$?
116
+ set -e
117
+ [ "$replay_rc" -ne 0 ] || { echo "replay --json should fail on corrupted DB" >&2; exit 1; }
118
+ assert_json "$replay_bad" '.status == "error" and .error == "database_integrity" and .database.integrity.status == "error"' "replay --json should emit a structured DB integrity error"
119
+
120
+ statusline_bad=$(cd "$repo" && EAGLE_MEM_DIR="$corrupt_home" "$EAGLE_BIN" statusline | strip_ansi)
121
+ assert_contains "$statusline_bad" "DB error" "statusline should show DB error for corrupted memory state"
122
+
123
+ echo "trust surface regressions passed"