diff --git a/gateway/builtin_hooks/__init__.py b/gateway/builtin_hooks/__init__.py new file mode 100644 index 000000000..37da09db9 --- /dev/null +++ b/gateway/builtin_hooks/__init__.py @@ -0,0 +1 @@ +"""Built-in gateway hooks that are always registered.""" diff --git a/gateway/builtin_hooks/boot_md.py b/gateway/builtin_hooks/boot_md.py new file mode 100644 index 000000000..fced0b5e1 --- /dev/null +++ b/gateway/builtin_hooks/boot_md.py @@ -0,0 +1,86 @@ +"""Built-in boot-md hook — run ~/.hermes/BOOT.md on gateway startup. + +This hook is always registered. It silently skips if no BOOT.md exists. +To activate, create ``~/.hermes/BOOT.md`` with instructions for the +agent to execute on every gateway restart. + +Example BOOT.md:: + + # Startup Checklist + + 1. Check if any cron jobs failed overnight + 2. Send a status update to Discord #general + 3. If there are errors in /opt/app/deploy.log, summarize them + +The agent runs in a background thread so it doesn't block gateway +startup. If nothing needs attention, it replies with [SILENT] to +suppress delivery. +""" + +import logging +import os +import threading +from pathlib import Path + +logger = logging.getLogger("hooks.boot-md") + +HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")) +BOOT_FILE = HERMES_HOME / "BOOT.md" + + +def _build_boot_prompt(content: str) -> str: + """Wrap BOOT.md content in a system-level instruction.""" + return ( + "You are running a startup boot checklist. Follow the BOOT.md " + "instructions below exactly.\n\n" + "---\n" + f"{content}\n" + "---\n\n" + "Execute each instruction. If you need to send a message to a " + "platform, use the send_message tool.\n" + "If nothing needs attention and there is nothing to report, " + "reply with ONLY: [SILENT]" + ) + + +def _run_boot_agent(content: str) -> None: + """Spawn a one-shot agent session to execute the boot instructions.""" + try: + from run_agent import AIAgent + + prompt = _build_boot_prompt(content) + agent = AIAgent( + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + max_iterations=20, + ) + result = agent.run_conversation(prompt) + response = result.get("final_response", "") + if response and "[SILENT]" not in response: + logger.info("boot-md completed: %s", response[:200]) + else: + logger.info("boot-md completed (nothing to report)") + except Exception as e: + logger.error("boot-md agent failed: %s", e) + + +async def handle(event_type: str, context: dict) -> None: + """Gateway startup handler — run BOOT.md if it exists.""" + if not BOOT_FILE.exists(): + return + + content = BOOT_FILE.read_text(encoding="utf-8").strip() + if not content: + return + + logger.info("Running BOOT.md (%d chars)", len(content)) + + # Run in a background thread so we don't block gateway startup. + thread = threading.Thread( + target=_run_boot_agent, + args=(content,), + name="boot-md", + daemon=True, + ) + thread.start() diff --git a/gateway/hooks.py b/gateway/hooks.py index 15ecd3fee..c50394b20 100644 --- a/gateway/hooks.py +++ b/gateway/hooks.py @@ -51,14 +51,33 @@ class HookRegistry: """Return metadata about all loaded hooks.""" return list(self._loaded_hooks) + def _register_builtin_hooks(self) -> None: + """Register built-in hooks that are always active.""" + try: + from gateway.builtin_hooks.boot_md import handle as boot_md_handle + + self._handlers.setdefault("gateway:startup", []).append(boot_md_handle) + self._loaded_hooks.append({ + "name": "boot-md", + "description": "Run ~/.hermes/BOOT.md on gateway startup", + "events": ["gateway:startup"], + "path": "(builtin)", + }) + except Exception as e: + print(f"[hooks] Could not load built-in boot-md hook: {e}", flush=True) + def discover_and_load(self) -> None: """ Scan the hooks directory for hook directories and load their handlers. + Also registers built-in hooks that are always active. + Each hook directory must contain: - HOOK.yaml with at least 'name' and 'events' keys - handler.py with a top-level 'handle' function (sync or async) """ + self._register_builtin_hooks() + if not HOOKS_DIR.exists(): return diff --git a/website/docs/user-guide/features/hooks.md b/website/docs/user-guide/features/hooks.md index 0ae8905de..87c7f9846 100644 --- a/website/docs/user-guide/features/hooks.md +++ b/website/docs/user-guide/features/hooks.md @@ -88,6 +88,26 @@ Handlers registered for `command:*` fire for any `command:` event (`command:mode ### Examples +#### Boot Checklist (BOOT.md) — Built-in + +The gateway ships with a built-in `boot-md` hook that looks for `~/.hermes/BOOT.md` on every startup. If the file exists, the agent runs its instructions in a background session. No installation needed — just create the file. + +**Create `~/.hermes/BOOT.md`:** + +```markdown +# Startup Checklist + +1. Check if any cron jobs failed overnight — run `hermes cron list` +2. Send a message to Discord #general saying "Gateway restarted, all systems go" +3. Check if /opt/app/deploy.log has any errors from the last 24 hours +``` + +The agent runs these instructions in a background thread so it doesn't block gateway startup. If nothing needs attention, the agent replies with `[SILENT]` and no message is delivered. + +:::tip +No BOOT.md? The hook silently skips — zero overhead. Create the file whenever you need startup automation, delete it when you don't. +::: + #### Telegram Alert on Long Tasks Send yourself a message when the agent takes more than 10 steps: