eagle-mem 4.13.0 → 4.14.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/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.14.0 Governance Parity & Curator Provenance
8
+
9
+ Two trust-surface features that close the remaining governance gaps from the v4.13.0 review.
10
+
11
+ - **Release-gate parity at the git layer (`eagle-mem gate`).** The feature-verification gate was enforced only in `PreToolUse`, so a bare-shell `git push` or a Grok session (no hook lifecycle) bypassed governance entirely. New `eagle-mem gate`:
12
+ - `gate check` runs the same reconcile-and-block logic as the hook for the current repo (exit 1 when verifications are pending). It **fails open** (exit 0) when Eagle Mem is disabled, has no database, or the directory isn't a recognized project — a git hook must never wedge unrelated pushes.
13
+ - `gate install [--repo PATH] [--force]` installs an **opt-in, repo-local** `pre-push` hook that calls `gate check`, and **refuses to clobber** a pre-existing non-Eagle-Mem hook. `gate uninstall` removes only the managed hook.
14
+ - Bypass a single push with `git push --no-verify` or `EAGLE_MEM_DISABLE_HOOKS=1 git push`.
15
+ - Opt-in by design (Eagle Mem installs agent hooks globally but does not silently add git hooks to every repo). The cross-agent enforcement scope is documented in `docs/agent-compatibility/README.md`.
16
+ - **Provenance + trust gate for curator-generated command rules.** Command rules steer `PreToolUse` output handling (`truncate` appends `| head -N`; `summary` adds a hint). They could be authored by the LLM curator with only format-level validation. Migration 045 adds `command_rules.source` (`manual` | `curator`); the curator tags its rules `curator`; and `eagle_get_command_rule` honors `token_guard.trust_learned_rules` (default **true**, preserving auto-learning). Set it `false` to apply only human-authored rules, so model output cannot silently shape command handling. (Guardrails already carried provenance; the actual command rewrite path is code-derived, not LLM-derived — so the gap was scoped to command rules.)
17
+ - **Performance proposals evaluated, intentionally not implemented.** The deferred Phase 4 micro-optimizations were assessed and found net-negative: a PostToolUse zero-state short-circuit adds a query to the common populated case to save queries only in rare empty projects; UserPromptSubmit query consolidation entangles three differently-ranked FTS queries to save ~1 process spawn; a provider output cache has a near-zero hit rate because enrichment prompts embed unique per-session transcript excerpts. The meaningful token-economy win (the SessionStart injection ceiling) shipped in v4.13.0.
18
+
19
+ New coverage: `tests/test_release_gate_prepush.sh`, `tests/test_command_rule_provenance.sh`.
20
+
21
+ ---
22
+
23
+ ## v4.13.1 Test-Suite & Packaging Hygiene
24
+
25
+ Follow-up cleanup that clears the bounded items left after the v4.13.0 review. No runtime behavior change for normal sessions.
26
+
27
+ - **`eagle-mem test` is now green from a published install.** Two suites failed when run from an installed package rather than a source checkout:
28
+ - *Compaction Survival Matrix* created its fixture repo inside `$ROOT_DIR`; from a published install that path is under `node_modules/`, which the code scanner excludes — so the fixture indexed 0 files and the post-compact "Relevant Code" recall never appeared. The fixture now lives in a neutral system temp dir and is indexed explicitly (no longer relying on a racy background auto-index).
29
+ - *Rust Migration Plan* guards `MIGRATION.md`, a maintainer roadmap intentionally not shipped in the npm package. It now **skips cleanly** when the doc is absent and runs strictly from a source checkout.
30
+ - The test runner learned an honest **"skipped"** state (a dev-only contract test with absent preconditions is neither a pass nor a failure).
31
+ - **Antigravity Python hook test is now in the suite.** `tests/test_antigravity_hook.py` (mocked, stdlib-only) ran only by hand before; it's now a guarded python lane that skips cleanly if `python3` is unavailable.
32
+ - **One source of truth for Claude hook registration.** `install.sh` and `update.sh` each held their own copy of the event→matcher→script mapping (the historical drift class). Extracted `eagle_register_claude_hooks` into `lib/hooks.sh`; both call it (installer verbose, updater quiet). Behavior preserved exactly.
33
+ - **Dead-function sweep — analyzed, nothing removed.** A full cross-surface call-graph (`.sh`/`.py`/`.js`/skills/`bin`, 280 functions) found zero unreferenced functions and no computed-name dispatch; the "no shell caller" candidates are all reached via tests, skills, adapters, or the public CLI. Removing any would be regression risk with no benefit.
34
+
35
+ ---
36
+
7
37
  ## v4.13.0 Full-Spectrum Security & Reliability Hardening
8
38
 
9
39
  A six-lens fix-in-place review of the whole codebase (security, data integrity, reliability, token economy, code quality, architecture). 32 files hardened, 6 new regression suites, full smoke suite green. No behavioral surface for normal sessions changed — recall and capture are byte-for-byte the same; what changed is the failure, concurrency, and trust behavior underneath.
package/bin/eagle-mem CHANGED
@@ -31,6 +31,7 @@ case "$command" in
31
31
  updates) bash "$SCRIPTS_DIR/updates.sh" "$@" ;;
32
32
  statusline) "$SCRIPTS_DIR/statusline-em.sh" "$@" ;;
33
33
  guard) bash "$SCRIPTS_DIR/guard.sh" "$@" ;;
34
+ gate) bash "$SCRIPTS_DIR/gate.sh" "$@" ;;
34
35
  overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
35
36
  graph) bash "$SCRIPTS_DIR/memories.sh" graph "$@" ;;
36
37
  session|sessions)
@@ -0,0 +1,14 @@
1
+ -- Migration 045: Command-rule provenance
2
+ -- command_rules steer output handling (truncate / summary) in the PreToolUse
3
+ -- hook. They can be authored by a human OR generated by the LLM curator from
4
+ -- observed command patterns. Tag who authored each rule so model-derived rules
5
+ -- are auditable and can be excluded from enforcement when desired:
6
+ -- 'manual' — human-authored (default; trusted)
7
+ -- 'curator' — generated by the LLM curator (eagle-mem curate)
8
+ -- Default 'manual' preserves every existing row and the current behavior. The
9
+ -- getter (eagle_get_command_rule) honors `token_guard.trust_learned_rules`
10
+ -- (default true); set it false to apply only 'manual' rules.
11
+
12
+ ALTER TABLE command_rules ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';
13
+
14
+ CREATE INDEX IF NOT EXISTS idx_command_rules_source ON command_rules(source);
@@ -36,3 +36,18 @@ The compatibility gate applies to these file groups:
36
36
  - `CLAUDE.md` when present
37
37
 
38
38
  The test `tests/test_agent_compatibility_docs_gate.sh` enforces that these docs and fixtures exist. When sensitive files are changed in an uncommitted worktree, the same test also requires a changed compatibility doc or agent-hook fixture in the same diff.
