eagle-mem 4.13.1 → 4.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/bin/eagle-mem +1 -0
- package/db/045_command_rules_source.sql +14 -0
- package/docs/agent-compatibility/README.md +15 -0
- package/docs/agent-compatibility/claude-code.md +9 -0
- package/lib/common.sh +12 -6
- package/lib/db-observations.sh +17 -0
- package/package.json +1 -1
- package/scripts/curate.sh +5 -2
- package/scripts/gate.sh +159 -0
- package/scripts/statusline-em.sh +7 -5
- package/scripts/test.sh +3 -0
- package/tests/test_busy_timeout_echo.sh +90 -0
- package/tests/test_command_rule_provenance.sh +60 -0
- package/tests/test_release_gate_prepush.sh +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ All notable changes to the **Eagle Mem** project are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v4.14.1 busy_timeout Echo Fix
|
|
8
|
+
|
|
9
|
+
A latent data-corruption and statusline-display bug, introduced when `busy_timeout` protection was added to three hot SQLite paths (the 2026-06-10 data-integrity hardening). No new feature; pure correctness.
|
|
10
|
+
|
|
11
|
+
- **Root cause.** Setting the busy timeout with an inline value-form `PRAGMA busy_timeout=10000;` run in the *same* `sqlite3` invocation as a `SELECT` makes SQLite echo the timeout value (`10000`) as the **first output row**. Any caller that reads the first line then mis-reads `10000` as query data.
|
|
12
|
+
- **Impact (all three fixed).**
|
|
13
|
+
- `scripts/statusline-em.sh`: the HUD parsed the echoed `10000` as the session count, rendering `Sessions: 10000`, `Memories: 0`, `Last: never`.
|
|
14
|
+
- `lib/common.sh` `eagle_get_session_project_light`: returned `10000` as the project, **mis-filing sessions under a phantom `10000` project key** (the data-corruption vector).
|
|
15
|
+
- `lib/common.sh` `eagle_project_has_table_row`: the echoed value failed the `= "1"` test, so it **always reported "no row"**, breaking ancestor-project repair.
|
|
16
|
+
- **Fix.** All three set the timeout via the silent `-cmd ".timeout 10000"` dot-command, which emits no row. No SQL behavior, hook contract, statusline schema, or output format changes — only the timeout-set mechanism. `db/migrate.sh` (uses `tail -n1`) and the `.output`-bracketed setups in `lib/db-core.sh` were already echo-safe and are unchanged.
|
|
17
|
+
- **No data migration needed for most installs.** The corruption only accrues while running unpatched code, and only affects session→project tags. The release ships the durable source fix so `eagle-mem update` can no longer reintroduce the bug.
|
|
18
|
+
|
|
19
|
+
New coverage: `tests/test_busy_timeout_echo.sh` (reproduces the failure — buggy form returns `10000`/always-false — plus a structural guard against re-introducing the inline value-PRAGMA-before-SELECT shape). Compatibility evidence: `docs/agent-compatibility/claude-code.md`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## v4.14.0 Governance Parity & Curator Provenance
|
|
24
|
+
|
|
25
|
+
Two trust-surface features that close the remaining governance gaps from the v4.13.0 review.
|
|
26
|
+
|
|
27
|
+
- **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`:
|
|
28
|
+
- `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.
|
|
29
|
+
- `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.
|
|
30
|
+
- Bypass a single push with `git push --no-verify` or `EAGLE_MEM_DISABLE_HOOKS=1 git push`.
|
|
31
|
+
- 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`.
|
|
32
|
+
- **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.)
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
New coverage: `tests/test_release_gate_prepush.sh`, `tests/test_command_rule_provenance.sh`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
7
39
|
## v4.13.1 Test-Suite & Packaging Hygiene
|
|
8
40
|
|
|
9
41
|
Follow-up cleanup that clears the bounded items left after the v4.13.0 review. No runtime behavior change for normal sessions.
|
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).
|
|
@@ -84,3 +84,12 @@ Phase 4 token-economy hardening added a generous global size ceiling on the reca
|
|
|
84
84
|
|
|
85
85
|
Covered by `tests/test_context_budget.sh`.
|
|
86
86
|
|
|
87
|
+
### Evidence: busy_timeout echo fix (2026-06-13)
|
|
88
|
+
|
|
89
|
+
Corrects a regression introduced by the 2026-06-10 data-integrity note above. Setting the SQLite busy timeout with an inline value-form `PRAGMA busy_timeout=10000;` run in the *same* `sqlite3` invocation as a `SELECT` echoes the timeout value (`10000`) as the first output row. Any caller that reads the first row then misreads `10000` as data. The earlier note's claim that "statusline output rendering [is] unchanged" was therefore wrong — the statusline showed `Sessions: 10000`, `Memories: 0`. The fix sets the timeout via the silent `-cmd ".timeout 10000"` dot-command (no echoed row); no Claude Code contract changes (hook events, statusline stdin schema, and stdout/exit semantics are all unchanged). Specifically:
|
|
90
|
+
|
|
91
|
+
- `scripts/statusline-em.sh`: the stats query drops the inline `PRAGMA busy_timeout=10000;` prefix and passes `-cmd ".timeout 10000"` to `sqlite3` instead, so `IFS='|' read` parses the real `sessions|memories|last` row rather than the echoed `10000`.
|
|
92
|
+
- `lib/common.sh`: `eagle_get_session_project_light` (was returning `10000` as the project, mis-filing sessions under a phantom `10000` project key) and `eagle_project_has_table_row` (whose `= "1"` test always failed on the echoed value, so ancestor-project repair always saw "no row") use the same `-cmd ".timeout"` form.
|
|
93
|
+
|
|
94
|
+
Covered by `tests/test_busy_timeout_echo.sh` (reproduces the failure: the buggy form returns `10000`/always-false; also a structural guard against re-introducing the inline value-PRAGMA-before-SELECT shape).
|
|
95
|
+
|
package/lib/common.sh
CHANGED
|
@@ -456,9 +456,12 @@ eagle_get_session_project_light() {
|
|
|
456
456
|
|
|
457
457
|
local sid_sql project
|
|
458
458
|
sid_sql=$(eagle_sql_escape "$session_id")
|
|
459
|
-
# busy_timeout
|
|
460
|
-
#
|
|
461
|
-
|
|
459
|
+
# busy_timeout (via the silent `-cmd ".timeout"` dot-command, NOT an inline
|
|
460
|
+
# `PRAGMA busy_timeout=N;`) so a momentary SQLITE_BUSY waits for the lock
|
|
461
|
+
# instead of exiting non-zero and being misread as "session has no project"
|
|
462
|
+
# (fail-open). An inline value-setting PRAGMA echoes its value ("10000") as
|
|
463
|
+
# the first output row, which `awk 'NF{...}'` would then return as the project.
|
|
464
|
+
project=$("$sqlite_bin" -cmd ".timeout 10000" "$EAGLE_MEM_DB" "SELECT project FROM sessions WHERE id = '$sid_sql' AND project != '' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
|
|
462
465
|
[ -n "$project" ] || return 1
|
|
463
466
|
printf '%s\n' "$project"
|
|
464
467
|
}
|
|
@@ -481,9 +484,12 @@ eagle_project_has_table_row() {
|
|
|
481
484
|
|
|
482
485
|
local project_sql found
|
|
483
486
|
project_sql=$(eagle_sql_escape "$project")
|
|
484
|
-
# busy_timeout
|
|
485
|
-
#
|
|
486
|
-
|
|
487
|
+
# busy_timeout (via the silent `-cmd ".timeout"` dot-command, NOT an inline
|
|
488
|
+
# `PRAGMA busy_timeout=N;`) so a momentary SQLITE_BUSY waits for the lock
|
|
489
|
+
# instead of exiting non-zero and being misread as "row doesn't exist"
|
|
490
|
+
# (fail-open). An inline value-setting PRAGMA echoes its value ("10000") as
|
|
491
|
+
# the first output row, which would then fail the `= "1"` test below.
|
|
492
|
+
found=$("$sqlite_bin" -cmd ".timeout 10000" "$EAGLE_MEM_DB" "SELECT 1 FROM $table WHERE project = '$project_sql' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
|
|
487
493
|
[ "$found" = "1" ]
|
|
488
494
|
}
|
|
489
495
|
|
package/lib/db-observations.sh
CHANGED
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eagle-mem",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.1",
|
|
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
|
-
|
|
320
|
-
|
|
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
|
package/scripts/gate.sh
ADDED
|
@@ -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
|
package/scripts/statusline-em.sh
CHANGED
|
@@ -65,11 +65,13 @@ eagle_mem_statusline_stats() {
|
|
|
65
65
|
project_scope=$(eagle_recall_project_scope_from_cwd "${current_dir:-$project_dir}" "$project_key")
|
|
66
66
|
project_condition=$(eagle_sql_project_scope_condition "project" "$project_scope")
|
|
67
67
|
|
|
68
|
-
# busy_timeout
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
# busy_timeout (via the silent `-cmd ".timeout"` dot-command, NOT an inline
|
|
69
|
+
# `PRAGMA busy_timeout=N;`) so a momentary SQLITE_BUSY (this is the hottest
|
|
70
|
+
# standalone query during live sessions) waits for the lock instead of exiting
|
|
71
|
+
# non-zero, which would otherwise escalate to an integrity-status mislabel below.
|
|
72
|
+
# An inline value-setting PRAGMA echoes "10000" as the first output row, which
|
|
73
|
+
# the `IFS='|' read` below would parse as sessions=10000, memories=0, last=never.
|
|
74
|
+
stats=$("$sqlite_bin" -cmd ".timeout 10000" "$em_db" "SELECT
|
|
73
75
|
COUNT(*) || '|' ||
|
|
74
76
|
(SELECT COUNT(*) FROM agent_memories WHERE $project_condition) || '|' ||
|
|
75
77
|
COALESCE(MAX(COALESCE(last_activity_at, started_at)), 'never')
|
package/scripts/test.sh
CHANGED
|
@@ -94,6 +94,9 @@ run_check "Test Runner No-Abort (failing check does not kill the suite under set
|
|
|
94
94
|
# Python lane: the native Antigravity hook (mocked). Subshell-wrapped so a
|
|
95
95
|
# missing python3 yields a clean skip (exit 2) instead of aborting the suite.
|
|
96
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\""
|
|
99
|
+
run_check "busy_timeout Echo (no PRAGMA value leaks into project/row/statusline reads)" "bash \"$SCRIPTS_DIR/../tests/test_busy_timeout_echo.sh\""
|
|
97
100
|
|
|
98
101
|
echo ""
|
|
99
102
|
if [ "$errors" -eq 0 ]; then
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — busy_timeout echo regression test
|
|
4
|
+
#
|
|
5
|
+
# Root cause guarded here: an inline value-setting `PRAGMA busy_timeout=N;`
|
|
6
|
+
# run in the SAME sqlite3 invocation as a SELECT echoes its value ("10000")
|
|
7
|
+
# as the FIRST output row. Any caller that reads the first line then mis-reads
|
|
8
|
+
# the timeout value as data:
|
|
9
|
+
# - eagle_get_session_project_light -> returns "10000" as the project,
|
|
10
|
+
# mis-filing every session under a phantom "10000" project.
|
|
11
|
+
# - eagle_project_has_table_row -> `[ "10000" = "1" ]` is false, so it
|
|
12
|
+
# ALWAYS reports "no row", breaking ancestor-project repair.
|
|
13
|
+
# - statusline stats -> `IFS='|' read` parses "10000" as sessions, 0 memories.
|
|
14
|
+
# The fix sets the timeout via the silent `-cmd ".timeout"` dot-command, which
|
|
15
|
+
# emits no output. This test reproduces the failure (buggy code returns 10000
|
|
16
|
+
# / always-false) and structurally guards against re-introducing the shape.
|
|
17
|
+
# ═══════════════════════════════════════════════════════════
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
21
|
+
tmp_dir=$(mktemp -d)
|
|
22
|
+
trap 'rm -rf "$tmp_dir"' EXIT
|
|
23
|
+
|
|
24
|
+
export EAGLE_MEM_DIR="$tmp_dir/em"
|
|
25
|
+
export EAGLE_AGENT_SOURCE="claude-code"
|
|
26
|
+
export EAGLE_MEM_DISABLE_HOOKS=1
|
|
27
|
+
mkdir -p "$EAGLE_MEM_DIR"
|
|
28
|
+
|
|
29
|
+
pass=0; fail=0
|
|
30
|
+
ok() { echo " ok: $1"; pass=$((pass+1)); }
|
|
31
|
+
bad() { echo " FAIL: $1" >&2; fail=$((fail+1)); }
|
|
32
|
+
|
|
33
|
+
bash "$ROOT_DIR/db/migrate.sh" >/dev/null 2>&1
|
|
34
|
+
|
|
35
|
+
. "$ROOT_DIR/lib/common.sh"
|
|
36
|
+
. "$ROOT_DIR/lib/db.sh"
|
|
37
|
+
|
|
38
|
+
# ── eagle_get_session_project_light returns the real project ────────────────
|
|
39
|
+
SID="sess-busy-timeout-001"
|
|
40
|
+
PROJ="personal_projects/eagle-mem"
|
|
41
|
+
eagle_upsert_session "$SID" "$PROJ" "$tmp_dir" "" "test" "claude-code"
|
|
42
|
+
|
|
43
|
+
got=$(eagle_get_session_project_light "$SID")
|
|
44
|
+
if [ "$got" = "10000" ]; then
|
|
45
|
+
bad "eagle_get_session_project_light returned the busy_timeout echo ('10000') as the project"
|
|
46
|
+
elif [ "$got" = "$PROJ" ]; then
|
|
47
|
+
ok "eagle_get_session_project_light returns the real project ('$got')"
|
|
48
|
+
else
|
|
49
|
+
bad "eagle_get_session_project_light returned unexpected value: '$got' (expected '$PROJ')"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Unknown session must resolve to nothing (rc=1), never the echo value.
|
|
53
|
+
if unknown=$(eagle_get_session_project_light "sess-does-not-exist-999"); then
|
|
54
|
+
bad "eagle_get_session_project_light succeeded for unknown session, returned '$unknown'"
|
|
55
|
+
else
|
|
56
|
+
ok "eagle_get_session_project_light fails closed for unknown session"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ── eagle_project_has_table_row: true for present, false for absent ──────────
|
|
60
|
+
if eagle_project_has_table_row "sessions" "$PROJ"; then
|
|
61
|
+
ok "eagle_project_has_table_row finds the existing project row"
|
|
62
|
+
else
|
|
63
|
+
bad "eagle_project_has_table_row reports 'no row' for a project that HAS a row (busy_timeout echo broke the '= 1' test)"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if eagle_project_has_table_row "sessions" "no/such/project-xyz"; then
|
|
67
|
+
bad "eagle_project_has_table_row reports a row for a project with none"
|
|
68
|
+
else
|
|
69
|
+
ok "eagle_project_has_table_row correctly reports no row for an absent project"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ── Structural guard: no inline value-setting PRAGMA before a parsed SELECT ──
|
|
73
|
+
# (comment lines are stripped so the explanatory comments above don't match)
|
|
74
|
+
common_hit=$(grep -vE '^[[:space:]]*#' "$ROOT_DIR/lib/common.sh" | grep -nE 'busy_timeout=[0-9]+;[[:space:]]*SELECT' || true)
|
|
75
|
+
if [ -n "$common_hit" ]; then
|
|
76
|
+
bad "lib/common.sh re-introduced an inline 'PRAGMA busy_timeout=N; SELECT' (echoes the value): $common_hit"
|
|
77
|
+
else
|
|
78
|
+
ok "lib/common.sh has no inline value-setting PRAGMA before a SELECT"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
statusline_hit=$(grep -vE '^[[:space:]]*#' "$ROOT_DIR/scripts/statusline-em.sh" | grep -nE 'busy_timeout' || true)
|
|
82
|
+
if [ -n "$statusline_hit" ]; then
|
|
83
|
+
bad "scripts/statusline-em.sh re-introduced an inline PRAGMA busy_timeout (use -cmd \".timeout\"): $statusline_hit"
|
|
84
|
+
else
|
|
85
|
+
ok "scripts/statusline-em.sh sets the timeout via the silent -cmd \".timeout\" dot-command"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo
|
|
89
|
+
echo "busy_timeout echo regression: $pass passed, $fail failed"
|
|
90
|
+
[ "$fail" -eq 0 ]
|
|
@@ -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
|
|
@@ -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
|