eagle-mem 4.11.0 → 4.12.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,18 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.12.0 Clean, Branded Session Capture
8
+
9
+ Session endings are now clean across every agent — no more raw `<eagle-summary>` XML blocks in the visible reply.
10
+
11
+ - **CLI-first capture**: Agents persist structured summaries by running `eagle-mem session save --session-id <id> ...` (a pre-approved, quiet Bash call), then end with a short human recap plus one branded line: `Eagle Mem | Session captured — N decisions, M gotchas`. The installer adds a `permissions.allow` entry so the capture runs without a prompt.
12
+ - **Extended `session save`**: New flags `--session-id`, `--completed`, `--investigated`, `--files-read`, `--files-modified`, `--affected-features`, `--verified-features`, `--regression-risks`. With `--session-id` the capture merges into the live session row instead of creating a standalone `manual-*` row, and no longer marks the session completed.
13
+ - **Capture-integrity fixes**: New `capture_source` column (`agent`/`hook`/`enrich`) with an atomic fill-only upsert. Agent-authored captures are authoritative — Stop-hook heuristics and background LLM enrichment now only fill empty gaps and can never clobber richer data (previously the winning-COALESCE upsert could overwrite it). Enrichment queueing is skipped once a session is agent-authored.
14
+ - **Cross-agent**: Claude Code, Codex, OpenCode, Grok, and Antigravity all capture through the same CLI. Instruction text updated in SessionStart/UserPromptSubmit (Claude + Codex), the installed CLAUDE.md/AGENTS.md sections, and `compaction.sh`.
15
+ - **Backward compatible**: The Stop hook still parses any `<eagle-summary>` block it finds; agents are simply no longer instructed to emit one.
16
+
17
+ ---
18
+
7
19
  ## v4.11.0 Agent Compatibility and Governance Surfaces
8
20
 
9
21
  This feature release expands Eagle Mem from Claude/Codex memory hooks into a broader multi-agent governance substrate:
package/README.md CHANGED
@@ -88,7 +88,7 @@ Hooks fire automatically at different points in the agent lifecycle:
88
88
  | **PreToolUse** | before Bash/shell, Read, Edit, Write, apply_patch | Surfaces guardrails and decisions before edits. Blocks release-boundary commands while feature verification is pending. Rewrites noisy commands through RTK when available. Detects redundant reads, nudges co-edit partners, detects stuck loops. |
89
89
  | **UserPromptSubmit** | user sends a message | FTS5 search across past sessions and indexed code for relevant context |
90
90
  | **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes, surfaces decision history and feature impacts on reads, stale memory warnings on edits |
91
- | **Stop** | agent turn ends | Saves fast heuristic summaries and extracts `<eagle-summary>` blocks when present. LLM enrichment runs later in the background so the agent lifecycle is not blocked. |
91
+ | **Stop** | agent turn ends | Saves the session summary. Agent-authored captures from `eagle-mem session save` are authoritative; heuristic extraction only fills gaps and LLM enrichment runs later in the background so the agent lifecycle is not blocked. |
92
92
  | **SessionEnd** | session closes | Re-syncs tasks, marks session completed |
93
93
 
94
94
  Codex shell hooks are registered for `Bash`, `exec_command`, `shell_command`, and `unified_exec` tool names so release-boundary protection works across current Codex shell paths. OpenCode uses a global local plugin to normalize `chat.message`, `tool.execute.before`, `tool.execute.after`, `todo.updated`, `session.idle`, and compaction events into the same Eagle Mem hook payloads.
@@ -128,7 +128,7 @@ Run `eagle-mem compaction` anytime to check readiness.
128
128
 
129
129
  Eagle Mem prevents Claude from repeating past mistakes:
130
130
 
131
- - **Decision surfacing** — when you edit a file that has past decisions recorded (from `<eagle-summary>` blocks), PreToolUse reminds Claude not to revert without asking
131
+ - **Decision surfacing** — when you edit a file that has past decisions recorded (from captured session summaries), PreToolUse reminds Claude not to revert without asking
132
132
  - **Guardrails** — file-level rules (manual or curator-discovered) that fire before every Edit/Write
133
133
  - **Feature verification** — tracks features with smoke tests and dependencies; current git diffs create fingerprinted pending verification records, and release-boundary commands such as `git push`, `gh pr create`, and package publish are blocked until the current fingerprint is verified or waived
134
134
  - **Gotcha surfacing** — past surprises and gotchas are surfaced when editing related files
@@ -0,0 +1,12 @@
1
+ -- Migration 044: Summary capture provenance
2
+ -- Tracks how each summary row was captured so later writers do not clobber
3
+ -- richer data. Values:
4
+ -- 'agent' — agent-authored via `eagle-mem session save` or a parsed
5
+ -- <eagle-summary> block (authoritative)
6
+ -- 'hook' — Stop-hook heuristic extraction (gap-fill only once 'agent')
7
+ -- 'enrich' — background LLM enrichment (gap-fill only once 'agent')
8
+ -- Empty default preserves existing rows. Not indexed by FTS.
9
+
10
+ ALTER TABLE summaries ADD COLUMN capture_source TEXT NOT NULL DEFAULT '';
11
+
12
+ CREATE INDEX IF NOT EXISTS idx_summaries_capture_source ON summaries(capture_source);
@@ -1,6 +1,6 @@
1
1
  # Claude Code Compatibility
2
2
 
3
- Last verified: 2026-06-02
3
+ Last verified: 2026-06-10
4
4
 
5
5
  ## Official Sources
6
6
 
@@ -21,6 +21,14 @@ Last verified: 2026-06-02
21
21
  - `SessionEnd` runs when a session terminates.
22
22
  - The custom statusline is configured with `statusLine` in Claude settings; its command receives JSON on stdin and the first stdout line becomes the visible statusline text.
23
23
 
24
+ ## Session Capture Behavior
25
+
26
+ - Eagle Mem no longer instructs Claude to print a raw `<eagle-summary>` block. `SessionStart` injects guidance to capture the session by running `eagle-mem session save --session-id <session_id> ...` (a quiet shell call), then to end the turn with human prose plus one branded line: `Eagle Mem | Session captured — N decisions, M gotchas`.
27
+ - The `session_id` injected into the instruction is the same id Claude passes to every hook, so the CLI capture merges into the live session row rather than creating a standalone `manual-*` row.
28
+ - The installer adds `permissions.allow: ["Bash(eagle-mem session save:*)"]` to Claude settings so the capture runs without a permission prompt. The instruction must use that exact command prefix (no leading path, no `cd &&`) for the permission to match.
29
+ - Agent-authored captures are authoritative (`capture_source = agent`). The `Stop` hook still parses any `<eagle-summary>` block for backward compatibility, but when an agent row already exists its heuristics only fill empty fields and background enrichment is skipped — neither can clobber agent data.
30
+ - `UserPromptSubmit` context-pressure nudges (≥20 / ≥30 turns) also point at `eagle-mem session save`, not the raw block.
31
+
24
32
  ## Eagle Mem Files Depending On This
