eagle-mem 4.15.1 → 4.16.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,19 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.16.0 Factory Droid as a First-Class Agent
8
+
9
+ Factory Droid (the `droid` CLI) joins Claude Code, Codex, OpenCode, and Antigravity as a fully hook-backed agent. It now gets the complete automatic lifecycle — SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, and PreCompact — with no manual setup beyond installing Eagle Mem.
10
+
11
+ - **Why.** Factory Droid was previously unsupported. A hand-rolled scaffold (a `~/.factory/hooks.json` plus a translation adapter) had been attempted but was silently dead: Factory does not read `hooks.json` — its hooks live in `~/.factory/settings.json`, the **same** file that holds the user's `apiKey`, `customModels`, and `statusLine`. Registering there demands the same secret-preserving discipline Eagle Mem already uses for Claude/Codex/OpenCode config files.
12
+ - **What.** New `lib/factory-hooks.sh` registers the 8 handlers into `~/.factory/settings.json` using the documented Factory entry schema `{ "type": "command", "command": "<shell>", "timeout": <seconds> }` — no `statusMessage` key (it isn't in the schema, and an unknown key in an `apiKey`-bearing file risks strict-parse breakage). Handlers are grouped under `hooks.<Event>` as matcher-groups; matchers (`PreToolUse`/`PostToolUse`/`PreCompact` only) use Factory's tool names — `Create` (write), `Execute` (shell), `Edit`, `Read`. There is no translation adapter: each handler sets `EAGLE_AGENT_SOURCE=factory` and invokes the shared `$EAGLE_MEM_DIR/hooks/<hook>.sh` directly, so rows record `agent = factory` (displayed as `Factory Droid`). Factory's transcript is a third JSONL schema (`type:"message"`, role at `.message.role`, `.message.content` always an array); the existing `or`-union jq selectors already serve it, so capture works with no extraction rewrite.
13
+ - **Safety.** Registration rewrites **only** the `.hooks` subtree of `settings.json`. `apiKey`, `customModels`, `statusLine`, and any other sibling are preserved verbatim, written atomically (tmpfile + `mv`) with a once-per-run backup before the first write, and the file's `600` mode is inherited. The event name is indexed dynamically via a jq `--arg` (injection-safe). Removal (`eagle_remove_factory_hooks`, called by `uninstall.sh`) strips only commands containing `.eagle-mem/hooks/` and leaves secrets intact. No change to any shared hook's stdin/stdout/exit contract.
14
+ - **Registration.** `scripts/update.sh` registers the Factory hooks when Factory is detected (`~/.factory` exists or `droid` is on `PATH`) and `jq` is present; `eagle-mem update` adds them to existing installs automatically. `scripts/doctor.sh` reports Factory status. Registration deduplicates on `(matcher, command)` and updates a stale command in place at the same `.eagle-mem/hooks/<script>.sh` path (e.g. a wrapper-prefix change), so re-running is idempotent.
15
+
16
+ New coverage: `tests/test_factory_hooks_config.sh` (secret-preservation, exactly-8 `{type,command,timeout}` handlers, no `statusMessage`, correct matchers, idempotency, in-place update, clean removal) and fixtures `tests/fixtures/agent-hooks/factory-post-tool-use.json` + `factory-create.json`. Compatibility evidence: `docs/agent-compatibility/factory.md`.
17
+
18
+ ---
19
+
7
20
  ## v4.15.1 Documentation & npm Listing Refresh
8
21
 
9
22
  Docs-only patch. No runtime code change — no edits to hooks, `lib`, `db`, `scripts`, or any stdin/stdout/exit contract.
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  # Eagle Mem
9
9
 
10
- **Shared memory, release guardrails, and worker lanes for Claude Code, Codex, OpenCode, Grok, and Google Antigravity.**
10
+ **Shared memory, release guardrails, and worker lanes for Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid.**
11
11
 
12
- Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code, Codex, OpenCode, and Google Antigravity hook-backed shared memory, gives Grok the same skills and CLI memory surface, labels which agent created each memory, blocks risky release commands until affected features are verified, and lets broad work split into durable worker lanes.
12
+ Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid hook-backed shared memory, gives Grok the same skills and CLI memory surface, labels which agent created each memory, blocks risky release commands until affected features are verified, and lets broad work split into durable worker lanes.
13
13
 
14
14
  **v4.10.5 and onward focuses on Graph Memory, Dream Cycle Curation, OpenCode, Grok, Google Antigravity support, and Compaction Survival:** OpenCode users get a global local plugin plus linked skills, Grok users get first-class skill linking and `eagle-mem grok-bootstrap`, and Antigravity users get native Python SDK hook integration via `google_antigravity_hook.py`. Claude Code, Codex, OpenCode, and Antigravity receive the deepest automatic lifecycle support through hooks or plugins; Grok currently uses the shared CLI and skill workflow.
15
15
 
@@ -17,6 +17,8 @@ Eagle Mem turns AI coding sessions into compounding project knowledge. It gives
17
17
 
18
18
  **v4.15.0 adds PreCompact Synchronous Capture:** a new lifecycle hook captures a rich, LLM-enriched session summary *before* Claude Code compacts the context window (manual or auto), closing the gap where the background enricher could otherwise finish after the window collapsed. It is side-effect-only — it never blocks compaction and never overwrites an agent-authored `eagle-mem session save`. Recent releases also added release-gate parity at the git layer (`eagle-mem gate` plus an opt-in `pre-push` hook) and provenance with a trust gate for curator-generated command rules (v4.14.0), plus a `busy_timeout` echo fix that closed a session→project mis-filing vector (v4.14.1). See [`CHANGELOG.md`](CHANGELOG.md).
19
19
 
20
+ **v4.16.0 adds Factory Droid as a first-class agent:** Factory Droid (the `droid` CLI) now gets the full automatic hook lifecycle — SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, and PreCompact — registered into `~/.factory/settings.json` alongside Claude Code, Codex, OpenCode, and Antigravity. Registration rewrites only the `.hooks` subtree and preserves the `apiKey`, `customModels`, and `statusLine` siblings in that file verbatim. Captured rows are source-attributed as `Factory Droid`, and Factory's distinct tool names (`Create`, `Execute`, `Edit`, `Read`) and transcript schema are handled natively. See [`CHANGELOG.md`](CHANGELOG.md).
21
+
20
22
  **Website:** [Product](https://eagleisbatman.github.io/eagle-mem/) |
21
23
  [Architecture](https://eagleisbatman.github.io/eagle-mem/architecture.html) |
22
24
  [About](https://eagleisbatman.github.io/eagle-mem/about.html)
@@ -26,12 +28,12 @@ Eagle Mem turns AI coding sessions into compounding project knowledge. It gives
26
28
  - **Start warmer** - every new session can recall project overviews, decisions, gotchas, summaries, hot files, mirrored memories, plans, and tasks.
27
29
  - **Ship safer** - feature-mapped changes create pending verification records, and release-boundary commands stay blocked until the current diff is verified or waived.
28
30
  - **Waste fewer tokens** - Eagle Mem injects compact context, nudges duplicate reads, and can route noisy shell output through RTK.
29
- - **Coordinate agents** - Claude, Codex, OpenCode, Grok, and Antigravity can share one project memory while worker lanes record owner, model, effort, worktree, logs, validation, and handoff.
31
+ - **Coordinate agents** - Claude, Codex, OpenCode, Grok, Antigravity, and Factory Droid can share one project memory while worker lanes record owner, model, effort, worktree, logs, validation, and handoff.
30
32
  - **Stay local** - no daemon, no hosted memory service, no vector database. The core is hooks plus SQLite/FTS5.
31
33
 
32
34
  ## The Problem
33
35
 
34
- Claude Code, Codex, OpenCode, Grok, and Google Antigravity start every session with amnesia. They don't remember what you built yesterday, what decisions you made, what files matter, or what broke last time. Every `/compact` wipes context. Every new session is a cold start. You waste tokens re-explaining your project, re-reading files, and watching agents repeat mistakes you already corrected.
36
+ Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid start every session with amnesia. They don't remember what you built yesterday, what decisions you made, what files matter, or what broke last time. Every `/compact` wipes context. Every new session is a cold start. You waste tokens re-explaining your project, re-reading files, and watching agents repeat mistakes you already corrected.
35
37
 
36
38
  The longer you work with Claude Code, the worse this gets. Projects accumulate history — decisions, gotchas, architectural patterns, feature dependencies — and none of it survives across sessions.
37
39
 
@@ -45,7 +47,7 @@ Eagle Mem is a local runtime layer for AI coding agents. It adds three things th
45
47
  | **Guardrails** | "The agent cannot casually undo known decisions or push unverified feature changes." | Surfaces decisions before edits and enforces feature verification on push, PR, and publish boundaries. |
46
48
  | **Lanes** | "A big task can survive compaction and split across agents." | Persists orchestrations, worker lanes, worktrees, logs, validation commands, and handoffs. |
47
49
 
48
- Claude Code, Codex, OpenCode, and Google Antigravity share the same SQLite database at `~/.eagle-mem/memory.db`, and captured rows are source-attributed as `Claude Code`, `Codex`, `OpenCode`, or `Antigravity`. Grok uses the same database through skills and CLI commands.
50
+ Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid share the same SQLite database at `~/.eagle-mem/memory.db`, and captured rows are source-attributed as `Claude Code`, `Codex`, `OpenCode`, `Antigravity`, or `Factory Droid`. Grok uses the same database through skills and CLI commands.
49
51
 
50
52
  **Zero per-instance overhead.** No daemon, no vector DB, no MCP server. Just bash/python hooks, sqlite3 (WAL mode, FTS5 full-text search), and jq.
51
53
 
@@ -70,17 +72,17 @@ eagle-mem install
70
72
  eagle-mem doctor
71
73
  ```
72
74
 
73
- That's it. `doctor` should report a healthy install. Open Claude Code, Codex, or OpenCode, or import the Antigravity hook in your agent config, and Eagle Mem activates automatically. For Grok, run `eagle-mem grok-bootstrap` after install to confirm the linked skills and CLI workflow.
75
+ That's it. `doctor` should report a healthy install. Open Claude Code, Codex, OpenCode, or Factory Droid, or import the Antigravity hook in your agent config, and Eagle Mem activates automatically. For Grok, run `eagle-mem grok-bootstrap` after install to confirm the linked skills and CLI workflow.
74
76
 
75
- For Claude Code, Codex, OpenCode, and Google Antigravity, everything is automatic. Eagle Mem scans your codebase, indexes source files, captures session summaries, mirrors memories and tasks (including planning-mode artifacts like `implementation_plan.md`, `task.md`, and `walkthrough.md`), learns which commands are noisy, and prunes stale data — all in the background via hooks or plugins.
77
+ For Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid, everything is automatic. Eagle Mem scans your codebase, indexes source files, captures session summaries, mirrors memories and tasks (including planning-mode artifacts like `implementation_plan.md`, `task.md`, and `walkthrough.md`), learns which commands are noisy, and prunes stale data — all in the background via hooks or plugins.
76
78
 
77
- For Google Antigravity, the installer copies the native Python integration to `~/.eagle-mem/integrations/google_antigravity_hook.py`. For OpenCode, it installs a global local plugin at `~/.config/opencode/plugins/eagle-mem.js` and symlinks skills into `~/.config/opencode/skills`. For Codex, the installer enables `hooks` in `~/.codex/config.toml`, registers hooks in `~/.codex/hooks.json`, symlinks Eagle Mem skills into `~/.codex/skills`, and patches `~/.codex/AGENTS.md`. For Claude Code, it integrates with `~/.claude/settings.json`, `CLAUDE.md`, and `~/.claude/skills`. Grok users get skill symlinks into `~/.grok/skills/` and can run `eagle-mem grok-bootstrap` for setup guidance and self-linking.
79
+ For Google Antigravity, the installer copies the native Python integration to `~/.eagle-mem/integrations/google_antigravity_hook.py`. For OpenCode, it installs a global local plugin at `~/.config/opencode/plugins/eagle-mem.js` and symlinks skills into `~/.config/opencode/skills`. For Codex, the installer enables `hooks` in `~/.codex/config.toml`, registers hooks in `~/.codex/hooks.json`, symlinks Eagle Mem skills into `~/.codex/skills`, and patches `~/.codex/AGENTS.md`. For Claude Code, it integrates with `~/.claude/settings.json`, `CLAUDE.md`, and `~/.claude/skills`. For Factory Droid, it registers the hook lifecycle into `~/.factory/settings.json`, preserving the `apiKey`, `customModels`, and `statusLine` already in that file. Grok users get skill symlinks into `~/.grok/skills/` and can run `eagle-mem grok-bootstrap` for setup guidance and self-linking.
78
80
 
79
81
  ### Prerequisites
80
82
 
81
83
  - `sqlite3` with FTS5 support (ships with macOS; Eagle Mem prefers known system/Homebrew SQLite binaries before PATH shims)
82
84
  - `jq` (the installer offers to install if missing)
83
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Codex, OpenCode, Google Antigravity SDK, or a Grok environment (`~/.grok/`)
85
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Codex, OpenCode, Google Antigravity SDK, Factory Droid (`~/.factory/`), or a Grok environment (`~/.grok/`)
84
86
 
85
87
  ## How It Works
86
88
 
@@ -114,7 +116,7 @@ Eagle Mem actively reduces token consumption:
114
116
 
115
117
  ### Compaction Survival
116
118
 
117
- One of the core promises of Eagle Mem is protecting against `/compact` and session amnesia. Compaction Survival is project-level: it reads shared Eagle Mem state such as durable tasks, enriched summaries, stale work, and orchestration lanes. Claude Code, Codex, OpenCode, and Google Antigravity get automatic hook-backed or plugin-backed recovery through session start, turn-end, and compaction events. Grok can inspect and use the same state through `eagle-mem compaction`, `eagle-mem tasks`, and the linked skills.
119
+ One of the core promises of Eagle Mem is protecting against `/compact` and session amnesia. Compaction Survival is project-level: it reads shared Eagle Mem state such as durable tasks, enriched summaries, stale work, and orchestration lanes. Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid get automatic hook-backed or plugin-backed recovery through session start, turn-end, and compaction events. Grok can inspect and use the same state through `eagle-mem compaction`, `eagle-mem tasks`, and the linked skills.
118
120
 
119
121
  Run `eagle-mem compaction` anytime to check readiness.
120
122
 
@@ -203,7 +205,7 @@ eagle-mem uninstall --dry-run
203
205
 
204
206
  Install and update print the files/configs they intend to touch before they change the runtime. The installed runtime writes `~/.eagle-mem/install-manifest.json` with file sizes, modes, and SHA-256 hashes, so `doctor` can tell whether hooks, scripts, libraries, and database helpers still match the package that installed them.
205
207
 
206
- Uninstall removes Claude Code, Codex, OpenCode, Grok, and Google Antigravity hook/plugin registrations, Eagle Mem instruction blocks, custom Claude statusline integration, and skill links. It backs up user config files before editing them and keeps `~/.eagle-mem/memory.db` unless you explicitly confirm data deletion.
208
+ Uninstall removes Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid hook/plugin registrations, Eagle Mem instruction blocks, custom Claude statusline integration, and skill links. It backs up user config files before editing them and keeps `~/.eagle-mem/memory.db` unless you explicitly confirm data deletion.
207
209
 
208
210
  ### Download Counts, Privacy, and Telemetry
209
211
 
@@ -290,12 +292,12 @@ Each lane is stored in `orchestration_lanes` and mirrored into `agent_tasks`, so
290
292
 
291
293
  Both agents write to `~/.eagle-mem/memory.db`:
292
294
 
293
- - `sessions.agent` records whether a session came from Claude Code, Codex, OpenCode, or Antigravity
295
+ - `sessions.agent` records whether a session came from Claude Code, Codex, OpenCode, Antigravity, or Factory Droid
294
296
  - `summaries.agent` records which agent produced the session summary
295
297
  - mirrored memories, plans, and tasks include `origin_agent`
296
- - SessionStart recall labels sources as `Claude Code`, `Codex`, `OpenCode`, or `Antigravity`
298
+ - SessionStart recall labels sources as `Claude Code`, `Codex`, `OpenCode`, `Antigravity`, or `Factory Droid`
297
299
 
298
- That means opening the same project in Claude Code, Codex, OpenCode, and Google Antigravity does not create isolated memory worlds. They recall the same project history while preserving the source of each memory. Grok can search, inspect, and update that same project memory through the linked skills and CLI commands.
300
+ That means opening the same project in Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid does not create isolated memory worlds. They recall the same project history while preserving the source of each memory. Grok can search, inspect, and update that same project memory through the linked skills and CLI commands.
299
301
 
300
302
  ## Skills (Inside Claude Code, Codex, OpenCode, and Grok)
301
303
 
@@ -25,6 +25,7 @@ The compatibility gate applies to these file groups:
25
25
  - `lib/hooks.sh`
26
26
  - `lib/codex-hooks.sh`
27
27
  - `lib/opencode-hooks.sh`
28
+ - `lib/factory-hooks.sh`
28
29
  - `integrations/opencode_eagle_mem_plugin.js`
29
30
  - `scripts/install.sh`
30
31
  - `scripts/update.sh`
@@ -0,0 +1,56 @@
1
+ # Factory Droid Compatibility
2
+
3
+ Last verified: 2026-06-20
4
+
5
+ ## Official Sources
6
+
7
+ - Factory documentation home: https://docs.factory.ai/
8
+ - Hooks guide (source): https://github.com/factory-ai/factory/blob/main/docs/cli/configuration/hooks-guide.mdx
9
+ - Hooks code-validation guide (source): https://github.com/factory-ai/factory/blob/main/docs/guides/hooks/code-validation.mdx
10
+ - Hooks auto-formatting guide (source): https://github.com/factory-ai/factory/blob/main/docs/guides/hooks/auto-formatting.mdx
11
+
12
+ ## Behavior Relied On
13
+
14
+ - Factory Droid hooks live in `~/.factory/settings.json` — the **same** file that holds `apiKey`, `customModels`, and `statusLine`. Factory does not read a separate `hooks.json`; registration that targets such a file is silently dead. Eagle Mem rewrites only the `.hooks` subtree of `settings.json` and preserves every secret-bearing sibling verbatim.
15
+ - A hook entry is exactly `{ "type": "command", "command": "<shell>", "timeout": <seconds> }`. `type` (always `"command"`) and `command` are required; `timeout` is an optional integer measured in seconds. The schema has **no `statusMessage` field** — emitting an unknown key into this `apiKey`-bearing file risks strict-parse breakage, so Eagle Mem never writes one.
16
+ - Events are grouped under `hooks.<Event>` as an array of matcher-groups, each `{ "matcher": "<pattern>", "hooks": [ {type,command,timeout}, ... ] }`. The matcher-group container mirrors Claude Code's settings.json shape.
17
+ - `matcher` is optional and **case-sensitive**, and applies only to `PreToolUse`, `PostToolUse`, and `PreCompact`. `SessionStart` carries no matcher.
18
+ - Factory tool names differ from Claude: `Create` is the file-write tool (Claude `Write`), `Execute` is the shell tool (Claude `Bash`), `Edit` edits files, and `Read` reads them. The `Execute` tool input is at `.tool_input.command`; `Create` and `Edit` expose the path at `.tool_input.file_path`. Eagle Mem's write-detection (`lib/hooks-posttool.sh`) therefore treats `Create` as a write alongside `Write`/`Edit`/`apply_patch`.
19
+ - `PreCompact` registers both the `manual` and `auto` matchers (mirroring Claude) so rich pre-compaction capture fires on either trigger.
20
+ - The Factory transcript is a third schema: a JSONL stream where every line is `type:"message"`, the role is at `.message.role`, and `.message.content` is **always an array** of blocks (`{type:"text",...}` or `{type:"tool_use", name, input}`). Eagle Mem's `or`-union jq selectors let one extraction body serve both Claude and Factory transcripts.
21
+ - Factory exposes `$DROID_PROJECT_DIR` to hook commands. Eagle Mem does not depend on a translation adapter: it sets `EAGLE_AGENT_SOURCE=factory` and invokes the shared hook scripts in `$EAGLE_MEM_DIR/hooks/` directly, so the Factory lane records with `agent = factory` provenance across observations and summaries.
22
+
23
+ ## Session Capture Behavior
24
+
25
+ - Factory replies stay prose-only; Eagle Mem never asks Factory to print summary blocks, XML, or hook payloads. The `Stop` hook captures a summary from the Factory transcript automatically (the third schema parses via the union selectors above).
26
+ - For a richer structured capture, Factory may run `eagle-mem session save --session-id <session_id> --agent factory ...` once at wrap-up. 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.
27
+ - Modified-file lists are derived from `Create`/`Edit` `.tool_input.file_path`; `Execute` activity is summarized from `.tool_input.command`.
28
+
29
+ ## Eagle Mem Files Depending On This
30
+
31
+ - `lib/factory-hooks.sh`
32
+ - `lib/common.sh`
33
+ - `lib/hooks-posttool.sh`
34
+ - `hooks/session-start.sh`
35
+ - `hooks/user-prompt-submit.sh`
36
+ - `hooks/pre-tool-use.sh`
37
+ - `hooks/post-tool-use.sh`
38
+ - `hooks/pre-compact.sh`
39
+ - `hooks/stop.sh`
40
+ - `scripts/install.sh`
41
+ - `scripts/update.sh`
42
+ - `scripts/uninstall.sh`
43
+ - `scripts/doctor.sh`
44
+
45
+ ## Fixtures And Tests
46
+
47
+ - `tests/fixtures/agent-hooks/factory-post-tool-use.json`
48
+ - `tests/fixtures/agent-hooks/factory-create.json`
49
+ - `tests/test_factory_hooks_config.sh`
50
+ - `tests/test_agent_compatibility_docs_gate.sh`
51
+
52
+ ## Reverification Notes
53
+
54
+ When editing Factory hooks or install/update behavior, update this file with the new verification date and source URLs. If a future Factory version changes the settings.json hook location, the `{type, command, timeout}` entry schema, matcher support, tool names (`Create`/`Execute`/`Edit`/`Read`), PreCompact matchers, or the transcript schema, update the fixtures first and then the implementation. Never write a `statusMessage` key into `settings.json`, and never register into a `hooks.json` file — Factory ignores it.
55
+
56
+ Registration deduplicates on `(matcher, command)` and updates in place when a command at the same `.eagle-mem/hooks/<script>.sh` path differs (e.g. a wrapper-prefix change). One consequence (shared with the Codex/OpenCode peers): if a future Eagle Mem release **changes an existing matcher string** — e.g. adds a tool name to `"Execute|Read|Edit|Create"` — the stored entry's old matcher matches neither the dedup nor the in-place-update query, so registration appends a second handler and `PostToolUse` would double-fire. When changing a Factory matcher string, strip the stale matcher-group first (or bump the removal path) so the installer cannot leave a duplicate. The transcript-line schema here is verified against the documented spec, not live Droid JSONL — when a future Factory version ships, diff a real `Stop` transcript against the union selectors before trusting capture.
@@ -163,7 +163,7 @@ esac
163
163
 
164
164
  # Only track relevant tools
165
165
  case "$tool_name" in
166
- Read|Write|Edit|apply_patch|TaskUpdate) ;;
166
+ Read|Write|Edit|Create|apply_patch|TaskUpdate) ;;
167
167
  *) eagle_is_shell_tool "$tool_name" || exit 0 ;;
168
168
  esac
169
169
 
@@ -198,6 +198,11 @@ case "$tool_name" in
198
198
  [ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
199
199
  tool_summary="Write $fp"
200
200
  ;;
201
+ Create)
202
+ fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
203
+ [ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
204
+ tool_summary="Create $fp"
205
+ ;;
201
206
  Edit)
202
207
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
203
208
  [ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
@@ -215,7 +220,7 @@ case "$tool_name" in
215
220
  tool_summary="apply_patch"
216
221
  fi
217
222
  ;;
218
- Bash|exec_command|shell_command|unified_exec)
223
+ Bash|exec_command|shell_command|unified_exec|Execute)
219
224
  # Redact BEFORE truncating: a secret split across the 200-char boundary
220
225
  # could otherwise survive because the redaction prefix patterns no longer
221
226
  # see the full token.
@@ -265,7 +270,7 @@ esac
265
270
 
266
271
  if [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
267
272
  case "$tool_name" in
268
- Edit|Write|apply_patch)
273
+ Edit|Write|Create|apply_patch)
269
274
  modified_paths=$(printf '%s' "$files_modified" | jq -r '.[]?' 2>/dev/null)
270
275
  [ -n "$modified_paths" ] || modified_paths="$fp"
271
276
  while IFS= read -r modified_path; do
@@ -280,7 +285,7 @@ fi
280
285
  # ─── Enforced anti-regression: mark affected features pending ──
281
286
 
282
287
  case "$tool_name" in
283
- Edit|Write|apply_patch)
288
+ Edit|Write|Create|apply_patch)
284
289
  if [ -n "$files_modified" ] && [ "$files_modified" != "[]" ]; then
285
290
  while IFS= read -r mod_file; do
286
291
  [ -z "$mod_file" ] && continue
@@ -91,13 +91,14 @@ if [ "$existing_source" = "agent" ]; then
91
91
  exit 0
92
92
  fi
93
93
 
94
- # Latest assistant text from the transcript (Claude format). The same surface the
95
- # Stop hook hands the enricher; enrich-summary.sh bounds it to a tail excerpt
96
- # before sending it to the provider.
94
+ # Latest assistant text from the transcript (Claude top-level `assistant` and
95
+ # Factory `message`/role=assistant share the `.message.content` array). The same
96
+ # surface the Stop hook hands the enricher; enrich-summary.sh bounds it to a tail
97
+ # excerpt before sending it to the provider.
97
98
  text_content=""
98
99
  if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
99
100
  text_content=$(jq -r -s '
100
- [.[] | select(.type == "assistant")]
101
+ [.[] | select(.type == "assistant" or (.type == "message" and .message.role == "assistant"))]
101
102
  | (.[-5:] // [])
102
103
  | [.[] | .message.content[]? | select(.type == "text") | .text]
103
104
  | join("\n")
@@ -26,7 +26,7 @@ IFS=$'\x1f' read -r tool_name session_id cwd <<< \
26
26
  agent=$(eagle_agent_source_from_json "$input")
27
27
 
28
28
  case "$tool_name" in
29
- Read|Edit|Write|apply_patch) ;;
29
+ Read|Edit|Write|Create|apply_patch) ;;
30
30
  *) eagle_is_shell_tool "$tool_name" || exit 0 ;;
31
31
  esac
32
32
 
@@ -39,7 +39,7 @@ context=""
39
39
  updated_input=""
40
40
 
41
41
  case "$tool_name" in
42
- Bash|exec_command|shell_command|unified_exec)
42
+ Bash|exec_command|shell_command|unified_exec|Execute)
43
43
  cmd=$(eagle_tool_command_from_json "$input")
44
44
  [ -z "$cmd" ] && exit 0
45
45
 
@@ -213,7 +213,7 @@ ${context}================"
213
213
  fi
214
214
  ;;
215
215
 
216
- Edit|Write|apply_patch)
216
+ Edit|Write|Create|apply_patch)
217
217
  target_files=$(echo "$input" | jq -r '.tool_input.file_path // empty')
218
218
  if [ "$tool_name" = "apply_patch" ]; then
219
219
  patch_cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
package/hooks/stop.sh CHANGED
@@ -68,7 +68,7 @@ eagle_clean_request_candidates() {
68
68
  }
69
69
 
70
70
  if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
71
- request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
71
+ request=$(jq -r 'select(.type == "user" or (.type == "message" and .message.role == "user")) | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
72
72
  | eagle_clean_request_candidates)
73
73
 
74
74
  if [ -z "$request" ]; then
@@ -87,8 +87,8 @@ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
87
87
  | eagle_clean_request_candidates)
88
88
  fi
89
89
 
90
- heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
91
- heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
90
+ heuristic_reads=$(jq -r 'select(.type == "assistant" or (.type == "message" and .message.role == "assistant")) | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
91
+ heuristic_writes=$(jq -r 'select(.type == "assistant" or (.type == "message" and .message.role == "assistant")) | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit" or .name == "Create") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
92
92
  fi
93
93
 
94
94
  files_read="[]"
@@ -118,7 +118,7 @@ eagle_log "INFO" "Stop: heuristic extraction complete"
118
118
 
119
119
  if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
120
120
  text_content=$(jq -r -s '
121
- [.[] | select(.type == "assistant")] | last |
121
+ [.[] | select(.type == "assistant" or (.type == "message" and .message.role == "assistant"))] | last |
122
122
  if . then
123
123
  [.message.content[]? | select(.type == "text") | .text] | join("\n")
124
124
  else "" end
package/lib/common.sh CHANGED
@@ -26,6 +26,8 @@ EAGLE_OPENCODE_CONFIG="${EAGLE_OPENCODE_CONFIG:-$EAGLE_OPENCODE_DIR/opencode.jso
26
26
  EAGLE_OPENCODE_PLUGINS_DIR="${EAGLE_OPENCODE_PLUGINS_DIR:-$EAGLE_OPENCODE_DIR/plugins}"
27
27
  EAGLE_OPENCODE_PLUGIN="${EAGLE_OPENCODE_PLUGIN:-$EAGLE_OPENCODE_PLUGINS_DIR/eagle-mem.js}"
28
28
  EAGLE_OPENCODE_SKILLS_DIR="${EAGLE_OPENCODE_SKILLS_DIR:-$EAGLE_OPENCODE_DIR/skills}"
29
+ EAGLE_FACTORY_DIR="${EAGLE_FACTORY_DIR:-$HOME/.factory}"
30
+ EAGLE_FACTORY_SETTINGS="${EAGLE_FACTORY_SETTINGS:-$EAGLE_FACTORY_DIR/settings.json}"
29
31
  EAGLE_RAW_BASH_UNLOCK="${EAGLE_RAW_BASH_UNLOCK:-/tmp/eagle-mem-raw-bash-unlock}"
30
32
 
31
33
  _eagle_sqlite_candidate_paths() {
@@ -935,6 +937,7 @@ eagle_agent_source() {
935
937
  codex|openai-codex) echo "codex" ;;
936
938
  claude|claude-code|cloud-code) echo "claude-code" ;;
937
939
  antigravity*|google-antigravity*|google_antigravity*) echo "antigravity" ;;
940
+ factory|factory-droid|droid) echo "factory" ;;
938
941
  *)
939
942
  if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ -n "${CODEX_MANAGED_BY_NPM:-}" ]; then
940
943
  echo "codex"
@@ -962,10 +965,12 @@ eagle_agent_source_from_json() {
962
965
  case "$agent_field" in
963
966
  antigravity*) echo "antigravity"; return ;;
964
967
  opencode*) echo "opencode"; return ;;
968
+ factory*|droid*) echo "factory"; return ;;
965
969
  esac
966
970
 
967
971
  case "$transcript_path" in
968
972
  "$HOME/.codex/"*|*/.codex/*) echo "codex"; return ;;
973
+ "$EAGLE_FACTORY_DIR/"*|*/.factory/*) echo "factory"; return ;;
969
974
  "$HOME/.claude/"*|*/.claude/*) echo "claude-code"; return ;;
970
975
  esac
971
976
  [ -n "$turn_id" ] && { echo "codex"; return; }
@@ -979,6 +984,7 @@ eagle_agent_label() {
979
984
  codex) echo "Codex" ;;
980
985
  antigravity*) echo "Antigravity" ;;
981
986
  opencode) echo "OpenCode" ;;
987
+ factory) echo "Factory Droid" ;;
982
988
  *) echo "Claude Code" ;;
983
989
  esac
984
990
  }
@@ -1001,7 +1007,7 @@ eagle_trim_text() {
1001
1007
 
1002
1008
  eagle_is_shell_tool() {
1003
1009
  case "${1:-}" in
1004
- Bash|exec_command|shell_command|unified_exec) return 0 ;;
1010
+ Bash|exec_command|shell_command|unified_exec|Execute) return 0 ;;
1005
1011
  *) return 1 ;;
1006
1012
  esac
1007
1013
  }
@@ -1674,6 +1680,7 @@ eagle_runtime_change_plan() {
1674
1680
  local claude_found="${3:-false}"
1675
1681
  local codex_found="${4:-false}"
1676
1682
  local opencode_found="${5:-false}"
1683
+ local factory_found="${6:-false}"
1677
1684
 
1678
1685
  echo ""
1679
1686
  echo -e " ${BOLD:-}What will change${RESET:-}"
@@ -1716,6 +1723,15 @@ eagle_runtime_change_plan() {
1716
1723
  echo -e " ${DIM:-}-> OpenCode not detected; OpenCode plugin/skills skipped${RESET:-}"
1717
1724
  fi
1718
1725
 
1726
+ if [ "$factory_found" = true ]; then
1727
+ echo -e " ${CYAN:-}->${RESET:-} Update Factory Droid"
1728
+ echo -e " ${DIM:-}settings:${RESET:-} $EAGLE_FACTORY_SETTINGS"
1729
+ echo -e " ${DIM:-}hooks: ${RESET:-} SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, PreCompact"
1730
+ echo -e " ${DIM:-}mode: ${RESET:-} merge Eagle Mem hooks into settings.json; backup + 600 perms first"
1731
+ else
1732
+ echo -e " ${DIM:-}-> Factory Droid not detected; Factory hooks skipped${RESET:-}"
1733
+ fi
1734
+
1719
1735
  if [ "$action" = "update" ]; then
1720
1736
  echo -e " ${CYAN:-}->${RESET:-} Refresh installed version metadata"
1721
1737
  fi
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Factory Droid hook registration helpers
4
+ # Shared by install.sh, update.sh, and uninstall.sh
5
+ # ═══════════════════════════════════════════════════════════
6
+ # Factory Droid reads hooks from ~/.factory/settings.json (the SAME file that
7
+ # holds customModels, apiKey, statusLine). There is no separate hooks.json —
8
+ # Factory never reads one. The jq patch below rewrites only the .hooks subtree,
9
+ # leaving every sibling key (incl. secrets) untouched, and only commits the
10
+ # result when jq succeeds, so a malformed settings.json is never clobbered.
11
+ # ═══════════════════════════════════════════════════════════
12
+ [ -n "${_EAGLE_FACTORY_HOOKS_LOADED:-}" ] && return 0
13
+ _EAGLE_FACTORY_HOOKS_LOADED=1
14
+
15
+ eagle_factory_backup_file() {
16
+ local file="$1"
17
+ [ -f "$file" ] || return 0
18
+ if [ -z "${_EAGLE_FACTORY_BACKUP_DONE:-}" ]; then
19
+ local timestamp
20
+ timestamp=$(date +%Y%m%d%H%M%S)
21
+ cp "$file" "${file}.bak.${timestamp}" 2>/dev/null || true
22
+ _EAGLE_FACTORY_BACKUP_DONE=1
23
+ fi
24
+ }
25
+
26
+ eagle_patch_factory_hook() {
27
+ local hooks_file="$1"
28
+ local event="$2"
29
+ local matcher="$3"
30
+ local command="$4"
31
+ local description="${5:-}"
32
+ local timeout="${6:-}"
33
+ local script_path="$command"
34
+ script_path="${script_path#EAGLE_AGENT_SOURCE=factory bash \"}"
35
+ script_path="${script_path#bash \"}"
36
+ script_path="${script_path%\"}"
37
+
38
+ mkdir -p "$(dirname "$hooks_file")"
39
+ eagle_factory_backup_file "$hooks_file"
40
+ if [ ! -f "$hooks_file" ]; then
41
+ printf '{"hooks":{}}\n' > "$hooks_file"
42
+ chmod 600 "$hooks_file" 2>/dev/null || true
43
+ fi
44
+
45
+ local match_query
46
+ if [ -n "$matcher" ]; then
47
+ match_query='.hooks[$event][]? | select(.matcher == $matcher and (.hooks[]?.command == $command))'
48
+ else
49
+ match_query='.hooks[$event][]? | select((.matcher == null or .matcher == "") and (.hooks[]?.command == $command))'
50
+ fi
51
+ if jq -e --arg event "$event" --arg matcher "$matcher" --arg command "$command" "$match_query" "$hooks_file" &>/dev/null; then
52
+ [ -n "$description" ] && eagle_ok "$description ${DIM}(already registered)${RESET}"
53
+ return 0
54
+ fi
55
+
56
+ if jq -e --arg event "$event" --arg matcher "$matcher" --arg script "$script_path" '
57
+ .hooks[$event][]?
58
+ | select((($matcher == "" and (.matcher == null or .matcher == "")) or .matcher == $matcher)
59
+ and any(.hooks[]?; (.command // "") | contains($script)))
60
+ ' "$hooks_file" &>/dev/null; then
61
+ local tmp_existing
62
+ tmp_existing=$(mktemp)
63
+ jq --arg event "$event" --arg matcher "$matcher" --arg script "$script_path" --arg command "$command" '
64
+ .hooks[$event] |= map(
65
+ if ((($matcher == "" and (.matcher == null or .matcher == "")) or .matcher == $matcher)
66
+ and any(.hooks[]?; (.command // "") | contains($script)))
67
+ then .hooks |= map(if ((.command // "") | contains($script)) then .command = $command else . end)
68
+ else .
69
+ end
70
+ )
71
+ ' "$hooks_file" > "$tmp_existing" && mv "$tmp_existing" "$hooks_file"
72
+ [ -n "$description" ] && eagle_ok "$description ${DIM}(updated)${RESET}"
73
+ return 0
74
+ fi
75
+
76
+ local entry
77
+ # Factory's hook command schema is {type, command, timeout?} only — no
78
+ # statusMessage field (confirmed against factory-ai/factory hooks-reference).
79
+ # An unknown key here lands in the same settings.json as model config + apiKey
80
+ # and could break strict parsing, so we emit strictly the documented shape.
81
+ entry=$(jq -nc \
82
+ --arg m "$matcher" \
83
+ --arg c "$command" \
84
+ --arg timeout "$timeout" '
85
+ {
86
+ hooks: [
87
+ {
88
+ type: "command",
89
+ command: $c
90
+ }
91
+ + (if $timeout == "" then {} else {timeout: ($timeout | tonumber)} end)
92
+ ]
93
+ }
94
+ + (if $m == "" then {} else {matcher: $m} end)')
95
+
96
+ local tmp
97
+ tmp=$(mktemp)
98
+ # Pass $event as data via --arg and index dynamically so the event name is
99
+ # never interpolated into the jq program string (injection-safe even if the
100
+ # caller ever passes an untrusted event name). Sibling top-level keys
101
+ # (customModels, apiKey, statusLine) are preserved by this targeted assign.
102
+ jq --argjson entry "$entry" --arg event "$event" \
103
+ '.hooks[$event] = ((.hooks[$event] // []) + [$entry])' "$hooks_file" > "$tmp" && mv "$tmp" "$hooks_file"
104
+ chmod 600 "$hooks_file" 2>/dev/null || true
105
+ [ -n "$description" ] && eagle_ok "$description"
106
+ }
107
+
108
+ eagle_register_factory_hooks() {
109
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "SessionStart" "" \
110
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\"" \
111
+ "Factory SessionStart hook" \
112
+ "30"
113
+
114
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "UserPromptSubmit" "" \
115
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh\"" \
116
+ "Factory UserPromptSubmit hook" \
117
+ "30"
118
+
119
+ # Matchers use Factory's tool names: Execute (shell), Create (new file),
120
+ # Edit, Read. The hooks recognize these natively (see eagle_is_shell_tool
121
+ # and the Create/Execute cases in pre/post-tool-use.sh).
122
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PreToolUse" "Execute|Read|Edit|Create" \
123
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh\"" \
124
+ "Factory PreToolUse hook" \
125
+ "30"
126
+
127
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PostToolUse" "Read|Create|Edit|Execute" \
128
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/post-tool-use.sh\"" \
129
+ "Factory PostToolUse hook" \
130
+ "30"
131
+
132
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "Stop" "" \
133
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" \
134
+ "Factory Stop hook" \
135
+ "60"
136
+
137
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "SessionEnd" "" \
138
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/session-end.sh\"" \
139
+ "Factory SessionEnd hook" \
140
+ "30"
141
+
142
+ # PreCompact's matcher is the trigger itself ("manual" | "auto"); register
143
+ # both so auto-compaction — the gap users actually hit — also captures before
144
+ # the context window collapses. The hook reads `reason`/`trigger` from stdin.
145
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PreCompact" "manual" \
146
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/pre-compact.sh\"" \
147
+ "Factory PreCompact hook (manual)" \
148
+ "30"
149
+
150
+ eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PreCompact" "auto" \
151
+ "EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/pre-compact.sh\"" \
152
+ "Factory PreCompact hook (auto)" \
153
+ "30"
154
+ }
155
+
156
+ eagle_remove_factory_hooks() {
157
+ local hooks_file="$EAGLE_FACTORY_SETTINGS"
158
+ [ -f "$hooks_file" ] || return 1
159
+ command -v jq &>/dev/null || return 1
160
+
161
+ eagle_factory_backup_file "$hooks_file"
162
+
163
+ local tmp
164
+ tmp=$(mktemp)
165
+ jq '
166
+ def without_eagle_mem_handlers:
167
+ .hooks = ((.hooks // [])
168
+ | map(select(((.command // "") | contains(".eagle-mem/hooks/")) | not)));
169
+
170
+ if .hooks then
171
+ .hooks |= with_entries(
172
+ .value = [
173
+ .value[]?
174
+ | without_eagle_mem_handlers
175
+ | select((.hooks // []) | length > 0)
176
+ ]
177
+ | select(.value != [])
178
+ )
179
+ else . end
180
+ | if .hooks == {} then del(.hooks) else . end
181
+ ' "$hooks_file" > "$tmp" && mv "$tmp" "$hooks_file"
182
+ chmod 600 "$hooks_file" 2>/dev/null || true
183
+ }
@@ -11,7 +11,7 @@ eagle_posttool_mirror_writes() {
11
11
  local agent="${5:-$(eagle_agent_source)}"
12
12
 
13
13
  case "$tool_name" in
14
- Write|Edit|apply_patch)
14
+ Write|Edit|apply_patch|Create)
15
15
  if [ -n "$fp" ]; then
16
16
  case "$fp" in
17
17
  *..*) ;; # path traversal — skip
@@ -48,7 +48,7 @@ eagle_posttool_stale_hint() {
48
48
  local agent="${4:-$(eagle_agent_source)}"
49
49
 
50
50
  case "$tool_name" in
51
- Write|Edit|apply_patch)
51
+ Write|Edit|apply_patch|Create)
52
52
  if [ -n "$fp" ]; then
53
53
  local fname fname_stem
54
54
  fname=$(basename "$fp")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.15.1",
4
- "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, and Google Antigravity",
3
+ "version": "4.16.0",
4
+ "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
7
7
  },
@@ -29,6 +29,7 @@
29
29
  "codex",
30
30
  "opencode",
31
31
  "grok",
32
+ "factory-droid",
32
33
  "memory",
33
34
  "sqlite",
34
35
  "fts5",
package/scripts/doctor.sh CHANGED
@@ -14,6 +14,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
14
14
  . "$SCRIPTS_DIR/style.sh"
15
15
  . "$LIB_DIR/common.sh"
16
16
  . "$LIB_DIR/opencode-hooks.sh"
17
+ . "$LIB_DIR/factory-hooks.sh"
17
18
 
18
19
  mode="install-footprint"
19
20
  json_output=false
@@ -140,6 +141,15 @@ if [ -f "$EAGLE_CODEX_HOOKS" ] && command -v jq >/dev/null 2>&1; then
140
141
  fi
141
142
  fi
142
143
 
144
+ factory_hooks="not found"
145
+ if [ -f "$EAGLE_FACTORY_SETTINGS" ] && command -v jq >/dev/null 2>&1; then
146
+ if jq -e '.hooks // {} | .. | objects | .command? // empty | select(test("eagle-mem|\\.eagle-mem"))' "$EAGLE_FACTORY_SETTINGS" >/dev/null 2>&1; then
147
+ factory_hooks="registered"
148
+ else
149
+ factory_hooks="not registered"
150
+ fi
151
+ fi
152
+
143
153
  statusline_state="not configured"
144
154
  statusline_command=""
145
155
  if [ -f "$EAGLE_SETTINGS" ] && command -v jq >/dev/null 2>&1; then
@@ -184,6 +194,7 @@ if [ "$json_output" = true ]; then
184
194
  --arg claude_hooks "$claude_hooks" \
185
195
  --arg codex_hooks "$codex_hooks" \
186
196
  --arg opencode_plugin "$opencode_plugin" \
197
+ --arg factory_hooks "$factory_hooks" \
187
198
  --arg statusline "$statusline_state" \
188
199
  --arg hooks_cmp "$hooks_cmp" \
189
200
  --arg lib_cmp "$lib_cmp" \
@@ -202,7 +213,7 @@ if [ "$json_output" = true ]; then
202
213
  versions:{package:$package_version, installed:$installed_version},
203
214
  sqlite:{path:$sqlite_bin, version:$sqlite_version, fts5:$sqlite_fts5},
204
215
  database:{exists:$db_exists, integrity:{status:$db_integrity_status, detail:$db_integrity_detail}},
205
- hooks:{claude:$claude_hooks, codex:$codex_hooks, opencode:$opencode_plugin, statusline:$statusline},
216
+ hooks:{claude:$claude_hooks, codex:$codex_hooks, opencode:$opencode_plugin, factory:$factory_hooks, statusline:$statusline},
206
217
  runtime_drift:{hooks:$hooks_cmp, lib:$lib_cmp, db:$db_cmp, scripts:$scripts_cmp},
207
218
  manifest:{path:$manifest_path, status:$manifest_status, checked:$manifest_checked,
208
219
  missing:$manifest_missing, drift:$manifest_drift, version:$manifest_version,
@@ -249,6 +260,7 @@ echo -e " ${BOLD}Hooks / Skills${RESET}"
249
260
  eagle_kv "Claude Code:" "$claude_hooks"
250
261
  eagle_kv "Codex:" "$codex_hooks"
251
262
  eagle_kv "OpenCode:" "$opencode_plugin"
263
+ eagle_kv "Factory Droid:" "$factory_hooks"
252
264
  [ -d "$EAGLE_GROK_DIR" ] && eagle_kv "Grok skills:" "$EAGLE_GROK_SKILLS_DIR"
253
265
  eagle_kv "Statusline:" "$statusline_state"
254
266
  echo ""
@@ -51,6 +51,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
51
51
  . "$LIB_DIR/hooks.sh"
52
52
  . "$LIB_DIR/codex-hooks.sh"
53
53
  . "$LIB_DIR/opencode-hooks.sh"
54
+ . "$LIB_DIR/factory-hooks.sh"
54
55
 
55
56
  SETTINGS="$EAGLE_SETTINGS"
56
57
 
@@ -186,6 +187,7 @@ claude_found=false
186
187
  codex_found=false
187
188
  grok_found=false
188
189
  opencode_found=false
190
+ factory_found=false
189
191
 
190
192
  if [ -d "$HOME/.claude" ]; then
191
193
  eagle_ok "Claude Code ${DIM}(~/.claude/)${RESET}"
@@ -218,10 +220,17 @@ else
218
220
  eagle_warn "OpenCode not found ${DIM}(~/.config/opencode/ missing and opencode not on PATH)${RESET}"
219
221
  fi
220
222
 
221
- if [ "$claude_found" = false ] && [ "$codex_found" = false ] && [ "$grok_found" = false ] && [ "$opencode_found" = false ]; then
223
+ if [ -d "$EAGLE_FACTORY_DIR" ] || command -v droid &>/dev/null; then
224
+ factory_found=true
225
+ eagle_ok "Factory Droid ${DIM}($EAGLE_FACTORY_DIR/)${RESET}"
226
+ else
227
+ eagle_warn "Factory Droid not found ${DIM}(~/.factory/ missing and droid not on PATH)${RESET}"
228
+ fi
229
+
230
+ if [ "$claude_found" = false ] && [ "$codex_found" = false ] && [ "$grok_found" = false ] && [ "$opencode_found" = false ] && [ "$factory_found" = false ]; then
222
231
  eagle_fail "No supported agent install found"
223
232
  echo ""
224
- eagle_dim "Install Claude Code, Codex, OpenCode, or ensure ~/.grok/ exists, then re-run."
233
+ eagle_dim "Install Claude Code, Codex, OpenCode, Factory Droid, or ensure ~/.grok/ exists, then re-run."
225
234
  echo ""
226
235
  exit 1
227
236
  fi
@@ -232,7 +241,7 @@ if [ "$prereqs_ok" = false ]; then
232
241
  exit 1
233
242
  fi
234
243
 
235
- eagle_runtime_change_plan "install" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found"
244
+ eagle_runtime_change_plan "install" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found" "$factory_found"
236
245
 
237
246
  echo ""
238
247
 
@@ -307,6 +316,18 @@ else
307
316
  eagle_info "OpenCode plugin skipped ${DIM}(OpenCode not detected)${RESET}"
308
317
  fi
309
318
 
319
+ if [ "$factory_found" = true ]; then
320
+ if ! command -v jq &>/dev/null; then
321
+ eagle_warn "Factory hooks skipped ${DIM}(jq not found)${RESET}"
322
+ elif [ "$DRY_RUN" -eq 1 ]; then
323
+ eagle_info "Would register Factory Droid hooks"
324
+ else
325
+ eagle_register_factory_hooks
326
+ fi
327
+ else
328
+ eagle_info "Factory hooks skipped ${DIM}(Factory Droid not detected)${RESET}"
329
+ fi
330
+
310
331
  # ─── Install skills ────────────────────────────────────────
311
332
 
312
333
  if [ "$claude_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
@@ -498,6 +519,7 @@ eagle_kv "Hooks:" "$EAGLE_MEM_DIR/hooks/"
498
519
  [ "$codex_found" = true ] && eagle_kv "Codex hooks:" "$EAGLE_CODEX_HOOKS"
499
520
  [ "$grok_found" = true ] && eagle_kv "Grok skills:" "$EAGLE_GROK_SKILLS_DIR"
500
521
  [ "$opencode_found" = true ] && eagle_kv "OpenCode plugin:" "$EAGLE_OPENCODE_PLUGIN"
522
+ [ "$factory_found" = true ] && eagle_kv "Factory settings:" "$EAGLE_FACTORY_SETTINGS"
501
523
  eagle_kv "Antigravity Hook:" "$EAGLE_MEM_DIR/integrations/google_antigravity_hook.py"
502
524
 
503
525
  echo ""
@@ -510,6 +532,9 @@ fi
510
532
  if [ "$claude_found" = true ] || [ "$codex_found" = true ]; then
511
533
  eagle_dim "Start a new Claude Code or Codex session — Eagle Mem will activate automatically."
512
534
  fi
535
+ if [ "$factory_found" = true ]; then
536
+ eagle_dim "Start a new Factory Droid session — Eagle Mem hooks are registered in settings.json."
537
+ fi
513
538
  echo ""
514
539
  eagle_info "Google Antigravity SDK Integration:"
515
540
  eagle_dim " To use Eagle Mem inside your Python Antigravity agents, simply import and register the hook:"
package/scripts/test.sh CHANGED
@@ -77,6 +77,7 @@ run_check "Claude Stop Hook Registration (bash wrapper)" "bash \"$SCRIPTS_DIR/..
77
77
  run_check "Codex Hooks Config (canonical features.hooks flag)" "bash \"$SCRIPTS_DIR/../tests/test_codex_hooks_config.sh\""
78
78
  run_check "OpenCode Hooks Config (global plugin and skill registration)" "bash \"$SCRIPTS_DIR/../tests/test_opencode_hooks_config.sh\""
79
79
  run_check "OpenCode Plugin Adapter (hook normalization)" "bash \"$SCRIPTS_DIR/../tests/test_opencode_plugin_adapter.sh\""
80
+ run_check "Factory Hooks Config (settings.json merge preserves secrets, no statusMessage, idempotent, clean removal)" "bash \"$SCRIPTS_DIR/../tests/test_factory_hooks_config.sh\""
80
81
  run_check "Rust Migration Plan (compatibility-first contract)" "bash \"$SCRIPTS_DIR/../tests/test_rust_migration_plan.sh\""
81
82
  run_check "Database Repair (safe preview and recovery failure path)" "bash \"$SCRIPTS_DIR/../tests/test_repair.sh\""
82
83
  run_check "Trust Surfaces (DB integrity, JSON errors, statusline)" "bash \"$SCRIPTS_DIR/../tests/test_trust_surfaces.sh\""
@@ -12,6 +12,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
12
12
  . "$LIB_DIR/common.sh"
13
13
  . "$LIB_DIR/codex-hooks.sh"
14
14
  . "$LIB_DIR/opencode-hooks.sh"
15
+ . "$LIB_DIR/factory-hooks.sh"
15
16
 
16
17
  SETTINGS="$EAGLE_SETTINGS"
17
18
  dry_run=false
@@ -73,6 +74,17 @@ else
73
74
  eagle_info "OpenCode plugin not present or not Eagle Mem-owned"
74
75
  fi
75
76
 
77
+ factory_settings_backup=""
78
+ if [ -f "$EAGLE_FACTORY_SETTINGS" ]; then
79
+ factory_settings_backup=$(eagle_backup_user_file "$EAGLE_FACTORY_SETTINGS" 2>/dev/null || true)
80
+ fi
81
+ if eagle_remove_factory_hooks; then
82
+ eagle_ok "Hooks removed from Factory settings.json"
83
+ [ -n "$factory_settings_backup" ] && eagle_dim " Backup: $factory_settings_backup"
84
+ else
85
+ eagle_info "Factory settings.json not present or jq unavailable"
86
+ fi
87
+
76
88
  # ─── Remove instruction blocks and statusline integration ───
77
89
 
78
90
  claude_md="$HOME/.claude/CLAUDE.md"
package/scripts/update.sh CHANGED
@@ -17,12 +17,14 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
17
17
  . "$LIB_DIR/hooks.sh"
18
18
  . "$LIB_DIR/codex-hooks.sh"
19
19
  . "$LIB_DIR/opencode-hooks.sh"
20
+ . "$LIB_DIR/factory-hooks.sh"
20
21
 
21
22
  SETTINGS="$EAGLE_SETTINGS"
22
23
  claude_found=false
23
24
  codex_found=false
24
25
  grok_found=false
25
26
  opencode_found=false
27
+ factory_found=false
26
28
 
27
29
  eagle_header "Update"
28
30
 
@@ -48,8 +50,11 @@ fi
48
50
  if eagle_opencode_detected; then
49
51
  opencode_found=true
50
52
  fi
53
+ if [ -d "$EAGLE_FACTORY_DIR" ] || command -v droid &>/dev/null; then
54
+ factory_found=true
55
+ fi
51
56
 
52
- eagle_runtime_change_plan "update" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found"
57
+ eagle_runtime_change_plan "update" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found" "$factory_found"
53
58
 
54
59
  # ─── Update files ──────────────────────────────────────────
55
60
 
@@ -105,6 +110,15 @@ else
105
110
  eagle_info "OpenCode plugin skipped ${DIM}(OpenCode not detected)${RESET}"
106
111
  fi
107
112
 
113
+ if [ "$factory_found" = true ] && command -v jq &>/dev/null; then
114
+ eagle_register_factory_hooks
115
+ eagle_ok "Factory Droid hooks registered"
116
+ elif [ "$factory_found" = true ]; then
117
+ eagle_warn "Factory hooks skipped ${DIM}(jq not found)${RESET}"
118
+ else
119
+ eagle_info "Factory hooks skipped ${DIM}(Factory Droid not detected)${RESET}"
120
+ fi
121
+
108
122
  # ─── Update skill symlinks ────────────────────────────────
109
123
 
110
124
  if [ "$claude_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
@@ -0,0 +1,10 @@
1
+ {
2
+ "session_id": "factory-fixture-session",
3
+ "cwd": "/repo",
4
+ "hook_event_name": "PostToolUse",
5
+ "tool_name": "Create",
6
+ "tool_input": {
7
+ "file_path": "/repo/src/widget.ts"
8
+ },
9
+ "tool_response": {}
10
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "session_id": "factory-fixture-session",
3
+ "cwd": "/repo",
4
+ "hook_event_name": "PostToolUse",
5
+ "tool_name": "Execute",
6
+ "tool_input": {
7
+ "command": "git status --short"
8
+ },
9
+ "tool_response": {
10
+ "stdout": " M src/widget.ts\n"
11
+ }
12
+ }
@@ -35,6 +35,7 @@ require_file "$DOC_DIR/README.md"
35
35
  require_file "$DOC_DIR/claude-code.md"
36
36
  require_file "$DOC_DIR/codex.md"
37
37
  require_file "$DOC_DIR/opencode.md"
38
+ require_file "$DOC_DIR/factory.md"
38
39
  require_file "$FIXTURE_DIR/claude-user-prompt-submit.json"
39
40
  require_file "$FIXTURE_DIR/claude-statusline.json"
40
41
  require_file "$FIXTURE_DIR/codex-user-prompt-submit.json"
@@ -44,6 +45,8 @@ require_file "$FIXTURE_DIR/opencode-tool-execute-before.json"
44
45
  require_file "$FIXTURE_DIR/opencode-tool-execute-after.json"
45
46
  require_file "$FIXTURE_DIR/opencode-todo-updated.json"
46
47
  require_file "$FIXTURE_DIR/opencode-session-compacting.json"
48
+ require_file "$FIXTURE_DIR/factory-post-tool-use.json"
49
+ require_file "$FIXTURE_DIR/factory-create.json"
47
50
 
48
51
  require_json "$FIXTURE_DIR/claude-user-prompt-submit.json"
49
52
  require_json "$FIXTURE_DIR/claude-statusline.json"
@@ -54,6 +57,8 @@ require_json "$FIXTURE_DIR/opencode-tool-execute-before.json"
54
57
  require_json "$FIXTURE_DIR/opencode-tool-execute-after.json"
55
58
  require_json "$FIXTURE_DIR/opencode-todo-updated.json"
56
59
  require_json "$FIXTURE_DIR/opencode-session-compacting.json"
60
+ require_json "$FIXTURE_DIR/factory-post-tool-use.json"
61
+ require_json "$FIXTURE_DIR/factory-create.json"
57
62
 
58
63
  require_contains "$DOC_DIR/README.md" "Before modifying" "the required pre-edit workflow"
59
64
  require_contains "$DOC_DIR/README.md" "official documentation" "official documentation"
@@ -91,6 +96,18 @@ require_contains "$DOC_DIR/opencode.md" "opencode --pure" "OpenCode pure-mode ca
91
96
  require_contains "$DOC_DIR/opencode.md" "tests/fixtures/agent-hooks/opencode-tool-execute-before\\.json" "OpenCode PreToolUse fixture"
92
97
  require_contains "$DOC_DIR/opencode.md" "tests/test_opencode_plugin_adapter\\.sh" "OpenCode adapter regression test"
93
98
 
99
+ require_contains "$DOC_DIR/factory.md" "Last verified: [0-9]{4}-[0-9]{2}-[0-9]{2}" "a Factory Last verified date"
100
+ require_contains "$DOC_DIR/factory.md" "https://docs\\.factory\\.ai/" "Factory documentation source"
101
+ require_contains "$DOC_DIR/factory.md" "github\\.com/factory-ai/factory" "Factory hooks guide source"
102
+ require_contains "$DOC_DIR/factory.md" "~/.factory/settings\\.json" "Factory hooks live in settings.json"
103
+ require_contains "$DOC_DIR/factory.md" "apiKey" "Factory settings.json secret co-location"
104
+ require_contains "$DOC_DIR/factory.md" "no .?statusMessage" "Factory schema has no statusMessage field"
105
+ require_contains "$DOC_DIR/factory.md" "Execute" "Factory Execute tool name"
106
+ require_contains "$DOC_DIR/factory.md" "Create" "Factory Create tool name"
107
+ require_contains "$DOC_DIR/factory.md" "PreCompact" "Factory PreCompact matchers"
108
+ require_contains "$DOC_DIR/factory.md" "tests/fixtures/agent-hooks/factory-post-tool-use\\.json" "Factory PostToolUse fixture"
109
+ require_contains "$DOC_DIR/factory.md" "tests/test_factory_hooks_config\\.sh" "Factory hooks config regression test"
110
+
94
111
  if grep -R "features\\.codex_hooks" "$ROOT_DIR/lib" "$ROOT_DIR/scripts" "$ROOT_DIR/hooks" "$ROOT_DIR/bin" >/dev/null 2>&1; then
95
112
  fail "implementation still uses deprecated Codex features.codex_hooks instead of features.hooks"
96
113
  fi
@@ -104,7 +121,7 @@ changed_sensitive=$(
104
121
  git -C "$ROOT_DIR" diff --name-only HEAD -- 2>/dev/null || true
105
122
  git -C "$ROOT_DIR" ls-files --others --exclude-standard 2>/dev/null || true
106
123
  } | awk '
107
- /^(hooks\/.*\.sh|lib\/hooks\.sh|lib\/codex-hooks\.sh|lib\/opencode-hooks\.sh|integrations\/opencode_eagle_mem_plugin\.js|scripts\/install\.sh|scripts\/update\.sh|scripts\/uninstall\.sh|scripts\/doctor\.sh|scripts\/statusline-em\.sh|lib\/common\.sh|AGENTS\.md|CLAUDE\.md)$/ { print }
124
+ /^(hooks\/.*\.sh|lib\/hooks\.sh|lib\/codex-hooks\.sh|lib\/opencode-hooks\.sh|lib\/factory-hooks\.sh|integrations\/opencode_eagle_mem_plugin\.js|scripts\/install\.sh|scripts\/update\.sh|scripts\/uninstall\.sh|scripts\/doctor\.sh|scripts\/statusline-em\.sh|lib\/common\.sh|AGENTS\.md|CLAUDE\.md)$/ { print }
108
125
  ' | sort -u
109
126
  )
110
127
 
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # Factory Droid hooks live in ~/.factory/settings.json — the SAME file that
3
+ # holds apiKey, customModels, and statusLine. Registration must rewrite ONLY
4
+ # the .hooks subtree (preserving every secret-bearing sibling), emit strictly
5
+ # the documented {type, command, timeout} shape (no statusMessage), stay
6
+ # idempotent, and remove cleanly. This guards all of those invariants.
7
+ set -euo pipefail
8
+
9
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+
11
+ command -v jq >/dev/null 2>&1 || { echo "factory hooks config: jq not found, skipping"; exit 0; }
12
+
13
+ tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-factory-hooks-config.XXXXXX")
14
+ trap 'rm -rf "$tmp_dir"' EXIT
15
+
16
+ # EAGLE_MEM_DIR mirrors the production layout ($HOME/.eagle-mem) so the command
17
+ # paths contain the literal ".eagle-mem/hooks/" substring that
18
+ # eagle_remove_factory_hooks matches on — otherwise the removal assertion below
19
+ # would pass vacuously against a path the real uninstaller would never strip.
20
+ export HOME="$tmp_dir/home"
21
+ export EAGLE_MEM_DIR="$HOME/.eagle-mem"
22
+ export EAGLE_FACTORY_DIR="$HOME/.factory"
23
+ export EAGLE_FACTORY_SETTINGS="$EAGLE_FACTORY_DIR/settings.json"
24
+
25
+ mkdir -p "$HOME" "$EAGLE_MEM_DIR" "$EAGLE_FACTORY_DIR"
26
+
27
+ . "$ROOT_DIR/scripts/style.sh"
28
+ . "$ROOT_DIR/lib/common.sh"
29
+ . "$ROOT_DIR/lib/factory-hooks.sh"
30
+
31
+ fail() {
32
+ echo "factory hooks config regression failed: $*" >&2
33
+ [ -f "$EAGLE_FACTORY_SETTINGS" ] && jq . "$EAGLE_FACTORY_SETTINGS" >&2 2>/dev/null || true
34
+ exit 1
35
+ }
36
+
37
+ # ── Seed a realistic settings.json carrying secret-bearing siblings ─────────
38
+ # (FAKE values — never real credentials.)
39
+ cat > "$EAGLE_FACTORY_SETTINGS" <<'JSON'
40
+ {
41
+ "apiKey": "fake-sk-do-not-leak-0000",
42
+ "customModels": [
43
+ { "model": "glm-5.2", "provider": "z-ai", "maxTokens": 200000 }
44
+ ],
45
+ "statusLine": { "type": "command", "command": "echo droid" }
46
+ }
47
+ JSON
48
+
49
+ # ── 1. Register — siblings preserved, hooks added ───────────────────────────
50
+ eagle_register_factory_hooks >/dev/null
51
+
52
+ jq -e '.apiKey == "fake-sk-do-not-leak-0000"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
53
+ || fail "apiKey was not preserved verbatim"
54
+ jq -e '.customModels[0].model == "glm-5.2" and .customModels[0].maxTokens == 200000' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
55
+ || fail "customModels were not preserved"
56
+ jq -e '.statusLine.command == "echo droid"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
57
+ || fail "statusLine was not preserved"
58
+
59
+ # Every registered Eagle handler points at .eagle-mem/hooks and uses type=command.
60
+ jq -e '
61
+ [.hooks | .. | objects | select(.command? != null)
62
+ | select(.command | test("\\.eagle-mem/hooks/"))]
63
+ | length == 8
64
+ and all(.[]; .type == "command")
65
+ ' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
66
+ || fail "expected 8 eagle command handlers across hook events"
67
+
68
+ # Every Eagle entry block carries a numeric timeout.
69
+ jq -e '
70
+ [.hooks | to_entries[] | .value[]
71
+ | select(any(.hooks[]?; .command | test("\\.eagle-mem/hooks/")))]
72
+ | all(.[]; all(.hooks[]; (.timeout | type) == "number"))
73
+ ' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
74
+ || fail "an Eagle hook entry is missing a numeric timeout"
75
+
76
+ # The documented schema has no statusMessage — an unknown key in this
77
+ # apiKey-bearing file risks strict-parse breakage, so it must never appear.
78
+ jq -e '[.hooks | .. | objects | select(has("statusMessage"))] | length == 0' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
79
+ || fail "statusMessage key leaked into Factory settings.json"
80
+
81
+ # Matchers belong only to PreToolUse / PostToolUse / PreCompact.
82
+ jq -e '(.hooks.SessionStart[0] | has("matcher")) | not' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
83
+ || fail "SessionStart should not carry a matcher"
84
+ jq -e '.hooks.PreToolUse[0].matcher | test("Execute") and test("Create")' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
85
+ || fail "PreToolUse matcher missing Execute/Create"
86
+ jq -e '.hooks.PostToolUse[0].matcher | test("Execute") and test("Create")' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
87
+ || fail "PostToolUse matcher missing Execute/Create"
88
+ jq -e '[.hooks.PreCompact[].matcher] | sort == ["auto","manual"]' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
89
+ || fail "PreCompact should register both manual and auto matchers"
90
+
91
+ # ── 2. Idempotent — re-running adds no duplicates ───────────────────────────
92
+ eagle_register_factory_hooks >/dev/null
93
+ jq -e '
94
+ [.hooks | .. | objects | select(.command? != null)
95
+ | select(.command | test("\\.eagle-mem/hooks/"))]
96
+ | length == 8
97
+ ' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
98
+ || fail "re-registration created duplicate handlers (not idempotent)"
99
+
100
+ # ── 2b. Update branch — a stale command at the same script path is rewritten ──
101
+ # in place (not appended) to the canonical command, so the handler count stays 8.
102
+ # The idempotency check above only exercises the exact-match early return; this
103
+ # drives the contains($script) update path that a wrapper-prefix change would hit.
104
+ canonical_ss="EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\""
105
+ tmp_mut=$(mktemp)
106
+ jq --arg stale "STALE=1 bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\"" \
107
+ '.hooks.SessionStart[0].hooks[0].command = $stale' \
108
+ "$EAGLE_FACTORY_SETTINGS" > "$tmp_mut" && mv "$tmp_mut" "$EAGLE_FACTORY_SETTINGS"
109
+
110
+ eagle_register_factory_hooks >/dev/null
111
+
112
+ jq -e --arg c "$canonical_ss" '.hooks.SessionStart[0].hooks[0].command == $c' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
113
+ || fail "update branch did not rewrite a stale same-path command to canonical"
114
+ jq -e '
115
+ [.hooks | .. | objects | select(.command? != null)
116
+ | select(.command | test("\\.eagle-mem/hooks/"))]
117
+ | length == 8
118
+ ' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
119
+ || fail "update branch appended instead of updating in place (count != 8)"
120
+
121
+ # ── 3. Remove — hooks gone, secrets intact ──────────────────────────────────
122
+ eagle_remove_factory_hooks
123
+ jq -e 'has("hooks") | not' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
124
+ || fail "hooks key was not removed after eagle_remove_factory_hooks"
125
+ jq -e '.apiKey == "fake-sk-do-not-leak-0000"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
126
+ || fail "apiKey was lost during hook removal"
127
+ jq -e '.customModels[0].model == "glm-5.2"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
128
+ || fail "customModels were lost during hook removal"
129
+ jq -e '.statusLine.command == "echo droid"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
130
+ || fail "statusLine was lost during hook removal"
131
+
132
+ echo "factory hooks config regressions passed"