# Hermes Agent - Development Guide 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: long form goes in `website/docs/developer-guide/`. **Never give up on the right solution.** ## What Hermes Is Hermes is a personal AI agent that runs one agent core across a CLI, a TUI, an Electron desktop app, a web dashboard, ACP editors and a messaging gateway (25+ platform plugins). It is extended 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. A change to the stock system prompt or `tools[]` fails `tests/ci/test_prompt_surface.py` until `scripts/ci/prompt_surface.py render` is committed with it, and that snapshot (`tests/fixtures/prompt_surface/`) needs a hermes-agent-core approval to merge. ## 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) → plugin → MCP server in the catalog → new core tool (fundamental, broadly useful, unreachable otherwise). ## Development Environment `source ./activate` (`activate.fish`, `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`** (`just 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). `just` lists every developer command (lefthook calls the same recipes); a new command is a `scripts/` file plus a recipe. The `HX0xx`/`PS-P0x` rules and their fixes are in `scripts/code_health/config.py`; `# noqa` does not waive a finding, `# health: allow -- ` does. ## Project Structure 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, handle_function_call(); discovery in tools/registry.py ├── 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 ├── scripts/ # check, run_tests.sh, code_health/, ci/ ├── evals/ # live-model A/B and probe harnesses └── website/ # Docusaurus docs (developer-guide/ holds the long-form area docs) ``` **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 (the tree above, `gateway/run.py`, `tools/mcp_tool.py`, `hermes_cli/kanban.py`, `hermes_cli/web_server.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:** 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). - **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`.** - **Dependencies carry upper bounds** (`>=floor,