25
33
 
26
34
  - `lib/hooks.sh`
@@ -1,6 +1,6 @@
1
1
  # Codex Compatibility
2
2
 
3
- Last verified: 2026-06-02
3
+ Last verified: 2026-06-10
4
4
 
5
5
  ## Official Sources
6
6
 
@@ -22,6 +22,12 @@ Last verified: 2026-06-02
22
22
  - Hook `timeout` is measured in seconds; `statusMessage` is optional.
23
23
  - Codex memory files are generated state. Required team guidance belongs in `AGENTS.md` or checked-in documentation, not only in local memories.
24
24
 
25
+ ## Session Capture Behavior
26
+
27
+ - Codex replies stay prose-only; Eagle Mem never asks Codex to print summary blocks, XML, or hook payloads. The `Stop` hook captures a summary from the Codex rollout transcript automatically.
28
+ - For a richer structured capture, Codex may run `eagle-mem session save --session-id <session_id> --agent codex ...` once at wrap-up (injected by `SessionStart` and the `AGENTS.md` section). This sets `capture_source = agent`, which is authoritative: later Stop-hook heuristics only fill empty fields and background enrichment is skipped, so the capture is never clobbered.
29
+ - Modified-file lists are most reliable when Codex passes `--files-modified` explicitly; the transcript heuristic is Claude-shaped and may not populate file lists from Codex rollout tool calls.
30
+
25
31
  ## Eagle Mem Files Depending On This
26
32
 
27
33
  - `lib/codex-hooks.sh`
@@ -1,6 +1,6 @@
1
1
  # OpenCode Compatibility
2
2
 
3
- Last verified: 2026-06-02
3
+ Last verified: 2026-06-10
4
4
 
5
5
  ## Official Sources
6
6
 
@@ -37,6 +37,7 @@ Last verified: 2026-06-02
37
37
  - `session.idle` maps to Eagle Mem `Stop` using the latest assistant text accumulated from message events.
38
38
  - `session.deleted` maps to Eagle Mem `SessionEnd`.
39
39
  - `experimental.session.compacting` maps to Eagle Mem compact recall by running `SessionStart` with `source=compact` and appending the returned context.
40
+ - Session capture follows the shared clean-capture flow: the agent may run `eagle-mem session save --session-id <id> ...` at wrap-up (sets `capture_source = agent`, authoritative) and keep replies prose-only. The `session.idle` → `Stop` path then only fills gaps and never clobbers an agent-authored row.
40
41
 
41
42
  ## Eagle Mem Files Depending On This
42
43
 
@@ -625,33 +625,24 @@ fi
625
625
  if [ "$agent" = "codex" ]; then
626
626
  context+="
627
627
  === Eagle Mem: Active ===
628
- Memory active for '$project'. Keep user-facing Codex replies clean: do not print Eagle Mem summary capture blocks, XML, JSON hook payloads, or internal templates unless the user explicitly asks. The Stop hook captures summaries from the transcript automatically.
628
+ Memory active for '$project'. Keep user-facing Codex replies clean: do not print Eagle Mem summary capture blocks, XML, JSON hook payloads, or internal templates unless the user explicitly asks. The Stop hook captures summaries from the transcript automatically. When you wrap up substantive work, you may persist a richer structured capture by running once: eagle-mem session save --session-id $session_id --agent codex --completed \"...\" --decisions \"choice — why\" --gotchas \"...\" --files-modified \"a.ts,b.ts\" (fill what applies). Keep the visible reply prose-only.
629
629
  "
630
630
  elif [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
631
631
  context+="
632
632
  === Eagle Mem: Active ===
633
- Memory active. Attribute recalled context to Eagle Mem. Do not revert PostToolUse-surfaced decisions without asking. Emit <eagle-summary> before final response.
633
+ Memory active. Attribute recalled context to Eagle Mem. Do not revert PostToolUse-surfaced decisions without asking. Before your final response, capture the session quietly via the Bash tool (pre-approved): eagle-mem session save --session-id $session_id --completed \"...\" --decisions \"choice — why\" --gotchas \"...\" (fill what applies, omit the rest). Then give a short human recap — NEVER print <eagle-summary> tags or raw field lists.
634
634
  "
635
635
  else
636
636
  context+="
637
637
  === Eagle Mem: Active ===
638
638
  Memory active for '$project'. Scan, index, prune, and self-learning run automatically — never ask the user to run these. Attribute recalled context: \"Eagle Mem recalls:\" Do not revert PostToolUse-surfaced decisions without user request. No raw secrets in summaries. If you contradict a loaded memory, update the memory file.
639
639
 
640
- Before your final response, emit:
641
- <eagle-summary>
642
- request: [what user asked]
643
- completed: [what shipped]
644
- learned: [non-obvious discoveries]
645
- decisions: [choicewhy]
646
- gotchas: [what surprised]
647
- next_steps: [concrete actions]
648
- key_files: [path — role]
649
- files_read: [path, ...]
650
- files_modified: [path, ...]
651
- affected_features: [feature, ...]
652
- verified_features: [feature, ...]
653
- regression_risks: [risk, ...]
654
- </eagle-summary>
640
+ Session capture is clean: NEVER print <eagle-summary> tags, XML, or raw field lists (request:/completed:/...) in your visible reply. When you wrap up substantive work (shipped a change, made decisions worth remembering, the user signals done, or Eagle Mem warns of context pressure), capture it by running this ONCE via the Bash tool — it is pre-approved and prints only a short confirmation. Fill the flags you have; omit the rest:
641
+
642
+ eagle-mem session save --session-id $session_id --request \"...\" --completed \"...\" --learned \"...\" --decisions \"choice — why; choice — why\" --gotchas \"...\" --next-steps \"...\" --key-files \"path — role\" --files-modified \"a.ts,b.ts\" --affected-features \"...\" --verified-features \"...\" --regression-risks \"...\"
643
+
644
+ Then end with a brief, human recap of the session in prose, followed by one line:
645
+ **Eagle Mem** | Session captured N decisions, M gotchas
655
646
  "
656
647
  fi
657
648
 
package/hooks/stop.sh CHANGED
@@ -396,10 +396,30 @@ if [ -n "$regression_notes" ]; then
396
396
  fi
397
397
 
398
398
  # ─── Write to database ─────────────────────────────────────
399
+ #
400
+ # Capture-source precedence:
401
+ # - A parsed <eagle-summary> block this turn is agent-authored → write 'agent'.
402
+ # - An existing 'agent' row (CLI `eagle-mem session save` or an earlier block)
403
+ # is authoritative → fill empty gaps only, never overwrite with heuristics.
404
+ # - Otherwise this is a heuristic capture → normal 'hook' write.
405
+ # agent_authored also gates background enrichment so it can't clobber later.
406
+
407
+ existing_source=$(eagle_summary_capture_source "$session_id" 2>/dev/null || true)
408
+ agent_authored=0
409
+ [ -n "$summary_block" ] && agent_authored=1
410
+ [ "$existing_source" = "agent" ] && agent_authored=1
399
411
 
400
412
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
401
- if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent"; then
402
- eagle_log "INFO" "Stop: summary saved for session=$session_id"
413
+ write_rc=0
414
+ if [ -n "$summary_block" ]; then
415
+ eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "agent" || write_rc=$?
416
+ elif [ "$existing_source" = "agent" ]; then
417
+ eagle_insert_summary_fill_only "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "hook" || write_rc=$?
418
+ else
419
+ eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "hook" || write_rc=$?
420
+ fi
421
+ if [ "$write_rc" -eq 0 ]; then
422
+ eagle_log "INFO" "Stop: summary saved for session=$session_id (prior_source=${existing_source:-none}, block=$([ -n "$summary_block" ] && echo yes || echo no))"
403
423
  eagle_insert_event "$project" "$session_id" "$agent" "memory_created" "" "Stop" "ok" "$(jq -nc --arg source "summary" '{source:$source}')" >/dev/null 2>&1 || true
404
424
  else
405
425
  eagle_log "ERROR" "Stop: summary insert FAILED for session=$session_id — check DB constraints"
@@ -413,7 +433,7 @@ eagle_hook_observability_set_detail "$(jq -nc \
413
433
  '{request_chars:($request | length), completed_chars:($completed | length), files_read:$files_read, files_modified:$files_modified}')"