39
+
40
+ ## Release-Gate Enforcement Scope
41
+
42
+ The feature-verification release gate (blocking `git push` / `npm publish` / `gh pr create` while changed files map to unverified features) is enforced inside `PreToolUse` for agents that have a hook lifecycle: **Claude Code** and **Codex** (and, via the plugin adapter, **OpenCode**). Two paths historically bypassed it:
43
+
44
+ - **Grok** is skills-and-CLI only — it has no hook lifecycle, so nothing intercepts its tool calls.
45
+ - A **bare-shell `git push`** (any terminal, any agent, or none) never reaches `PreToolUse`.
46
+
47
+ `eagle-mem gate` closes this gap at the git layer so governance is not silently skippable:
48
+
49
+ - `eagle-mem gate check` runs the same reconcile-and-block logic as the hook for the current repo, exiting non-zero when verifications are pending. It **fails open** (exit 0) when Eagle Mem is disabled (`EAGLE_MEM_DISABLE_HOOKS=1`), has no database, or the directory is not a recognized project — a git hook must never wedge unrelated pushes.
50
+ - `eagle-mem gate install [--repo PATH] [--force]` installs an **opt-in, repo-local** `pre-push` hook that calls `gate check`. It refuses to clobber a pre-existing non-Eagle-Mem `pre-push` hook unless `--force` is given. `eagle-mem gate uninstall` removes only the Eagle-Mem-managed hook.
51
+ - Bypass a single push with `git push --no-verify` (skips git hooks) or `EAGLE_MEM_DISABLE_HOOKS=1 git push`.
52
+
53
+ This is opt-in by design: Eagle Mem installs agent hooks globally but does not silently add git hooks to every repository. Coverage proof: `tests/test_release_gate_prepush.sh` (blocks on pending, fails open, install/foreign-protection/uninstall).
@@ -90,9 +90,26 @@ eagle_get_command_rule() {
90
90
  local project; project=$(eagle_sql_escape "$1")
91
91
  local base_cmd; base_cmd=$(eagle_sql_escape "$2")
92
92
  local full_cmd; full_cmd=$(eagle_sql_escape "${3:-$2}")
93
+
94
+ # Provenance gate: command_rules are human-authored ('manual') or generated
95
+ # by the LLM curator ('curator'). When token_guard.trust_learned_rules is
96
+ # false, only human-authored rules may steer command handling — model-derived
97
+ # rules are excluded so LLM output cannot silently shape execution. Default
98
+ # true preserves the auto-learning behavior. COALESCE guards rows written
99
+ # before migration 045 (treated as 'manual').
100
+ local trust
101
+ if declare -F eagle_config_get >/dev/null 2>&1; then
102
+ trust=$(eagle_config_get "token_guard" "trust_learned_rules" "true")
103
+ else
104
+ trust=$(eagle_config_get_light "token_guard" "trust_learned_rules" "true")
105
+ fi
106
+ local source_filter=""
107
+ [ "$trust" = "false" ] && source_filter="AND COALESCE(source, 'manual') = 'manual'"
108
+
93
109
  eagle_db "SELECT strategy, max_lines, reason
94
110
  FROM command_rules
95
111
  WHERE enabled = 1
112
+ $source_filter
96
113
  AND (project = '$project' OR project = '')
97
114
  AND ('$base_cmd' = pattern OR '$full_cmd' = pattern OR '$full_cmd' LIKE pattern || ' %')
98
115
  ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END,
package/lib/hooks.sh CHANGED
@@ -100,3 +100,40 @@ eagle_patch_hook() {
100
100
  [ -n "$description" ] && eagle_ok "$description"
101
101
  return 0
102
102
  }
103
+
104
+ # Register (idempotently) the full Claude Code hook set into a settings.json.
105
+ # Single source of truth for the event→matcher→script mapping so install.sh and
106
+ # update.sh can never drift (the historical bug class). Requires $EAGLE_MEM_DIR.
107
+ # $1 = settings.json path
108
+ # $2 = "verbose" to print a "✓ <Event> hook" line per registration (installer);
109
+ # omitted/anything else = quiet (updater prints its own summary line).
110
+ eagle_register_claude_hooks() {
111
+ local settings="$1"
112
+ local V=""
113
+ [ "${2:-}" = "verbose" ] && V=1
114
+
115
+ # Clean old registrations before re-registering (handles matcher changes across versions).
116
+ eagle_clean_hook_entries "$settings" "Stop" "$EAGLE_MEM_DIR/hooks/stop.sh"
117
+ eagle_clean_hook_entries "$settings" "PostToolUse" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
118
+ eagle_clean_hook_entries "$settings" "PreToolUse" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
119
+
120
+ # ${V:+label} expands to the label only in verbose mode; otherwise to "",
121
+ # which makes eagle_patch_hook silent (it only prints when given a description).
122
+ eagle_patch_hook "$settings" "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh" "${V:+SessionStart hook}"
123
+ eagle_patch_hook "$settings" "Stop" "" "bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" "${V:+Stop hook}"
124
+ eagle_patch_hook "$settings" "PostToolUse" "Read|Write|Edit|Bash|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" "${V:+PostToolUse hook}"
125
+ eagle_patch_hook "$settings" "TaskCreated" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" "${V:+TaskCreated hook}"
126
+ eagle_patch_hook "$settings" "TaskCompleted" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" "${V:+TaskCompleted hook}"
127
+ eagle_patch_hook "$settings" "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh" "${V:+SessionEnd hook}"
128
+ eagle_patch_hook "$settings" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh" "${V:+UserPromptSubmit hook}"
129
+ eagle_patch_hook "$settings" "PreToolUse" "Bash|Read|Edit|Write" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" "${V:+PreToolUse hook}"
130
+
131
+ # Allow agent-issued session capture to run without a permission prompt.
132
+ if eagle_patch_permission_allow "$settings" "Bash(eagle-mem session save:*)"; then
133
+ [ -n "$V" ] && eagle_ok "Capture permission ${DIM}(eagle-mem session save)${RESET}"
134
+ fi
135
+ # Explicit success: the trailing conditional above evaluates false in quiet
136
+ # mode (V empty), which would otherwise make this function return 1 and abort
137
+ # a `set -e` caller (update.sh) right after a fully successful registration.
138
+ return 0
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.13.0",
3
+ "version": "4.14.0",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, and Google Antigravity",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -316,12 +316,15 @@ If no rules needed, output: NONE"
316
316
  ml_val="${max_lines:-NULL}"
317
317
  [ "$ml_val" != "NULL" ] && ml_val=$(eagle_sql_int "$ml_val")
318
318
 
319
- eagle_db "INSERT INTO command_rules (project, pattern, strategy, max_lines, reason)
320
- VALUES ('$p_esc', '$pattern_esc', '$strategy', $ml_val, '$reason_esc')
319
+ # Tag provenance: these rules are LLM-derived. They can be
320
+ # excluded from enforcement via token_guard.trust_learned_rules=false.
321
+ eagle_db "INSERT INTO command_rules (project, pattern, strategy, max_lines, reason, source)
322
+ VALUES ('$p_esc', '$pattern_esc', '$strategy', $ml_val, '$reason_esc', 'curator')
321
323
  ON CONFLICT(project, pattern) DO UPDATE SET
322
324
  strategy = excluded.strategy,
323
325
  max_lines = excluded.max_lines,
324
326
  reason = excluded.reason,
327
+ source = 'curator',
325
328
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
326
329
  eagle_log "INFO" "Curator: added command rule: $pattern → $strategy"
327
330
  fi
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Release gate (git-layer parity)
4
+ #
5
+ # The PreToolUse hook already blocks release-boundary commands for Claude and
6
+ # Codex. But a bare-shell `git push`, or a Grok session (which has no hook
7
+ # lifecycle), bypasses that gate entirely. This brings the SAME feature-
8
+ # verification gate to the git layer via an opt-in `pre-push` hook, so
9
+ # governance is not silently skippable regardless of which agent (or no agent)
10
+ # runs the push.
11
+ #
12
+ # eagle-mem gate check run the gate for the current repo (exit 1 if blocked)
13
+ # eagle-mem gate install [--repo PATH] [--force]
14
+ # install a repo-local .git/hooks/pre-push that runs `gate check`
15
+ # eagle-mem gate uninstall [--repo PATH]
16
+ # remove the Eagle Mem pre-push hook (only if it's ours)
17
+ #
18
+ # Bypass a single push: `git push --no-verify` or `EAGLE_MEM_DISABLE_HOOKS=1 git push`.
19
+ # ═══════════════════════════════════════════════════════════
20
+ set -euo pipefail
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ LIB_DIR="$SCRIPT_DIR/../lib"
24
+
25
+ . "$LIB_DIR/common.sh"
26
+ . "$LIB_DIR/db.sh"
27
+ . "$SCRIPT_DIR/style.sh"
28
+
29
+ HOOK_SENTINEL="# eagle-mem-managed-pre-push"
30
+
31
+ gate_check() {
32
+ # Fail OPEN in any situation where blocking would be wrong: explicitly
33
+ # disabled, no database yet, or not inside a recognized project. A git hook
34
+ # must never wedge pushes just because Eagle Mem isn't set up here.
35
+ [ "${EAGLE_MEM_DISABLE_HOOKS:-}" = "1" ] && exit 0
36
+ [ -f "$EAGLE_MEM_DB" ] || exit 0
37
+
38
+ local cwd project
39
+ cwd="$(pwd)"
40
+ project=$(eagle_project_from_cwd "$cwd")
41
+ [ -z "$project" ] && exit 0
42
+
43
+ local release_changed_files
44
+ release_changed_files=$(eagle_changed_files_for_release "$cwd" 2>/dev/null || true)
45
+ eagle_reconcile_current_feature_verifications "$project" "$cwd" "git-prepush" "git-push" \
46
+ "Release boundary detected for current repository diff" "$release_changed_files" >/dev/null 2>&1 || true
47
+
48
+ local pending_rows pending_count
49
+ pending_rows=$(eagle_list_current_pending_feature_verifications "$project" "$cwd" "$release_changed_files" 8 2>/dev/null || true)
50
+ pending_count=$(printf '%s\n' "$pending_rows" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
51
+ pending_count=${pending_count:-0}
52
+
53
+ if [ "$pending_count" -gt 0 ] 2>/dev/null; then
54
+ {
55
+ echo ""
56
+ echo "Eagle Mem blocked this push because ${pending_count} feature verification(s) are pending."
57
+ echo ""
58
+ echo "Resolve them, then push again:"
59
+ echo " eagle-mem feature verify <name> --notes \"what passed\""
60
+ echo " eagle-mem feature waive <id> --reason \"why this is safe\""
61
+ echo ""
62
+ echo "Pending checks:"
63
+ while IFS='|' read -r pid pname pfile preason _ptrigger _pcreated psmoke pfingerprint; do
64
+ [ -z "$pid" ] && continue
65
+ local line=" #${pid} ${pname}"
66
+ [ -n "$pfile" ] && line+=" (${pfile})"
67
+ [ -n "$preason" ] && line+=" — ${preason}"
68
+ [ -n "$psmoke" ] && line+=" | smoke: ${psmoke}"
69
+ [ -n "$pfingerprint" ] && line+=" | diff: ${pfingerprint}"
70
+ echo "$line"
71
+ done <<< "$pending_rows"
72
+ echo ""
73
+ echo "Bypass once: git push --no-verify (or EAGLE_MEM_DISABLE_HOOKS=1 git push)"
74
+ } >&2
75
+ exit 1
76
+ fi
77
+ exit 0
78
+ }
79
+
80
+ # Resolve the .git/hooks dir for a repo (handles worktrees and submodules).
81
+ gate_hooks_dir() {
82
+ local repo="$1"
83
+ ( cd "$repo" 2>/dev/null && git rev-parse --git-path hooks 2>/dev/null )
84
+ }
85
+
86
+ gate_install() {
87
+ local repo="" force=0
88
+ while [ $# -gt 0 ]; do
89
+ case "$1" in
90
+ --repo) repo="${2:-}"; shift 2 ;;
91
+ --force|-f) force=1; shift ;;
92
+ *) shift ;;
93
+ esac
94
+ done
95
+ repo="${repo:-$(pwd)}"
96
+
97
+ if ! ( cd "$repo" 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1 ); then
98
+ eagle_err "Not a git repository: $repo"
99
+ exit 1
100
+ fi
101
+
102
+ local hooks_dir hook
103
+ hooks_dir="$(gate_hooks_dir "$repo")"
104
+ # git rev-parse --git-path returns a path relative to the repo; resolve it.
105
+ case "$hooks_dir" in
106
+ /*) ;;
107
+ *) hooks_dir="$repo/$hooks_dir" ;;
108
+ esac
109
+ mkdir -p "$hooks_dir"
110
+ hook="$hooks_dir/pre-push"
111
+
112
+ if [ -f "$hook" ] && ! grep -qF "$HOOK_SENTINEL" "$hook" 2>/dev/null && [ "$force" -ne 1 ]; then
113
+ eagle_err "A non-Eagle-Mem pre-push hook already exists: $hook"
114
+ eagle_info "Re-run with --force to replace it, or add this line to your hook manually:"
115
+ eagle_dim " command -v eagle-mem >/dev/null 2>&1 && eagle-mem gate check"
116
+ exit 1
117
+ fi
118
+
119
+ cat > "$hook" <<EOF
120
+ #!/usr/bin/env bash
121
+ $HOOK_SENTINEL
122
+ # Runs the Eagle Mem feature-verification gate before every push.
123
+ # Fail-open if eagle-mem is unavailable so pushes never wedge on a missing tool.
124
+ command -v eagle-mem >/dev/null 2>&1 || exit 0
125
+ exec eagle-mem gate check
126
+ EOF
127
+ chmod +x "$hook"
128
+ eagle_ok "Installed Eagle Mem pre-push gate: $hook"
129
+ eagle_dim "Bypass once with: git push --no-verify"
130
+ }
131
+
132
+ gate_uninstall() {
133
+ local repo="" ; repo="${1:-}"
134
+ [ "$repo" = "--repo" ] && { repo="${2:-}"; }
135
+ repo="${repo:-$(pwd)}"
136
+ local hooks_dir hook
137
+ hooks_dir="$(gate_hooks_dir "$repo")"
138
+ case "$hooks_dir" in /*) ;; *) hooks_dir="$repo/$hooks_dir" ;; esac
139
+ hook="$hooks_dir/pre-push"
140
+ if [ -f "$hook" ] && grep -qF "$HOOK_SENTINEL" "$hook" 2>/dev/null; then
141
+ rm -f "$hook"
142
+ eagle_ok "Removed Eagle Mem pre-push gate: $hook"
143
+ else
144
+ eagle_info "No Eagle Mem pre-push gate found for: $repo"
145
+ fi
146
+ }
147
+
148
+ subcommand="${1:-check}"
149
+ shift 2>/dev/null || true
150
+ case "$subcommand" in
151
+ check) gate_check ;;
152
+ install) gate_install "$@" ;;
153
+ uninstall) gate_uninstall "$@" ;;
154
+ *)
155
+ eagle_err "Unknown gate command: $subcommand"
156
+ eagle_info "Usage: eagle-mem gate [check|install|uninstall]"
157
+ exit 1
158
+ ;;
159
+ esac
@@ -283,47 +283,9 @@ if [ "$claude_found" = true ]; then
283
283
  echo '{}' > "$SETTINGS"
284
284
  fi
285
285
 
286
- eagle_patch_hook "$SETTINGS" "SessionStart" "" \
287
- "$EAGLE_MEM_DIR/hooks/session-start.sh" \
288
- "SessionStart hook"
289
-
290
- # Clean old registrations before re-registering (handles matcher changes across versions)
291
- eagle_clean_hook_entries "$SETTINGS" "Stop" "$EAGLE_MEM_DIR/hooks/stop.sh"
292
- eagle_clean_hook_entries "$SETTINGS" "PostToolUse" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
293
- eagle_clean_hook_entries "$SETTINGS" "PreToolUse" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
294
-
295
- eagle_patch_hook "$SETTINGS" "Stop" "" \
296
- "bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" \
297
- "Stop hook"
298
-
299
- eagle_patch_hook "$SETTINGS" "PostToolUse" "Read|Write|Edit|Bash|TaskUpdate" \
300
- "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
301
- "PostToolUse hook"
302
-
303
- eagle_patch_hook "$SETTINGS" "TaskCreated" "" \
304
- "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
305
- "TaskCreated hook"
306
-
307
- eagle_patch_hook "$SETTINGS" "TaskCompleted" "" \
308
- "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
309
- "TaskCompleted hook"
310
-
311
- eagle_patch_hook "$SETTINGS" "SessionEnd" "" \
312
- "$EAGLE_MEM_DIR/hooks/session-end.sh" \
313
- "SessionEnd hook"
314
-
315
- eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" \
316
- "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh" \
317
- "UserPromptSubmit hook"
318
-
319
- eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" \
320
- "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
321
- "PreToolUse hook"
322
-
323
- # Allow agent-issued session capture to run without a permission prompt
324
- if eagle_patch_permission_allow "$SETTINGS" "Bash(eagle-mem session save:*)"; then
325
- eagle_ok "Capture permission ${DIM}(eagle-mem session save)${RESET}"
326
- fi
286
+ # Single source of truth for the Claude hook set (see lib/hooks.sh);
287
+ # verbose mode prints a line per registration.
288
+ eagle_register_claude_hooks "$SETTINGS" verbose
327
289
  fi
328
290
  else
329
291
  eagle_info "Claude hooks skipped ${DIM}(Claude Code not detected)${RESET}"
package/scripts/test.sh CHANGED
@@ -16,13 +16,24 @@ eagle_banner
16
16
  eagle_header "Smoke Tests"
17
17
 
18
18
  errors=0
19
+ skipped=0
19
20
 
20
21
  run_check() {
21
22
  local name="$1"
22
23
  local cmd="$2"
23
24
  echo -e " ${BOLD}→${RESET} $name"
24
- if eval "$cmd" >/dev/null 2>&1; then
25
+ local rc=0
26
+ eval "$cmd" >/dev/null 2>&1 || rc=$?
27
+ if [ "$rc" -eq 0 ]; then
25
28
  eagle_ok "$name"
29
+ elif [ "$rc" -eq 2 ]; then
30
+ # Exit code 2 = the check skipped itself because its preconditions are
31
+ # absent in this environment — e.g. a dev-only contract test running
32
+ # from a published install, where the maintainer doc it guards is not
33
+ # shipped in the npm package. A skip is neither a pass nor a failure, so
34
+ # it must not increment the error count or fail the suite.
35
+ eagle_info "skipped: $name (preconditions not present here)"
36
+ skipped=$((skipped + 1))
26
37
  else
27
38
  eagle_fail "$name"
28
39
  # Assignment form (not ((errors++))) so a failing check does not abort
@@ -80,10 +91,19 @@ run_check "Data Integrity Hardening (migrate idempotency, SQL escaping, summary
80
91
  run_check "Mod-Tracker Concurrency (lock TTL, no lost append, observation dedup race)" "bash \"$SCRIPTS_DIR/../tests/test_mod_tracker_concurrency.sh\""
81
92
  run_check "Reliability Retention (scan in-flight vs freshness, eagle_events prune)" "bash \"$SCRIPTS_DIR/../tests/test_reliability_retention.sh\""
82
93
  run_check "Test Runner No-Abort (failing check does not kill the suite under set -e)" "bash \"$SCRIPTS_DIR/../tests/test_test_runner_no_abort.sh\""
94
+ # Python lane: the native Antigravity hook (mocked). Subshell-wrapped so a
95
+ # missing python3 yields a clean skip (exit 2) instead of aborting the suite.
96
+ run_check "Antigravity Hook (native Python SDK lifecycle, mocked)" "( command -v python3 >/dev/null 2>&1 || exit 2; python3 \"$SCRIPTS_DIR/../tests/test_antigravity_hook.py\" )"
97
+ run_check "Release Gate Parity (eagle-mem gate: blocks pending, fails open, pre-push install)" "bash \"$SCRIPTS_DIR/../tests/test_release_gate_prepush.sh\""
98
+ run_check "Command Rule Provenance (curator rules tagged + trust_learned_rules gate)" "bash \"$SCRIPTS_DIR/../tests/test_command_rule_provenance.sh\""
83
99
 
84
100
  echo ""
85
101
  if [ "$errors" -eq 0 ]; then
86
- eagle_ok "All smoke tests passed"
102
+ if [ "$skipped" -gt 0 ]; then
103
+ eagle_ok "All smoke tests passed ($skipped skipped — preconditions not present here)"
104
+ else
105
+ eagle_ok "All smoke tests passed"
106
+ fi
87
107
 
88
108
  # Auto-verify the 7 core features in the database
89
109
  for feat in "compaction-survival" "feature-verification" "grok-cli-integration" "agent-orchestration" "Cross Agent Memory" "Installer And Updater" "Code Scan And Index"; do
package/scripts/update.sh CHANGED
@@ -86,23 +86,9 @@ fi
86
86
  # ─── Re-register hooks (idempotent) ───────────────────────
87
87
 
88
88
  if [ "$claude_found" = true ] && [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
89
- # Clean old registrations before re-registering (handles matcher changes across versions)
90
- eagle_clean_hook_entries "$SETTINGS" "Stop" "$EAGLE_MEM_DIR/hooks/stop.sh"
91
- eagle_clean_hook_entries "$SETTINGS" "PostToolUse" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
92
- eagle_clean_hook_entries "$SETTINGS" "PreToolUse" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
93
-
94
- eagle_patch_hook "$SETTINGS" "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh"
95
- eagle_patch_hook "$SETTINGS" "Stop" "" "bash \"$EAGLE_MEM_DIR/hooks/stop.sh\""
96
- eagle_patch_hook "$SETTINGS" "PostToolUse" "Read|Write|Edit|Bash|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
97
- eagle_patch_hook "$SETTINGS" "TaskCreated" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
98
- eagle_patch_hook "$SETTINGS" "TaskCompleted" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
99
- eagle_patch_hook "$SETTINGS" "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
100
- eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
101
- eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
102
-
103
- # Allow agent-issued session capture to run without a permission prompt
104
- eagle_patch_permission_allow "$SETTINGS" "Bash(eagle-mem session save:*)"
105
-
89
+ # Single source of truth for the Claude hook set (see lib/hooks.sh); quiet
90
+ # mode the updater prints one summary line instead of per-hook lines.
91
+ eagle_register_claude_hooks "$SETTINGS"
106
92
  eagle_ok "Hooks registered"
107
93
  fi
108
94
 
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # Curator command-rule provenance — closes the curator -> guardrail -> PreToolUse
3
+ # loop where LLM output became enforcement input with no provenance.
4
+ # command_rules steer PreToolUse output handling (truncate/summary). Curator-
5
+ # generated rules are now tagged source='curator'; setting
6
+ # token_guard.trust_learned_rules=false excludes them so model output cannot
7
+ # silently shape command handling. Human ('manual') rules are unaffected.
8
+ set -euo pipefail
9
+
10
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
11
+ tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/eagle-rule-prov.XXXXXX")
12
+ trap 'rm -rf "$tmp_dir"' EXIT
13
+
14
+ export HOME="$tmp_dir/home"
15
+ export EAGLE_MEM_DIR="$tmp_dir/eagle-mem"
16
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR"
17
+
18
+ . "$ROOT_DIR/lib/common.sh"
19
+ "$ROOT_DIR/db/migrate.sh" >/dev/null
20
+ . "$ROOT_DIR/lib/db.sh"
21
+
22
+ pass=0; fail=0
23
+ ok() { echo " ok: $1"; pass=$((pass+1)); }
24
+ bad() { echo " FAIL: $1"; fail=$((fail+1)); }
25
+
26
+ project="rule-prov"
27
+ p=$(eagle_sql_escape "$project")
28
+ eagle_db "INSERT INTO command_rules (project, pattern, strategy, max_lines, reason, source)
29
+ VALUES ('$p','curatorcmd','truncate',50,'noisy','curator');" >/dev/null
30
+ eagle_db "INSERT INTO command_rules (project, pattern, strategy, max_lines, reason, source)
31
+ VALUES ('$p','manualcmd','truncate',50,'noisy','manual');" >/dev/null
32
+
33
+ # 1. provenance is persisted
34
+ [ "$(eagle_db "SELECT source FROM command_rules WHERE pattern='curatorcmd';")" = "curator" ] \
35
+ && ok "curator rule stored with source=curator" || bad "curator source not persisted"
36
+ [ "$(eagle_db "SELECT source FROM command_rules WHERE pattern='manualcmd';")" = "manual" ] \
37
+ && ok "manual rule stored with source=manual" || bad "manual source not persisted"
38
+
39
+ # 2. default (no config) trusts learned rules — curator rule applies
40
+ [ -n "$(eagle_get_command_rule "$project" "curatorcmd" "curatorcmd")" ] \
41
+ && ok "default: curator rule applies (auto-learning preserved)" \
42
+ || bad "default: curator rule should apply"
43
+
44
+ # 3. trust_learned_rules=false excludes curator rules, keeps manual rules
45
+ printf '[token_guard]\ntrust_learned_rules = false\n' > "$EAGLE_MEM_DIR/config.toml"
46
+ [ -z "$(eagle_get_command_rule "$project" "curatorcmd" "curatorcmd")" ] \
47
+ && ok "trust=false: curator rule excluded from enforcement" \
48
+ || bad "trust=false: curator rule should be excluded"
49
+ [ -n "$(eagle_get_command_rule "$project" "manualcmd" "manualcmd")" ] \
50
+ && ok "trust=false: manual rule still applies" \
51
+ || bad "trust=false: manual rule should still apply"
52
+
53
+ # 4. the curator code path tags new rules as 'curator'
54
+ grep -q "VALUES ('\$p_esc', '\$pattern_esc', '\$strategy', \$ml_val, '\$reason_esc', 'curator')" "$ROOT_DIR/scripts/curate.sh" \
55
+ && ok "curate.sh writes command rules with source='curator'" \
56
+ || bad "curate.sh does not tag command rules as curator"
57
+
58
+ echo ""
59
+ echo "test_command_rule_provenance: $pass passed, $fail failed"
60
+ [ "$fail" -eq 0 ] || exit 1
@@ -7,7 +7,12 @@ set -euo pipefail
7
7
  ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
8
8
  EAGLE_BIN="$ROOT_DIR/bin/eagle-mem"
9
9
 
10
- tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-compaction-survival.XXXXXX")
10
+ # Use a neutral system temp dir, NOT one inside $ROOT_DIR. From a published
11
+ # install $ROOT_DIR lives under node_modules/, and the code scanner excludes
12
+ # node_modules — so a fixture repo created here would index 0 files and the
13
+ # post-compact "Relevant Code" recall would never appear (the suite is run from
14
+ # the installed package via `eagle-mem test`).
15
+ tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/eagle-compaction-survival.XXXXXX")
11
16
  trap 'rm -rf "$tmp_dir"' EXIT
12
17
 
13
18
  export HOME="$tmp_dir/home"
@@ -190,6 +195,13 @@ assert_json "$compaction_json" '
190
195
  and .metrics.semantic_graph_nodes >= 5
191
196
  ' "compaction --json did not report durable survival metrics"
192
197
 
198
+ # Index the fixture code deterministically so the post-compact recall can
199
+ # surface "Relevant Code". This previously relied on a background auto-index
200
+ # that only won the race from a warm source checkout, so the assertion failed
201
+ # when the suite ran from a published install via `eagle-mem test`. The
202
+ # `index` command is synchronous, so chunks exist by the time the hook runs.
203
+ ( cd "$repo" && "$EAGLE_BIN" index >/dev/null 2>&1 )
204
+
193
205
  eagle_upsert_session "$post_session" "$project" "$repo" "test-model" "test" "codex" >/dev/null
194
206
  hook_input=$(jq -nc \
195
207
  --arg sid "$post_session" \
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ # Release-gate parity at the git layer (`eagle-mem gate`). The PreToolUse hook
3
+ # blocks release-boundary commands for Claude/Codex; this proves the SAME
4
+ # feature-verification gate is enforceable for bare-shell / Grok pushes via an
5
+ # opt-in pre-push hook, blocks when verifications are pending, fails OPEN where
6
+ # blocking would be wrong, and never clobbers a foreign pre-push hook.
7
+ set -euo pipefail
8
+
9
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+ BIN="$ROOT_DIR/bin/eagle-mem"
11
+ tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/eagle-gate-prepush.XXXXXX")
12
+ trap 'rm -rf "$tmp_dir"' EXIT
13
+
14
+ export HOME="$tmp_dir/home"
15
+ export EAGLE_MEM_DIR="$tmp_dir/eagle-mem"
16
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR"
17
+
18
+ . "$ROOT_DIR/lib/common.sh"
19
+ "$ROOT_DIR/db/migrate.sh" >/dev/null
20
+ . "$ROOT_DIR/lib/db.sh"
21
+
22
+ pass=0; fail=0
23
+ ok() { echo " ok: $1"; pass=$((pass+1)); }
24
+ bad() { echo " FAIL: $1"; fail=$((fail+1)); }
25
+
26
+ # ── git repo fixture with a committed, feature-mapped file ──────────────────
27
+ project="gate-prepush-test"
28
+ export EAGLE_MEM_PROJECT="$project"
29
+ repo="$HOME/proj"; mkdir -p "$repo/src"
30
+ (
31
+ cd "$repo"
32
+ git init -q
33
+ git config user.email t@example.com
34
+ git config user.name tester
35
+ printf 'console.log("v1");\n' > src/app.js
36
+ git add -A && git commit -qm init
37
+ )
38
+
39
+ eagle_upsert_feature "$project" "app-entry" "app entrypoint"
40
+ fid=$(eagle_get_feature_id "$project" "app-entry")
41
+ eagle_add_feature_file "$fid" "src/app.js" "entrypoint" >/dev/null
42
+
43
+ # ── 1. clean working tree → gate passes ────────────────────────────────────
44
+ if ( cd "$repo" && "$BIN" gate check ) >/dev/null 2>&1; then
45
+ ok "clean working tree passes the gate (exit 0)"
46
+ else
47
+ bad "clean working tree should pass the gate"
48
+ fi
49
+
50
+ # ── 2. uncommitted change to a feature file → gate blocks ──────────────────
51
+ printf 'console.log("v2");\n' > "$repo/src/app.js"
52
+ out=$( cd "$repo" && "$BIN" gate check 2>&1 ) && rc=0 || rc=$?
53
+ [ "$rc" -eq 1 ] && ok "pending verification blocks the push (exit 1)" || bad "expected block exit 1, got $rc"
54
+ case "$out" in *"blocked this push"*) ok "block message is shown" ;; *) bad "block message missing: $out" ;; esac
55
+
56
+ # ── 3. EAGLE_MEM_DISABLE_HOOKS bypasses even with a pending verification ────
57
+ if ( cd "$repo" && EAGLE_MEM_DISABLE_HOOKS=1 "$BIN" gate check ) >/dev/null 2>&1; then
58
+ ok "EAGLE_MEM_DISABLE_HOOKS=1 bypasses the gate"
59
+ else
60
+ bad "EAGLE_MEM_DISABLE_HOOKS=1 should bypass the gate"
61
+ fi
62
+
63
+ # ── 4. verifying the feature unblocks the gate ─────────────────────────────
64
+ ( cd "$repo" && "$BIN" feature verify "app-entry" --notes "tested" ) >/dev/null 2>&1 || true
65
+ if ( cd "$repo" && "$BIN" gate check ) >/dev/null 2>&1; then
66
+ ok "verified feature unblocks the gate (exit 0)"
67
+ else
68
+ bad "verified feature should unblock the gate"
69
+ fi
70
+
71
+ # ── 5. fail-open outside a recognized project ──────────────────────────────
72
+ other=$(mktemp -d "$tmp_dir/other.XXXXXX")
73
+ if ( cd "$other" && env -u EAGLE_MEM_PROJECT "$BIN" gate check ) >/dev/null 2>&1; then
74
+ ok "fails open outside a recognized project (exit 0)"
75
+ else
76
+ bad "should fail open outside a recognized project"
77
+ fi
78
+
79
+ # ── 6. install writes a managed, executable pre-push hook ──────────────────
80
+ rm -f "$repo/.git/hooks/pre-push"
81
+ ( cd "$repo" && "$BIN" gate install ) >/dev/null 2>&1 || true
82
+ if [ -x "$repo/.git/hooks/pre-push" ] && grep -q "eagle-mem-managed-pre-push" "$repo/.git/hooks/pre-push"; then
83
+ ok "gate install writes a managed, executable pre-push hook"
84
+ else
85
+ bad "gate install should write a managed pre-push hook"
86
+ fi
87
+
88
+ # ── 7. install refuses to clobber a FOREIGN pre-push hook ──────────────────
89
+ printf '#!/bin/sh\necho keepme\n' > "$repo/.git/hooks/pre-push"
90
+ if ( cd "$repo" && "$BIN" gate install ) >/dev/null 2>&1; then
91
+ bad "gate install should refuse a foreign pre-push hook"
92
+ else
93
+ ok "gate install refuses to clobber a foreign pre-push hook"
94
+ fi
95
+ grep -q "echo keepme" "$repo/.git/hooks/pre-push" && ok "foreign pre-push hook preserved" || bad "foreign pre-push hook was clobbered"
96
+
97
+ # ── 8. uninstall leaves a foreign hook alone ───────────────────────────────
98
+ ( cd "$repo" && "$BIN" gate uninstall ) >/dev/null 2>&1 || true
99
+ grep -q "echo keepme" "$repo/.git/hooks/pre-push" && ok "uninstall leaves a foreign hook untouched" || bad "uninstall removed a foreign hook"
100
+
101
+ echo ""
102
+ echo "test_release_gate_prepush: $pass passed, $fail failed"
103
+ [ "$fail" -eq 0 ] || exit 1
@@ -18,7 +18,14 @@ require_contains() {
18
18
  fi
19
19
  }
20
20
 
21
- [ -f "$PLAN" ] || fail "missing MIGRATION.md"
21
+ # MIGRATION.md is a maintainer-only roadmap and is intentionally NOT shipped in
22
+ # the npm package (`files` allowlist), so this contract test has nothing to
23
+ # guard when the suite runs from a published install via `eagle-mem test`.
24
+ # Skip cleanly (exit 2) in that case; run strictly from a source checkout.
25
+ if [ ! -f "$PLAN" ]; then
26
+ echo "skip: MIGRATION.md not present (dev-only contract test)" >&2
27
+ exit 2
28
+ fi
22
29
 
23
30
  require_contains "~/.eagle-mem/memory\\.db" "the existing user database path"
24
31
  require_contains "compatibility wrapper" "the Bash compatibility wrapper"