# Hermes Agent - Development Guide For AI coding assistants and developers working on hermes-agent. This root file is a hub: what applies everywhere, then a routing table. Each area's `AGENTS.md` loads automatically when you work in that directory; read it before editing there. `python scripts/check` caps this file at 12k chars and every root-to-area chain at 30k, so it loads whole on 128k+ models: long form goes in the guide. **Never give up on the right solution.** ## What Hermes Is Hermes is a personal AI agent that runs the same agent core across a CLI, a messaging gateway (Telegram, Discord, Slack, ~20 platforms), a TUI, and an Electron desktop app. It learns across sessions (memory + skills), delegates to subagents, runs scheduled jobs, and drives a real terminal and browser. It is extended primarily through **plugins and skills**, not by growing the core. Two invariants shape almost every design decision and are the lens for reviewing any change: - **Per-conversation prompt caching is sacred.** Mutating past context, swapping toolsets, reloading memories or rebuilding the system prompt mid-conversation breaks the cached prefix and multiplies the user's cost; the ONE exception is context compression. Slash commands that change system-prompt state defer to the next session, with an opt-in `--now` (`/skills install --now`). - **The core is a narrow waist; capability lives at the edges.** Every model tool is sent on every API call, so the bar for a new *core* tool is high. New capability should arrive as a CLI command + skill, a service-gated tool, or a plugin — not as core surface. ## Contribution rubric The project's intent layer, for contributors and for the triage sweeper (which may only close on `implemented_on_main`, `cannot_reproduce` or `incoherent`; taste-based closes are a maintainer's call, and when in doubt a PR stays open). Long form with examples: `website/docs/developer-guide/contributing.md` § Contribution rubric. **Wanted:** real bug fixes (repro on `main`, the exact line, the whole class incl. sibling paths); reach at the edges (adapters, providers, models, UI features) wired into the existing setup UX; god-file → module refactors; extending before duplicating (3+ PRs in one category → an ABC + orchestrator); behaviour-contract tests; E2E with real imports against a temp `HERMES_HOME` for resolution, config, security and I/O changes; salvage by cherry-pick so authorship survives. **Rejected even when well-built:** hooks with no concrete consumer; new `HERMES_*` env vars for non-secret config (`.env` is secrets only, behaviour goes in `config.yaml`); a new core tool when terminal + file or a skill already does the job; `offset`/`limit` pagination on instructional tools; "fixes" that destroy the feature they secure; outbound telemetry without an opt-in gate; change-detector tests; plugins that touch core files; third-party products in the core tree (ship a standalone plugin repo). **Before you call it a bug,** verify the claim AND the intent (`git log -p -S ""`): the isolation is often the design (profiles are islands on purpose), an absence can be load-bearing, and a fix that cannot point to the line where the bug manifests has an unverified premise. **Security:** `SECURITY.md` is the scope authority. A §3.1 finding goes private (GitHub Security Advisories or security@nousresearch.com), never into a public issue, PR, commit or comment; §3.2 hardening is ordinary public work. Name the §2 boundary crossed, with a repro on `main`. **Footprint ladder** (take the highest rung that solves it): extend existing code → CLI command + skill → service-gated tool (`check_fn` answers reachability/opt-in, never per-session surface: `tools/AGENTS.md`) → plugin → MCP server in the catalog → new core tool (fundamental, broadly useful, unreachable otherwise). ## Development Environment `source ./activate` (fish: `activate.fish`, PowerShell: `activate.ps1`) provisions and activates the PM environment; pick an isolated `HERMES_HOME`/`HERMES_RUNTIME_DIR` first (`website/docs/reference/package-management.md#developer-workflow`). Tests need the separate test environment in `CONTRIBUTING.md`. **`python scripts/check`** runs every blocking lint check CI runs, with CI's pinned tools; `--install-hook pre-push` runs it on every push (re-run after pulling to refresh the hook). `# noqa` does not waive a ratchet finding: `# health: allow -- ` does. ## Project Structure Counts shift constantly; the filesystem is canonical. Load-bearing entry points: ``` hermes-agent/ ├── run_agent.py # AIAgent facade; the turn loop lives in agent/turn_*.py ├── model_tools.py # Tool orchestration, discover_builtin_tools(), handle_function_call() ├── toolsets.py # TOOLSETS dict, _HERMES_CORE_TOOLS ├── cli.py # HermesCLI (REPL, slash dispatch) + hermes_cli/cli_*_mixin.py ├── hermes_state.py # SessionDB facade; hermes_state_*.py siblings ├── hermes_constants.py # get_hermes_home(), display_hermes_home() — profile-aware paths ├── agent/ # turn loop phases, providers, memory, compression, prompt builder ├── hermes_cli/ # CLI subcommands, setup, config, plugins loader, updater, web_routers/ ├── tools/ # Tool implementations (tools/registry.py) + environments/ backends ├── gateway/ # run.py facade + run_*.py phases + session*.py + platforms/ ├── plugins/ # memory/, context_engine/, model-providers/, kanban/, image_gen/, ... ├── skills/ # Built-in skills (by category) optional-skills/: shipped, not active ├── ui-tui/, tui_gateway/ # Ink terminal UI + its Python JSON-RPC backend (also serves Desktop) ├── apps/desktop/ # Electron desktop app (+ apps/shared) web/: dashboard SPA ├── cron/ # jobs.py + scheduler.py (+ scheduler_*.py) ├── pm/, hermes_platform/ # dependency/environment manager; machine facts + executable lookup ├── scripts/ # check, run_tests.sh, code_health/, ci/ ├── website/ # Docusaurus docs (developer-guide/ holds the long-form area docs) └── tests/ # Pytest suite, mirrors the source tree ``` **User state:** `~/.hermes/config.yaml` (settings), `.env` (secrets only), `logs/` (`hermes logs`); all profile-aware via `get_hermes_home()`. ### Facade + siblings layout Every former god file is a **facade** (public entry points + the names other packages import) plus **siblings** `_.py`, each owning one topic (`hermes_state.py`, `gateway/run.py`, `tools/mcp_tool.py`, `hermes_cli/kanban.py`, `hermes_cli/web_server.py`, `cli.py` → `hermes_cli/cli_*_mixin.py`, `run_agent.py` → `agent/turn_*.py`). - **Find code by topic:** `grep -rn "def name" /_*.py`, not by reading the facade. - **Siblings late-import the facade** inside functions; never a module-level cycle. - **Patch where production reads:** a sibling doing `from import name` inside the function makes the facade the seam; a patch on the defining module passes silently. - **Size and complexity are ratcheted per unit** (`scripts/code_health/config.py`): new functions CC ≤ 20, ≤ 300 lines, nesting ≤ 6; files ≤ 2,000 lines; units already over may only go down (move a function into a sibling to offset growth, or put new tests in a new test file). Behaviour goes in a sibling, never a facade; name ladders become a dict → handler. - **No re-export shims for internal moves;** internal paths are not API. Moving a symbol means fixing its docs in the same PR (grep `website/docs`, `skills/`, every `AGENTS.md`). ## Rules that apply everywhere - **No defensive clutter:** no wrappers or `try/except: pass` around code that cannot fail, no flags nobody sets, no dead code wired in without E2E proof. Comments keep the WHY, cut the WHAT. - **Never infer process identity from argv substrings;** use `gateway.status.looks_like_gateway_command_line` / `hermes_cli.update_cmd._hermes_holder_subcommand` (HX003; details `hermes_cli/AGENTS.md`). - **Never hardcode `~/.hermes`:** `get_hermes_home()` for paths, `display_hermes_home()` for text (HX001). `_get_profiles_root()` is HOME-anchored on purpose. - **One process serves many profiles.** Code that runs outside a turn (boot probes, eviction, tickers, deferred callbacks, RPC methods, thread hops, child spawns) binds the owning profile scope explicitly; `os.environ`, module globals and import-time values hold the launch profile's, so an unbound read is a silent default-profile leak. Binding points and seams: `gateway/AGENTS.md` § Profile scope (HX002/HX004/HX005/HX012, PS-P05/P06). - **Machine facts and executable lookup go through `hermes_platform`** (`hermes_platform/AGENTS.md`). - **Dependencies carry upper bounds** (`>=floor,