414
434
  eagle_hook_observability_complete 0
415
435
 
416
- if [ "$defer_enrichment" -eq 1 ] && [ "${EAGLE_MEM_STOP_BACKGROUND_ENRICH:-1}" = "1" ] && [ -n "$text_content" ]; then
436
+ if [ "$defer_enrichment" -eq 1 ] && [ "${EAGLE_MEM_STOP_BACKGROUND_ENRICH:-1}" = "1" ] && [ -n "$text_content" ] && [ "$agent_authored" -eq 0 ]; then
417
437
  mkdir -p "$EAGLE_MEM_DIR/tmp" 2>/dev/null || true
418
438
  enrich_job=$(mktemp "$EAGLE_MEM_DIR/tmp/summary-enrich.XXXXXX.json" 2>/dev/null)
419
439
  if [ -n "$enrich_job" ]; then
@@ -67,8 +67,9 @@ Eagle Mem context pressure: critical ($turn_count turns since compact)
67
67
  else
68
68
  context+="
69
69
  === Eagle Mem: Context Pressure Critical ($turn_count turns since compact) ===
70
- IMMEDIATELY emit a detailed <eagle-summary> covering ALL work this session.
71
- Tell the user to run /compact NOW to avoid losing context.
70
+ IMMEDIATELY capture ALL work this session by running (pre-approved, quiet):
71
+ eagle-mem session save --session-id $session_id --completed \"...\" --decisions \"choice why\" --gotchas \"...\" --learned \"...\" --next-steps \"...\" --key-files \"path — role\"
72
+ Then tell the user to run /compact NOW. Do NOT print <eagle-summary> tags or raw field lists.
72
73
  "
73
74
  fi
74
75
  echo "$turn_count" > "$EAGLE_MEM_DIR/.context-pressure"
@@ -83,8 +84,9 @@ Eagle Mem context pressure: high ($turn_count turns since compact)
83
84
  else
84
85
  context+="
85
86
  === Eagle Mem: Context Pressure High ($turn_count turns since compact) ===
86
- Include a thorough <eagle-summary> in your next response capture all decisions, gotchas, and learned context before compaction.
87
- Suggest the user run /compact to free context for continued work.
87
+ Capture your work before compaction by running (pre-approved, quiet):
88
+ eagle-mem session save --session-id $session_id --completed \"...\" --decisions \"choice why\" --gotchas \"...\" --learned \"...\"
89
+ Then suggest the user run /compact. Do NOT print <eagle-summary> tags or raw field lists.
88
90
  "
89
91
  fi
90
92
  echo "$turn_count" > "$EAGLE_MEM_DIR/.context-pressure"
package/lib/common.sh CHANGED
@@ -1893,16 +1893,19 @@ The person reading this transcript should experience natural, thoughtful, human
1893
1893
 
1894
1894
  The rich structured data still gets saved (so future sessions get excellent context and the anti-regression system keeps working), but it happens without making the transcript feel like you are reading a debug dump.
1895
1895
 
1896
- **Recommended capture approaches (in order of preference):**
1897
- - Best: Use `eagle-mem session save --summary "..."` or the equivalent capture skill/tool. The CLI writes the structured record cleanly; you only show the nice prose to the user.
1898
- - Strong alternative: Put any machine block inside a collapsed `<details>` element so it is available for debugging but invisible by default.
1899
- - Only as last resort: Emit the raw block at all and only when the user has explicitly asked for technical output.
1896
+ **How to capture (clean, no raw blocks):**
1897
+ - When you wrap up substantive work, run this once via the shell it is pre-approved and prints only a short confirmation. Fill the flags you have; omit the rest:
1898
+ `eagle-mem session save --session-id <id> --request "..." --completed "..." --learned "..." --decisions "choice why; choice why" --gotchas "..." --next-steps "..." --key-files "path — role" --files-modified "a.ts,b.ts" --affected-features "..." --verified-features "..." --regression-risks "..."`
1899
+ Your session id is injected by SessionStart; use it verbatim so the capture merges into the live session.
1900
+ - Then end with a short, human recap in prose, followed by one line: `**Eagle Mem** | Session captured — N decisions, M gotchas`.
1901
+ - NEVER print `<eagle-summary>` tags, XML, or raw field lists (`request:`/`completed:`/...) in the visible reply.
1902
+ - If you never run the CLI, the Stop hook still captures a summary automatically as a lower-fidelity fallback — so capture is never lost, it is just richer when you run the command.
1900
1903
 
1901
1904
  **Why this rule exists:** Every single session ends with a summary. If it looks technical, the entire conversation history slowly becomes unpleasant to read. The memory layer should make the experience of working with agents *more* human, not less.
1902
1905
 
1903
1906
  **How to apply:**
1904
1907
  - Write the visible recap in clear, narrative prose first.
1905
- - Persist the structured details via tool or collapsed element — never as raw tags in the main reply.
1908
+ - Persist the structured details via the `session save` command — never as raw tags in the main reply.
1906
1909
  - Keep durable task descriptions truthful and up to date; they often carry more value across compactions than any one summary.
1907
1910
  - When Eagle Mem context appears, attribute it naturally.
1908
1911
  - Protect secrets. Update the durable record if you change direction.
@@ -1916,8 +1919,11 @@ eagle_patch_claude_md() {
1916
1919
  mkdir -p "$HOME/.claude"
1917
1920
 
1918
1921
  if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
1919
- # Check if section has outdated pipe-separated format
1920
- if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null; then
1922
+ # Rewrite when the section uses an outdated capture doctrine:
1923
+ # - the old pipe-separated <eagle-summary> template, or
1924
+ # - the superseded "collapsed <details>" recommendation (pre-CLI-first).
1925
+ if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null \
1926
+ || grep -qF 'collapsed `<details>` element' "$claude_md" 2>/dev/null; then
1921
1927
  # Replace the outdated section: remove old, append new
1922
1928
  local tmp_md
1923
1929
  tmp_md=$(mktemp)
@@ -1965,6 +1971,7 @@ Eagle Mem hooks are active for Codex in this project. SessionStart and UserPromp
1965
1971
  - For broad multi-agent work, YOU run `eagle-mem orchestrate`; do not ask the user to run these commands
1966
1972
  - Codex does not currently expose a persistent custom statusline like Claude Code; if the user asks for Eagle Mem status, run `eagle-mem statusline`
1967
1973
  - For important decisions, preferences, gotchas, or durable project facts, include them briefly in normal prose. Eagle Mem will extract them from the transcript.
1974
+ - To persist a richer structured capture without printing anything raw, run once at wrap-up: `eagle-mem session save --agent codex --completed "..." --decisions "choice — why" --gotchas "..." --files-modified "a.ts,b.ts"` (fill what applies)
1968
1975
  - Do not revert Eagle Mem-surfaced decisions without asking the user
1969
1976
  - If Eagle Mem reports pending feature verification, verify or waive it before push/PR/publish
1970
1977
  - Never put raw secrets in summaries
@@ -20,9 +20,10 @@ eagle_insert_summary() {
20
20
  local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
21
21
  local key_files; key_files=$(eagle_sql_escape "${13:-}")
22
22
  local agent; agent=$(eagle_sql_escape "${14:-$(eagle_agent_source)}")
23
+ local capture_source; capture_source=$(eagle_sql_escape "${15:-}")
23
24
 
24
25
  eagle_db_pipe <<SQL
25
- INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files)
26
+ INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files, capture_source)
26
27
  VALUES (
27
28
  '$session_id',
28
29
  '$project',
@@ -37,7 +38,8 @@ VALUES (
37
38
  '$notes',
38
39
  '$decisions',
39
40
  '$gotchas',
40
- '$key_files'
41
+ '$key_files',
42
+ '$capture_source'
41
43
  )
42
44
  ON CONFLICT(session_id) DO UPDATE SET
43
45
  project = excluded.project,
@@ -52,10 +54,75 @@ ON CONFLICT(session_id) DO UPDATE SET
52
54
  notes = COALESCE(NULLIF(excluded.notes, ''), summaries.notes),
53
55
  decisions = COALESCE(NULLIF(excluded.decisions, ''), summaries.decisions),
54
56
  gotchas = COALESCE(NULLIF(excluded.gotchas, ''), summaries.gotchas),
55
- key_files = COALESCE(NULLIF(excluded.key_files, ''), summaries.key_files);
57
+ key_files = COALESCE(NULLIF(excluded.key_files, ''), summaries.key_files),
58
+ capture_source = CASE
59
+ WHEN summaries.capture_source = 'agent' THEN 'agent'
60
+ ELSE COALESCE(NULLIF(excluded.capture_source, ''), summaries.capture_source)
61
+ END;
56
62
  SQL
57
63
  }
58
64
 
