feat: Hermes-native plugin surface for Genie - #2517
Conversation
… seed from main The profile seed (profiles/hermes/genie/, commit b112309) exists only on main lineage; ported verbatim so Group 3 can modify the README on this dev-cut branch. The dev->main merge will reconcile on identical blobs except the README, which carries the wish's additive edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEJCsZTjxLyrM8BQKjGEVs
…only tools
Hermes-native surface for Genie (wish hermes-khaw-native-surface, Group 1).
7 read-only tools grounded on the v5 CLI (doctor/board/task list/task
status/launch --dry-run), argv-only subprocess bridge with shell-metachar
rejection, validate_ref traversal guard + in-bounds WISH.md read, uniform
{success, mutation:none, cwd, command|source} payload. 28 pytest cases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEJCsZTjxLyrM8BQKjGEVs
…scripts, profile seed update Group 3 of wish hermes-khaw-native-surface. Install script (symlink default, --copy mode), smoke script, native-surface + mutation-gates references, Hermes-native cross-links in root README, Claude Code plugin README, and the Hermes profile seed (additive). Two review LOWs applied: ln -sfn manual one-liner, payload-contract wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEJCsZTjxLyrM8BQKjGEVs
… CLI tree, skills Group 2 of wish hermes-khaw-native-surface. /genie dispatcher (+4 wrapper commands) with outcome-first rendering and evidence footers, advisory hooks (session-start .genie reminder, terminal-scrape advice, never blocking), hasattr-guarded CLI tree and 4 path-based skills. 18 new tests (46 total). Two review LOWs applied: on_session_start degrades on unresolvable cwd; list-shaped command events joined before advisory match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEJCsZTjxLyrM8BQKjGEVs
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a native Hermes plugin for Genie (plugins/hermes-genie/) that exposes read-only Genie state (such as status, board, wish/task queries, and dry-run plans) as structured tools, slash commands, advisory hooks, and skills. It also includes comprehensive documentation, local installation/smoke scripts, test suites, and a Genie Hermes profile seed. The review feedback focuses on several key improvements: dynamically importing commands and hooks inside registration functions to prevent circular dependency cycles at module load time, robustly locating the genie binary on the system PATH using shutil.which with a fallback path, and cleaning up __pycache__ directories during copy-style installations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| try: # package import (Hermes loads plugins as packages) | ||
| from . import commands, hooks | ||
| except ImportError: # flat import (module loaded from a file location) | ||
| import sys | ||
|
|
||
| _HERE = str(Path(__file__).resolve().parent) | ||
| if _HERE not in sys.path: | ||
| sys.path.insert(0, _HERE) | ||
| import commands # type: ignore[no-redef] | ||
| import hooks # type: ignore[no-redef] |
There was a problem hiding this comment.
To adhere to the general rules and break circular dependency cycles at module load time, the imports of commands and hooks should be moved dynamically inside the functions that use them (_register_commands and register) rather than being imported at the module level.
# Circular imports of commands and hooks have been removed from module level
# and moved dynamically inside _register_commands and register to break the load-time cycle.References
- Use dynamic imports (
require()orimport()) to break circular dependency cycles that would otherwise occur at module load time.
| def _register_commands(ctx) -> None: | ||
| """Slash commands: /genie dispatcher plus per-subcommand aliases.""" | ||
| command_defs: list[tuple[str, Any, str, str]] = [ |
There was a problem hiding this comment.
Dynamically import commands inside _register_commands to break the circular dependency cycle at module load time.
def _register_commands(ctx) -> None:
"""Slash commands: /genie dispatcher plus per-subcommand aliases."""
try:
from . import commands
except ImportError:
import commands
command_defs: list[tuple[str, Any, str, str]] = [| if hasattr(ctx, "register_command"): | ||
| _register_commands(ctx) | ||
|
|
||
| if hasattr(ctx, "register_hook"): | ||
| hook_defs: list[tuple[str, Any]] = [ | ||
| ("on_session_start", hooks.on_session_start), | ||
| ("pre_tool_call", hooks.pre_tool_call), | ||
| ("post_tool_call", hooks.post_tool_call), | ||
| ] | ||
| for event, handler in hook_defs: | ||
| ctx.register_hook(event, handler) | ||
|
|
||
| if hasattr(ctx, "register_cli_command"): | ||
| ctx.register_cli_command( | ||
| name="genie", | ||
| help="Genie read-only commands (status, board, wish, work-plan, review-plan)", | ||
| setup_fn=commands.setup_cli, | ||
| handler_fn=commands.cli_handler, | ||
| description="Operate Genie from the Hermes CLI without mutations", | ||
| ) |
There was a problem hiding this comment.
Dynamically import hooks and commands inside register to break the circular dependency cycle at module load time.
if hasattr(ctx, "register_command"):
_register_commands(ctx)
if hasattr(ctx, "register_hook"):
try:
from . import hooks
except ImportError:
import hooks
hook_defs: list[tuple[str, Any]] = [
("on_session_start", hooks.on_session_start),
("pre_tool_call", hooks.pre_tool_call),
("post_tool_call", hooks.post_tool_call),
]
for event, handler in hook_defs:
ctx.register_hook(event, handler)
if hasattr(ctx, "register_cli_command"):
try:
from . import commands
except ImportError:
import commands
ctx.register_cli_command(
name="genie",
help="Genie read-only commands (status, board, wish, work-plan, review-plan)",
setup_fn=commands.setup_cli,
handler_fn=commands.cli_handler,
description="Operate Genie from the Hermes CLI without mutations",
)| import json | ||
| import os | ||
| import re | ||
| import subprocess | ||
| from pathlib import Path | ||
| from typing import Any |
There was a problem hiding this comment.
| workdir = resolve_cwd(cwd) | ||
| try: | ||
| argv = build_genie_argv(args) | ||
| proc = subprocess.run( |
There was a problem hiding this comment.
Since genie is not reliably on the system PATH (especially on macOS where it defaults to ~/.genie/bin/genie), we should attempt to locate it using shutil.which and fall back to the default installation path if it is not found on PATH. This ensures the plugin works robustly across different environments.
| workdir = resolve_cwd(cwd) | |
| try: | |
| argv = build_genie_argv(args) | |
| proc = subprocess.run( | |
| workdir = resolve_cwd(cwd) | |
| try: | |
| argv = build_genie_argv(args) | |
| if not shutil.which(argv[0]): | |
| fallback = Path.home() / ".genie" / "bin" / "genie" | |
| if fallback.is_file() and os.access(fallback, os.X_OK): | |
| argv[0] = str(fallback) | |
| proc = subprocess.run( |
| if [ "$mode" = "copy" ]; then | ||
| cp -R "$plugin_src" "$target" | ||
| echo "installed (copy): $target (from $plugin_src)" |
There was a problem hiding this comment.
When performing a detached copy-style installation, __pycache__ directories from the development environment can be copied over. Clean them up from the target directory after copying.
| if [ "$mode" = "copy" ]; then | |
| cp -R "$plugin_src" "$target" | |
| echo "installed (copy): $target (from $plugin_src)" | |
| if [ "$mode" = "copy" ]; then | |
| cp -R "$plugin_src" "$target" | |
| find "$target" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true | |
| echo "installed (copy): $target (from $plugin_src)" |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 314f3a724a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| def pre_tool_call(event: Any = None, **kwargs: Any) -> dict[str, Any]: | ||
| """Advise (never block) when a tool call looks like scraping or polling Genie.""" | ||
| raw = _event_value(event, "command") or _event_value(event, "args") or "" |
There was a problem hiding this comment.
Read Hermes hook kwargs for terminal advice
In the Hermes plugin hook API, pre_tool_call is invoked with tool_name, args, and task_id keyword arguments (the docs/example call invoke_hook(..., tool_name="terminal", args=args, ...)), not an event object containing command. With the real call shape, a terminal scrape like args={"command":"tmux capture-pane -p"} leaves raw empty here and the advertised advisory hook never fires; keep the event-object fallback, but also inspect kwargs["args"]/kwargs["tool_name"].
Useful? React with 👍 / 👎.
| echo "installed (copy): $target (from $plugin_src)" | ||
| else | ||
| ln -s "$plugin_src" "$target" | ||
| echo "installed (symlink): $target -> $plugin_src" |
There was a problem hiding this comment.
Enable the plugin after installing it
This script stops immediately after creating the symlink/copy, but Hermes general plugins are opt-in per the plugin guide (hermes plugins enable <name> is required when a plugin is “not enabled in config”). A fresh user following the README will have files under $HERMES_HOME/plugins/genie but the tools/commands/hooks remain unloaded until they manually enable it, so the installer should run or at least print the required hermes plugins enable genie step.
Useful? React with 👍 / 👎.
Wish:
hermes-khaw-native-surfaceShips a native Hermes plugin from this repo (
plugins/hermes-genie/) so Hermes can act as the chat/reasoning cockpit while Genie remains the zero-daemon execution system. Strictly read-only MVP: every tool returnsmutation: "none"and wraps the genie v5 CLI via argv arrays (no shell strings).What's included
plugin.yaml,register(ctx), argv-only subprocess bridge with shell-metachar rejection +validate_reftraversal guard (incl. symlink-escape defense on the WISH.md read), 7 read-only tools grounded on the v5 CLI:genie_status,genie_board,genie_wish_status(board+task composite),genie_task_list,genie_task_status,genie_work_plan(launch --dry-run),genie_review_plan/genieslash dispatcher + 4 wrapper commands with outcome-first rendering and evidence footers; advisory hooks (session-start.geniereminder, terminal-scrape advice — never blocking); hasattr-guarded CLI tree + 4 skillsreferences/native-surface.md+mutation-gates.md, install/smoke scripts (symlink default,--copy), Hermes-native cross-links in root README, Claude Code plugin README, and the Hermes profile seed.genie/wishes/hermes-khaw-native-surface/WISH.md;profiles/hermes/genie/ported verbatim frommain(it exists only on main lineage) so the seed README could be modified here — the eventual dev→main merge reconciles on identical blobs except that README (additive edit)Verification
uv run --with pytest --with pyyaml --no-project python -m pytest plugins/hermes-genie/tests -qbun run check: typecheck/biome/knip/skills-lint/wishes-lint pass; bun test 655 pass / 2 pre-existing environmental failures (host~/.genie/config.jsonomni-approvals leaking intodispatch-fail-closed-regression+omni-dispatchvia hardcodedhomedir()insrc/lib/genie-config.ts— fails on any branch on that host; zero TS changed in this PR)/genie helpand/genie statusexecute natively inhermes chat; e2e evidence recorded in the KHAW repo (docs/evidence/genie-hermes-khaw-native-surface-smoke-2026-07-04.md)Follow-ups (non-blocking, documented)
active_profileuses~/.hermes/profiles/<name>/plugins/)--copyinstall carries__pycache__; KHAW-side extension-point test doesn't pin theforbiddenlist; bridge timeout has no upper clampThe KHAW-side bridge (Group 4) lands separately in the KHAW repo (
feat/khaw-genie-bridge).🤖 Generated with Claude Code
https://claude.ai/code/session_01BEJCsZTjxLyrM8BQKjGEVs