Add post-tool and session-end hooks for Codex/Hermes/OpenCode#230
Add post-tool and session-end hooks for Codex/Hermes/OpenCode#230Gradata wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
📝 Walkthrough
WalkthroughThis PR refactors the hook execution infrastructure to support post-tool and session-end phases alongside the existing pre-tool phase. A new generic ChangesThree-Phase Hook Installation Across All Adapters
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.22.0)OpenGrep fatal error (exit code 2): �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Gradata/src/gradata/hooks/adapters/_base.py`:
- Around line 132-136: The hook_command_for_module function currently embeds
brain_dir verbatim which can be relative; change it to serialize an
absolute/canonical path (e.g., call brain_dir.resolve() or convert to an
absolute string before quoting) so the generated BRAIN_DIR is always an absolute
path; update hook_command_for_module to use the resolved/absolute Path when
building the f-string passed to shlex.quote(sys.executable) and BRAIN_DIR.
In `@Gradata/src/gradata/hooks/adapters/hermes.py`:
- Around line 166-181: The migration currently skips writing the config when no
new hook is appended because it only checks `added`; update the logic around the
loop that calls `_migrate_legacy_event(hooks, legacy_key, current_key)` to
detect whether any legacy->current migrations occurred (e.g., set a boolean flag
like `migrated = True` when `_migrate_legacy_event` moves entries or compare
pre/post state of `hooks`), and if either `added` is non-empty or `migrated` is
true, call `atomic_write_text(agent_config_path, _dump_simple_yaml(data))` and
return the appropriate InstallResult; this ensures migrations performed by
`_migrate_legacy_event` are persisted even when no new hook (`sig`) was
appended.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4f364644-089b-4897-914c-1ac13562952d
📒 Files selected for processing (5)
Gradata/src/gradata/hooks/adapters/_base.pyGradata/src/gradata/hooks/adapters/codex.pyGradata/src/gradata/hooks/adapters/hermes.pyGradata/src/gradata/hooks/adapters/opencode.pyGradata/tests/test_hook_adapters.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: pytest macos-latest / py3.12
- GitHub Check: pytest ubuntu-latest / py3.12
- GitHub Check: pytest macos-latest / py3.11
- GitHub Check: pytest windows-latest / py3.12
- GitHub Check: pytest (py3.11)
- GitHub Check: pytest (py3.12)
- GitHub Check: pytest windows-latest / py3.11
- GitHub Check: pytest ubuntu-latest / py3.11
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/tests/**/*.py
📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Gradata/tests/**/*.py: SetBRAIN_DIRenvironment variable viatmp_pathin conftest.py for test isolation — ensure_paths.pymodule cache refreshes when callingBrain.init()directly inside tests
Add unit tests intests/test_*.pyfor every CI push without LLM calls (deterministic); mark integration tests with@pytest.mark.integrationand skip them by default (they hit real LLM APIs)
Files:
Gradata/tests/test_hook_adapters.py
Gradata/src/**/*.py
📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Gradata/src/**/*.py: Prefersentence-transformersfor local embeddings,google-genaifor Gemini embeddings,cryptographyfor AES-GCM encrypted system.db,bm25sfor BM25 rule ranking, andmem0aifor external memory adapters — guard all optional dependency imports withtry / except ImportErrorat the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bareexcept: pass— use typed exceptions or at minimumlogger.warning(...)withexc_info=Trueto avoid silent failure in a memory product
Never import from out-of-scope sibling directories../Sprites/or../Hausgem/withingradata/*code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to../Sprites/,../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from insidegradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes
Files:
Gradata/src/gradata/hooks/adapters/_base.pyGradata/src/gradata/hooks/adapters/opencode.pyGradata/src/gradata/hooks/adapters/hermes.pyGradata/src/gradata/hooks/adapters/codex.py
🔇 Additional comments (4)
Gradata/tests/test_hook_adapters.py (1)
55-68: LGTM!Also applies to: 70-105, 107-123
Gradata/src/gradata/hooks/adapters/codex.py (1)
74-91: LGTM!Also applies to: 100-123, 128-188
Gradata/src/gradata/hooks/adapters/hermes.py (1)
245-295: LGTM!Gradata/src/gradata/hooks/adapters/opencode.py (1)
49-78: LGTM!Also applies to: 83-120
| def hook_command_for_module(brain_dir: Path, module: str) -> str: | ||
| return ( | ||
| f"BRAIN_DIR={shlex.quote(str(brain_dir))} " | ||
| f"{shlex.quote(sys.executable)} -m gradata.hooks.inject_brain_rules" | ||
| f"{shlex.quote(sys.executable)} -m {module}" | ||
| ) |
There was a problem hiding this comment.
Serialize BRAIN_DIR as an absolute path.
hook_signature() already canonicalizes brain_dir, but the hook command stores it verbatim. If install is run with a relative brain dir, the generated hook later resolves against the agent's runtime CWD, so post-tool/session-end can target the wrong brain directory.
Suggested fix
def hook_command_for_module(brain_dir: Path, module: str) -> str:
+ resolved_brain_dir = brain_dir.resolve()
return (
- f"BRAIN_DIR={shlex.quote(str(brain_dir))} "
+ f"BRAIN_DIR={shlex.quote(str(resolved_brain_dir))} "
f"{shlex.quote(sys.executable)} -m {module}"
)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Gradata/src/gradata/hooks/adapters/_base.py` around lines 132 - 136, The
hook_command_for_module function currently embeds brain_dir verbatim which can
be relative; change it to serialize an absolute/canonical path (e.g., call
brain_dir.resolve() or convert to an absolute string before quoting) so the
generated BRAIN_DIR is always an absolute path; update hook_command_for_module
to use the resolved/absolute Path when building the f-string passed to
shlex.quote(sys.executable) and BRAIN_DIR.
| added: list[str] = [] | ||
| for current_key, legacy_key, command in ( | ||
| ("pre_tool_call", "pre_tool_use", hook_command(brain_dir)), | ||
| ("post_tool_call", "post_tool_use", post_tool_hook_command(brain_dir)), | ||
| ("on_session_end", "session_end", session_end_hook_command(brain_dir)), | ||
| ): | ||
| entries = _migrate_legacy_event(hooks, legacy_key, current_key) | ||
| if any(isinstance(entry, dict) and entry.get("id") == sig for entry in entries): | ||
| continue | ||
| entries.append({"id": sig, "command": command}) | ||
| added.append(current_key) | ||
| if not added: | ||
| return InstallResult( | ||
| AGENT, agent_config_path, "already_present", "hook already present" | ||
| AGENT, agent_config_path, "already_present", "hooks already present" | ||
| ) | ||
| pre_tool_call.append({"id": sig, "command": hook_command(brain_dir)}) | ||
| atomic_write_text(agent_config_path, _dump_simple_yaml(data)) |
There was a problem hiding this comment.
Persist legacy-key migrations even when no new hook is appended.
_migrate_legacy_event() mutates hooks before the added check. If the file only contains legacy pre_tool_use / post_tool_use / session_end entries, this path returns "already_present" without writing the migrated config, so Hermes keeps ignoring the old keys.
Suggested fix
- added: list[str] = []
+ added: list[str] = []
+ migrated = False
for current_key, legacy_key, command in (
("pre_tool_call", "pre_tool_use", hook_command(brain_dir)),
("post_tool_call", "post_tool_use", post_tool_hook_command(brain_dir)),
("on_session_end", "session_end", session_end_hook_command(brain_dir)),
):
+ had_legacy = legacy_key in hooks
entries = _migrate_legacy_event(hooks, legacy_key, current_key)
+ migrated = migrated or had_legacy
if any(isinstance(entry, dict) and entry.get("id") == sig for entry in entries):
continue
entries.append({"id": sig, "command": command})
added.append(current_key)
- if not added:
+ if not added and not migrated:
return InstallResult(
AGENT, agent_config_path, "already_present", "hooks already present"
)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Gradata/src/gradata/hooks/adapters/hermes.py` around lines 166 - 181, The
migration currently skips writing the config when no new hook is appended
because it only checks `added`; update the logic around the loop that calls
`_migrate_legacy_event(hooks, legacy_key, current_key)` to detect whether any
legacy->current migrations occurred (e.g., set a boolean flag like `migrated =
True` when `_migrate_legacy_event` moves entries or compare pre/post state of
`hooks`), and if either `added` is non-empty or `migrated` is true, call
`atomic_write_text(agent_config_path, _dump_simple_yaml(data))` and return the
appropriate InstallResult; this ensures migrations performed by
`_migrate_legacy_event` are persisted even when no new hook (`sig`) was
appended.
Closes GRA-1163.\n\n## Summary\n- Add shared hook command helpers for post_tool correction capture and session_end close.\n- Install/uninstall post_tool + session_end hooks for Codex, Hermes, and OpenCode adapters.\n- Add tests covering install/uninstall behavior for the new hooks.\n\n## Verification\n- pytest tests/test_hook_adapters.py (14 passed)