65
+ # Fill-only upsert: OLD value wins, only blank columns get filled. Atomic —
66
+ # no read-modify-write race. Use when a richer agent-authored row already
67
+ # exists and a later writer (Stop heuristics, background enrichment) should
68
+ # only top up missing fields, never overwrite. capture_source stays 'agent'.
69
+ eagle_insert_summary_fill_only() {
70
+ local session_id; session_id=$(eagle_sql_escape "$1")
71
+ local project; project=$(eagle_sql_escape "$2")
72
+ local request; request=$(eagle_sql_escape "$3")
73
+ local investigated; investigated=$(eagle_sql_escape "$4")
74
+ local learned; learned=$(eagle_sql_escape "$5")
75
+ local completed; completed=$(eagle_sql_escape "$6")
76
+ local next_steps; next_steps=$(eagle_sql_escape "$7")
77
+ local files_read; files_read=$(eagle_sql_escape "$8")
78
+ local files_modified; files_modified=$(eagle_sql_escape "$9")
79
+ local notes; notes=$(eagle_sql_escape "${10:-}")
80
+ local decisions; decisions=$(eagle_sql_escape "${11:-}")
81
+ local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
82
+ local key_files; key_files=$(eagle_sql_escape "${13:-}")
83
+ local agent; agent=$(eagle_sql_escape "${14:-$(eagle_agent_source)}")
84
+ local capture_source; capture_source=$(eagle_sql_escape "${15:-}")
85
+
86
+ eagle_db_pipe <<SQL
87
+ INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files, capture_source)
88
+ VALUES (
89
+ '$session_id',
90
+ '$project',
91
+ '$agent',
92
+ '$request',
93
+ '$investigated',
94
+ '$learned',
95
+ '$completed',
96
+ '$next_steps',
97
+ '$files_read',
98
+ '$files_modified',
99
+ '$notes',
100
+ '$decisions',
101
+ '$gotchas',
102
+ '$key_files',
103
+ '$capture_source'
104
+ )
105
+ ON CONFLICT(session_id) DO UPDATE SET
106
+ request = COALESCE(NULLIF(summaries.request, ''), NULLIF(excluded.request, ''), ''),
107
+ investigated = COALESCE(NULLIF(summaries.investigated, ''), NULLIF(excluded.investigated, ''), ''),
108
+ learned = COALESCE(NULLIF(summaries.learned, ''), NULLIF(excluded.learned, ''), ''),
109
+ completed = COALESCE(NULLIF(summaries.completed, ''), NULLIF(excluded.completed, ''), ''),
110
+ next_steps = COALESCE(NULLIF(summaries.next_steps, ''), NULLIF(excluded.next_steps, ''), ''),
111
+ files_read = COALESCE(NULLIF(summaries.files_read, '[]'), NULLIF(excluded.files_read, '[]'), '[]'),
112
+ files_modified = COALESCE(NULLIF(summaries.files_modified, '[]'), NULLIF(excluded.files_modified, '[]'), '[]'),
113
+ notes = COALESCE(NULLIF(summaries.notes, ''), NULLIF(excluded.notes, ''), ''),
114
+ decisions = COALESCE(NULLIF(summaries.decisions, ''), NULLIF(excluded.decisions, ''), ''),
115
+ gotchas = COALESCE(NULLIF(summaries.gotchas, ''), NULLIF(excluded.gotchas, ''), ''),
116
+ key_files = COALESCE(NULLIF(summaries.key_files, ''), NULLIF(excluded.key_files, ''), '');
117
+ SQL
118
+ }
119
+
120
+ # How a session's summary was captured: 'agent' | 'hook' | 'enrich' | ''
121
+ eagle_summary_capture_source() {
122
+ local session_id; session_id=$(eagle_sql_escape "$1")
123
+ eagle_db "SELECT capture_source FROM summaries WHERE session_id = '$session_id' LIMIT 1;"
124
+ }
125
+
59
126
  eagle_get_recent_summaries() {
60
127
  local project_scope="${1:-}"
61
128
  local limit; limit=$(eagle_sql_int "${2:-5}")
package/lib/hooks.sh CHANGED
@@ -31,6 +31,35 @@ eagle_clean_hook_entries() {
31
31
  "$settings" > "$tmp" && mv "$tmp" "$settings"
32
32
  }
33
33
 
34
+ # Add a permissions.allow rule to Claude settings.json so agent-issued capture
35
+ # commands (e.g. `eagle-mem session save`) run without a permission prompt.
36
+ # Idempotent and order-preserving: appends only when the rule is absent, and
37
+ # leaves existing permissions.deny/ask and allow entries untouched.
38
+ eagle_patch_permission_allow() {
39
+ local settings="$1"
40
+ local rule="$2"
41
+ [ -f "$settings" ] || return 0
42
+ command -v jq >/dev/null 2>&1 || return 0
43
+
44
+ if jq -e --arg r "$rule" '(.permissions.allow // []) | index($r) != null' "$settings" >/dev/null 2>&1; then
45
+ return 0
46
+ fi
47
+
48
+ eagle_backup_file "$settings"
49
+ local tmp
50
+ tmp=$(mktemp)
51
+ if jq --arg r "$rule" '
52
+ .permissions = (.permissions // {})
53
+ | .permissions.allow = ((.permissions.allow // []) + [$r])
54
+ ' "$settings" > "$tmp" 2>/dev/null; then
55
+ mv "$tmp" "$settings"
56
+ else
57
+ rm -f "$tmp" 2>/dev/null || true
58
+ return 1
59
+ fi
60
+ return 0
61
+ }
62
+
34
63
  eagle_patch_hook() {
35
64
  local settings="$1"
36
65
  local event="$2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.11.0",
3
+ "version": "4.12.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"
@@ -157,7 +157,7 @@ if [ "$readiness" = "strong" ]; then
157
157
  elif [ "$readiness" = "moderate" ]; then
158
158
  eagle_info "Compaction Survival: Moderate — add more durable tasks and summaries"
159
159
  else
160
- eagle_warn "Compaction Survival: Weak — start using durable tasks and <eagle-summary> blocks"
160
+ eagle_warn "Compaction Survival: Weak — start using durable tasks and 'eagle-mem session save' captures"
161
161
  fi
162
162
 
163
163
  echo ""
@@ -90,8 +90,14 @@ gotchas=$(extract_section "$enrich_result" "GOTCHAS" | eagle_redact)
90
90
  key_files=$(extract_section "$enrich_result" "KEY_FILES" | eagle_redact)
91
91
 
92
92
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ] || [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ]; then
93
- eagle_insert_summary "$session_id" "$project" "$request" "" "$learned" "$completed" "" "[]" "[]" "" "$decisions" "$gotchas" "$key_files" "$agent"
94
- eagle_log "INFO" "Summary enrichment saved for session=$session_id provider=$provider"
93
+ existing_source=$(eagle_summary_capture_source "$session_id" 2>/dev/null || true)
94
+ if [ "$existing_source" = "agent" ]; then
95
+ # An agent-authored row exists — only fill gaps, never overwrite it.
96
+ eagle_insert_summary_fill_only "$session_id" "$project" "$request" "" "$learned" "$completed" "" "[]" "[]" "" "$decisions" "$gotchas" "$key_files" "$agent" "enrich"
97
+ else
98
+ eagle_insert_summary "$session_id" "$project" "$request" "" "$learned" "$completed" "" "[]" "[]" "" "$decisions" "$gotchas" "$key_files" "$agent" "enrich"
99
+ fi
100
+ eagle_log "INFO" "Summary enrichment saved for session=$session_id provider=$provider source=${existing_source:-none}"
95
101
  fi
96
102
 
97
103
  rm -f "$job_file" 2>/dev/null || true
@@ -319,6 +319,11 @@ if [ "$claude_found" = true ]; then
319
319
  eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" \
320
320
  "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
321
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
322
327
  fi
323
328
  else
324
329
  eagle_info "Claude hooks skipped ${DIM}(Claude Code not detected)${RESET}"
@@ -19,21 +19,30 @@ show_help() {
19
19
  echo -e " eagle-mem session ${CYAN}save <text>${RESET}"
20
20
  echo ""
21
21
  echo -e " ${BOLD}Options for save:${RESET}"
22
- echo -e " ${CYAN}--summary${RESET} <text> Summary to store"
22
+ echo -e " ${CYAN}--completed${RESET} <text> What was accomplished (alias: --summary)"
23
23
  echo -e " ${CYAN}--request${RESET} <text> User request that caused the work"
24
+ echo -e " ${CYAN}--investigated${RESET} <text> What was explored or analyzed"
24
25
  echo -e " ${CYAN}--learned${RESET} <text> Non-obvious discoveries"
25
26
  echo -e " ${CYAN}--decisions${RESET} <text> Decisions and why"
26
27
  echo -e " ${CYAN}--gotchas${RESET} <text> Surprises or pitfalls"
27
28
  echo -e " ${CYAN}--next-steps${RESET} <text> Follow-up work"
28
- echo -e " ${CYAN}--key-files${RESET} <text> Important files"
29
+ echo -e " ${CYAN}--key-files${RESET} <text> Important files (path — role)"
30
+ echo -e " ${CYAN}--files-read${RESET} <list> Comma-separated files read"
31
+ echo -e " ${CYAN}--files-modified${RESET} <list> Comma-separated files modified"
32
+ echo -e " ${CYAN}--affected-features${RESET} <text> Features touched (need re-verify)"
33
+ echo -e " ${CYAN}--verified-features${RESET} <text> Features verified this session"
34
+ echo -e " ${CYAN}--regression-risks${RESET} <text> Known risks introduced"
29
35
  echo -e " ${CYAN}--notes${RESET} <text> Extra notes"
36
+ echo -e " ${CYAN}--session-id${RESET} <id> Live session id (merges into the active row;"
37
+ echo -e " omit for a standalone manual save)"
30
38
  echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current git root)"
31
- echo -e " ${CYAN}--agent${RESET} <name> Source agent: codex or claude-code"
39
+ echo -e " ${CYAN}--agent${RESET} <name> Source: claude-code, codex, antigravity, opencode, grok"
32
40
  echo -e " ${CYAN}--cwd${RESET} <path> Working directory for project detection"
33
41
  echo -e " ${CYAN}--json${RESET} Output JSON"
34
42
  echo ""
35
- echo -e " ${DIM}This command is mainly for agent fallbacks. Normal sessions are captured${RESET}"
36
- echo -e " ${DIM}automatically by hooks when Claude Code or Codex stops a turn.${RESET}"
43
+ echo -e " ${DIM}Agents use this to capture a clean, branded session summary without printing${RESET}"
44
+ echo -e " ${DIM}raw blocks. Pass --session-id <id> to merge into the live session row. Stop${RESET}"
45
+ echo -e " ${DIM}hooks still capture automatically as a safety net when no save is made.${RESET}"
37
46
  echo ""
38
47
  }
39
48
 
@@ -49,24 +58,48 @@ json_string() {
49
58
  jq -Rn --arg v "${1:-}" '$v'
50
59
  }
51
60
 
61
+ # Comma-separated list → JSON array (matches hooks/stop.sh storage shape).
62
+ # Safe under set -e: empty input yields [] without a failing pipeline.
63
+ # Slurp the whole value (-s) so embedded newlines split into items rather than
64
+ # emitting one JSON array per line (which would corrupt the stored column).
65
+ csv_to_json_array() {
66
+ local raw="${1:-}"
67
+ [ -z "$raw" ] && { echo '[]'; return 0; }
68
+ printf '%s' "$raw" | jq -Rsc 'split("[,\n]"; "") | map(gsub("^\\s+|\\s+$";"")) | map(select(. != ""))'
69
+ }
70
+
71
+ # Count items in a field separated by ';' or newlines (for the capture banner).
72
+ count_items() {
73
+ local text="${1:-}"
74
+ [ -z "$text" ] && { echo 0; return 0; }
75
+ printf '%s' "$text" | awk 'BEGIN{RS="[;\n]"} {gsub(/^[ \t]+|[ \t]+$/,""); if($0!="") n++} END{print n+0}'
76
+ }
77
+
52
78
  save_session() {
53
79
  local summary=""
54
- local request="Manual session save"
80
+ local request=""
81
+ local investigated=""
55
82
  local learned=""
56
83
  local decisions=""
57
84
  local gotchas=""
58
85
  local next_steps=""
59
86
  local key_files=""
87
+ local files_read_raw=""
88
+ local files_modified_raw=""
89
+ local affected_features=""
90
+ local verified_features=""
91
+ local regression_risks=""
60
92
  local notes=""
61
93
  local project=""
62
94
  local cwd
63
95
  cwd="$(pwd)"
64
96
  local agent=""
97
+ local cli_session_id=""
65
98
  local json_output=false
66
99
 
67
100
  while [ $# -gt 0 ]; do
68
101
  case "$1" in
69
- --summary)
102
+ --summary|--completed)
70
103
  require_value "$1" "${2:-}"
71
104
  summary="$2"
72
105
  shift 2
@@ -76,6 +109,11 @@ save_session() {
76
109
  request="$2"
77
110
  shift 2
78
111
  ;;
112
+ --investigated)
113
+ require_value "$1" "${2:-}"
114
+ investigated="$2"
115
+ shift 2
116
+ ;;
79
117
  --learned)
80
118
  require_value "$1" "${2:-}"
81
119
  learned="$2"
@@ -101,11 +139,41 @@ save_session() {
101
139
  key_files="$2"
102
140
  shift 2
103
141
  ;;
142
+ --files-read)
143
+ require_value "$1" "${2:-}"
144
+ files_read_raw="$2"
145
+ shift 2
146
+ ;;
147
+ --files-modified)
148
+ require_value "$1" "${2:-}"
149
+ files_modified_raw="$2"
150
+ shift 2
151
+ ;;
152
+ --affected-features)
153
+ require_value "$1" "${2:-}"
154
+ affected_features="$2"
155
+ shift 2
156
+ ;;
157
+ --verified-features)
158
+ require_value "$1" "${2:-}"
159
+ verified_features="$2"
160
+ shift 2
161
+ ;;
162
+ --regression-risks)
163
+ require_value "$1" "${2:-}"
164
+ regression_risks="$2"
165
+ shift 2
166
+ ;;
104
167
  --notes)
105
168
  require_value "$1" "${2:-}"
106
169
  notes="$2"
107
170
  shift 2
108
171
  ;;
172
+ --session-id)
173
+ require_value "$1" "${2:-}"
174
+ cli_session_id="$2"
175
+ shift 2
176
+ ;;
109
177
  --project|-p)
110
178
  require_value "$1" "${2:-}"
111
179
  project="$2"
@@ -147,11 +215,25 @@ save_session() {
147
215
  esac
148
216
  done
149
217
 
150
- if [ -z "$summary" ]; then
151
- eagle_err "Nothing to save. Pass --summary <text>."
218
+ if [ -z "$summary" ] && [ -z "$learned" ] && [ -z "$decisions" ] && [ -z "$gotchas" ] \
219
+ && [ -z "$next_steps" ] && [ -z "$key_files" ] && [ -z "$investigated" ] \
220
+ && [ -z "$files_read_raw" ] && [ -z "$files_modified_raw" ] \
221
+ && [ -z "$affected_features" ] && [ -z "$verified_features" ] && [ -z "$regression_risks" ]; then
222
+ eagle_err "Nothing to save. Pass --completed/--summary (or another field)."
152
223
  exit 1
153
224
  fi
154
225
 
226
+ if [ -n "$cli_session_id" ] && ! eagle_validate_session_id "$cli_session_id"; then
227
+ eagle_err "Invalid --session-id (allowed: letters, digits, '_', '-', max 128 chars)."
228
+ exit 1
229
+ fi
230
+
231
+ # For a live session, prefer its recorded project so a save issued from a
232
+ # subdirectory (whose cwd derives a different key) does not trigger an
233
+ # unintended project rekey of the active session.
234
+ if [ -z "$project" ] && [ -n "$cli_session_id" ] && [ -f "${EAGLE_MEM_DB:-}" ]; then
235
+ project=$(eagle_db "SELECT project FROM sessions WHERE id='$(eagle_sql_escape "$cli_session_id")' LIMIT 1;" 2>/dev/null || true)
236
+ fi
155
237
  [ -z "$project" ] && project=$(eagle_project_from_cwd "$cwd")
156
238
  if [ -z "$project" ]; then
157
239
  eagle_err "Could not determine project. Re-run with --project <name>."
@@ -165,44 +247,99 @@ save_session() {
165
247
  codex|openai-codex) agent="codex" ;;
166
248
  claude|claude-code|cloud-code) agent="claude-code" ;;
167
249
  antigravity*|google-antigravity*|google_antigravity*) agent="antigravity" ;;
250
+ opencode) agent="opencode" ;;
251
+ grok|grok-cli) agent="grok" ;;
168
252
  *)
169
- eagle_err "--agent must be codex, claude-code, or antigravity"
253
+ eagle_err "--agent must be codex, claude-code, antigravity, opencode, or grok"
170
254
  exit 1
171
255
  ;;
172
256
  esac
173
257
  fi
174
258
 
259
+ # Default request only for manual (non-session-id) saves; for a live
260
+ # session leave it empty so the Stop hook's real request is preserved.
261
+ if [ -z "$request" ] && [ -z "$cli_session_id" ]; then
262
+ request="Manual session save"
263
+ fi
264
+
265
+ local files_read files_modified
266
+ files_read=$(csv_to_json_array "$files_read_raw")
267
+ files_modified=$(csv_to_json_array "$files_modified_raw")
268
+
175
269
  summary=$(printf '%s' "$summary" | eagle_redact)
176
270
  request=$(printf '%s' "$request" | eagle_redact)
271
+ investigated=$(printf '%s' "$investigated" | eagle_redact)
177
272
  learned=$(printf '%s' "$learned" | eagle_redact)
178
273
  decisions=$(printf '%s' "$decisions" | eagle_redact)
179
274
  gotchas=$(printf '%s' "$gotchas" | eagle_redact)
180
275
  next_steps=$(printf '%s' "$next_steps" | eagle_redact)
181
276
  key_files=$(printf '%s' "$key_files" | eagle_redact)
182
277
  notes=$(printf '%s' "$notes" | eagle_redact)
278
+ affected_features=$(printf '%s' "$affected_features" | eagle_redact)
279
+ verified_features=$(printf '%s' "$verified_features" | eagle_redact)
280
+ regression_risks=$(printf '%s' "$regression_risks" | eagle_redact)
281
+
282
+ # Fold feature-tracking fields into notes (same shape as hooks/stop.sh)
283
+ local regression_notes=""
284
+ [ -n "$affected_features" ] && regression_notes+="affected_features: $affected_features"
285
+ if [ -n "$verified_features" ]; then
286
+ [ -n "$regression_notes" ] && regression_notes+="; "
287
+ regression_notes+="verified_features: $verified_features"
288
+ fi
289
+ if [ -n "$regression_risks" ]; then
290
+ [ -n "$regression_notes" ] && regression_notes+="; "
291
+ regression_notes+="regression_risks: $regression_risks"
292
+ fi
293
+ if [ -n "$regression_notes" ]; then
294
+ if [ -n "$notes" ]; then
295
+ notes="${notes}; ${regression_notes}"
296
+ else
297
+ notes="$regression_notes"
298
+ fi
299
+ fi
183
300
 
184
301
  eagle_ensure_db
185
302
 
186
- local stamp session_id
187
- stamp=$(date -u +%Y%m%dT%H%M%SZ)
188
- session_id="manual-${stamp}-$$-${RANDOM:-0}"
303
+ local session_id end_after_save=1
304
+ if [ -n "$cli_session_id" ]; then
305
+ # Live session: ensure the row exists and stays active. Empty source/cwd
306
+ # preserve whatever SessionStart already recorded; do NOT end the session.
307
+ session_id="$cli_session_id"
308
+ end_after_save=0
309
+ eagle_upsert_session "$session_id" "$project" "" "" "" "$agent"
310
+ else
311
+ local stamp
312
+ stamp=$(date -u +%Y%m%dT%H%M%SZ)
313
+ session_id="manual-${stamp}-$$-${RANDOM:-0}"
314
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" "manual" "$agent"
315
+ fi
316
+
317
+ # capture_source=agent: agent-authored capture is authoritative and must not
318
+ # be clobbered by later Stop-hook heuristics or background enrichment.
319
+ eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$summary" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "agent"
320
+
321
+ [ "$end_after_save" = "1" ] && eagle_end_session "$session_id"
189
322
 
190
- eagle_upsert_session "$session_id" "$project" "$cwd" "" "manual" "$agent"
191
- eagle_insert_summary "$session_id" "$project" "$request" "" "$learned" "$summary" "$next_steps" "[]" "[]" "$notes" "$decisions" "$gotchas" "$key_files" "$agent"
192
- eagle_end_session "$session_id"
323
+ local n_dec n_got dec_word got_word
324
+ n_dec=$(count_items "$decisions")
325
+ n_got=$(count_items "$gotchas")
326
+ [ "$n_dec" = "1" ] && dec_word="decision" || dec_word="decisions"
327
+ [ "$n_got" = "1" ] && got_word="gotcha" || got_word="gotchas"
193
328
 
194
329
  if [ "$json_output" = true ]; then
195
330
  printf '{'
196
331
  printf '"session_id":%s,' "$(json_string "$session_id")"
197
332
  printf '"project":%s,' "$(json_string "$project")"
198
333
  printf '"agent":%s,' "$(json_string "$agent")"
334
+ printf '"decisions":%s,' "$n_dec"
335
+ printf '"gotchas":%s,' "$n_got"
199
336
  printf '"summary":%s' "$(json_string "$summary")"
200
337
  printf '}\n'
201
338
  else
202
- eagle_ok "Session summary saved"
339
+ printf ' %bEagle Mem%b | Session captured — %s %s, %s %s\n' \
340
+ "$CYAN" "$RESET" "$n_dec" "$dec_word" "$n_got" "$got_word"
203
341
  eagle_kv "Project:" "$project"
204
342
  eagle_kv "Source:" "$(eagle_agent_label "$agent")"
205
- eagle_kv "Session:" "$session_id"
206
343
  fi
207
344
  }
208
345
 
package/scripts/update.sh CHANGED
@@ -100,6 +100,9 @@ if [ "$claude_found" = true ] && [ -f "$SETTINGS" ] && command -v jq &>/dev/null
100
100
  eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
101
101
  eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
102
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
+
103
106
  eagle_ok "Hooks registered"
104
107
  fi
105
108
 
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Clean, branded session capture regression test
4
+ # Covers: CLI-first capture, clobber prevention, backward-compat
5
+ # <eagle-summary> override, and enrichment fill-only.
6
+ # ═══════════════════════════════════════════════════════════
7
+ set -uo pipefail
8
+
9
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+ tmp_dir=$(mktemp -d)
11
+ trap 'rm -rf "$tmp_dir"' EXIT
12
+
13
+ export EAGLE_MEM_DIR="$tmp_dir/em"
14
+ export EAGLE_AGENT_SOURCE="claude-code"
15
+ export EAGLE_MEM_DISABLE_HOOKS=0
16
+ # Pin the project so both the CLI save and the Stop hook resolve identically
17
+ # (the temp cwd lives under /var/folders, which Eagle Mem treats as ephemeral).
18
+ export EAGLE_MEM_PROJECT="test/clean-capture"
19
+ mkdir -p "$EAGLE_MEM_DIR"
20
+
21
+ pass=0; fail=0
22
+ ok() { echo " ok: $1"; pass=$((pass+1)); }
23
+ bad() { echo " FAIL: $1" >&2; fail=$((fail+1)); }
24
+
25
+ bash "$ROOT_DIR/db/migrate.sh" >/dev/null 2>&1
26
+
27
+ . "$ROOT_DIR/lib/common.sh"
28
+ . "$ROOT_DIR/lib/db.sh"
29
+
30
+ # A real cwd for the hook payload; project is pinned via EAGLE_MEM_PROJECT.
31
+ work="$tmp_dir/work"
32
+ mkdir -p "$work"
33
+ PROJECT="$EAGLE_MEM_PROJECT"
34
+ ok "pinned project: $PROJECT"
35
+
36
+ field() { eagle_db "SELECT COALESCE($2,'') FROM summaries WHERE session_id='$1';"; }
37
+
38
+ # ── Scenario A: CLI capture then a heuristic Stop must NOT clobber it ──
39
+ SID="clean-capture-aaa111"
40
+ eagle_upsert_session "$SID" "$PROJECT" "$work" "" "startup" "claude-code"
41
+
42
+ bash "$ROOT_DIR/scripts/session.sh" save --session-id "$SID" --project "$PROJECT" \
43
+ --completed "Rich agent completed" \
44
+ --decisions "Pin v1 — why safety; Use TOTP — why no SMTP" \
45
+ --gotchas "needs PUBLIC_URL" >/dev/null 2>&1
46
+
47
+ [ "$(field "$SID" capture_source)" = "agent" ] && ok "A: capture_source=agent after CLI save" || bad "A: capture_source not agent"
48
+
49
+ # Heuristic Stop: transcript has an Edit (fills files), NO <eagle-summary> block.
50
+ tA="$tmp_dir/transcriptA.jsonl"
51
+ {
52
+ printf '%s\n' '{"type":"user","message":{"content":[{"type":"text","text":"please harden the pipeline"}]}}'
53
+ printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Edit","input":{"file_path":"'"$work"'/foo.sh"}}]}}'
54
+ printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"Done. We hardened the pipeline this session."}]}}'
55
+ } > "$tA"
56
+
57
+ stopA=$(jq -nc --arg s "$SID" --arg c "$work" --arg t "$tA" \
58
+ '{session_id:$s, cwd:$c, transcript_path:$t}')
59
+ echo "$stopA" | EAGLE_MEM_STOP_BACKGROUND_ENRICH=0 bash "$ROOT_DIR/hooks/stop.sh" >/dev/null 2>&1
60
+
61
+ [ "$(field "$SID" completed)" = "Rich agent completed" ] && ok "A: completed NOT clobbered by heuristic" || bad "A: completed was clobbered -> '$(field "$SID" completed)'"
62
+ [ "$(field "$SID" capture_source)" = "agent" ] && ok "A: capture_source still agent after Stop" || bad "A: capture_source downgraded"
63
+ case "$(field "$SID" files_modified)" in *foo.sh*) ok "A: files_modified gap-filled by heuristic" ;; *) bad "A: files_modified not filled -> '$(field "$SID" files_modified)'" ;; esac
64
+ case "$(field "$SID" decisions)" in *"Pin v1"*) ok "A: decisions preserved" ;; *) bad "A: decisions lost" ;; esac
65
+ mrows=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE session_id LIKE 'manual-%';")
66
+ [ "$mrows" = "0" ] && ok "A: no stray manual-* row" || bad "A: $mrows manual-* rows created"
67
+ sstatus=$(eagle_db "SELECT status FROM sessions WHERE id='$SID';")
68
+ [ "$sstatus" = "active" ] && ok "A: live session still active (not ended)" || bad "A: session status=$sstatus"
69
+
70
+ # ── Scenario B: a <eagle-summary> block still overrides (backward compat) ──
71
+ SIDB="clean-capture-bbb222"
72
+ eagle_upsert_session "$SIDB" "$PROJECT" "$work" "" "startup" "claude-code"
73
+ tB="$tmp_dir/transcriptB.jsonl"
74
+ blocktext='Summary below.
75
+
76
+ <eagle-summary>
77
+ request: legacy block path
78
+ completed: shipped via block
79
+ decisions: keep parser — why backward compat
80
+ gotchas: none
81
+ </eagle-summary>'
82
+ {
83
+ printf '%s\n' '{"type":"user","message":{"content":[{"type":"text","text":"do the legacy thing"}]}}'
84
+ printf '%s\n' "$(jq -nc --arg t "$blocktext" '{type:"assistant",message:{content:[{type:"text",text:$t}]}}')"
85
+ } > "$tB"
86
+ stopB=$(jq -nc --arg s "$SIDB" --arg c "$work" --arg t "$tB" '{session_id:$s, cwd:$c, transcript_path:$t}')
87
+ echo "$stopB" | EAGLE_MEM_STOP_BACKGROUND_ENRICH=0 bash "$ROOT_DIR/hooks/stop.sh" >/dev/null 2>&1
88
+
89
+ [ "$(field "$SIDB" capture_source)" = "agent" ] && ok "B: block sets capture_source=agent" || bad "B: capture_source=$(field "$SIDB" capture_source)"
90
+ case "$(field "$SIDB" completed)" in *"shipped via block"*) ok "B: completed parsed from block" ;; *) bad "B: block completed not parsed" ;; esac
91
+ case "$(field "$SIDB" decisions)" in *"keep parser"*) ok "B: decisions parsed from block" ;; *) bad "B: block decisions not parsed" ;; esac
92
+
93
+ # ── Scenario C: enrichment must not overwrite an agent row ──
94
+ SIDC="clean-capture-ccc333"
95
+ eagle_upsert_session "$SIDC" "$PROJECT" "$work" "" "startup" "claude-code"
96
+ eagle_insert_summary "$SIDC" "$PROJECT" "req" "" "" "agent completed C" "" "[]" "[]" "" "agent decision C" "" "" "claude-code" "agent"
97
+ # fill-only path used by enrich when source=agent: simulate enrich trying to overwrite
98
+ eagle_insert_summary_fill_only "$SIDC" "$PROJECT" "" "" "enriched learned" "ENRICH OVERWRITE" "" "[]" "[]" "" "ENRICH DECISION" "" "" "claude-code" "enrich"
99
+ [ "$(field "$SIDC" completed)" = "agent completed C" ] && ok "C: enrich fill-only did NOT overwrite completed" || bad "C: enrich overwrote -> '$(field "$SIDC" completed)'"
100
+ case "$(field "$SIDC" learned)" in *"enriched learned"*) ok "C: enrich filled empty learned gap" ;; *) bad "C: enrich did not fill empty learned" ;; esac
101
+ [ "$(field "$SIDC" capture_source)" = "agent" ] && ok "C: capture_source stays agent through enrich" || bad "C: capture_source changed"
102
+
103
+ echo ""
104
+ echo "test_clean_session_capture: $pass passed, $fail failed"
105
+ [ "$fail" -eq 0 ]