01 / Context budgeting
Treat prompt stability as a conversation invariant
The guide explains that changing past context or toolsets invalidates the cached prefix and increases cost. Commands that change prompt state default to the next session, with an explicit immediate option.
Source excerpt starting at line 20.20- **Per-conversation prompt caching is sacred.** A long-lived conversation reuses a cached21 prefix every turn. Anything that mutates past context, swaps toolsets, reloads memories, or22 rebuilds the system prompt mid-conversation invalidates that cache and multiplies the user's23 cost. We do not do it; the ONE exception is context compression. Slash commands that mutate24 system-prompt state (skills, tools, memory) must be **cache-aware**: default to deferred25 invalidation (takes effect next session) with an opt-in `--now` flag (`/skills install --now`26 is the canonical pattern).
02 / Behavioral framing
Provide an ordered decision process for new capability
The footprint ladder starts with extending existing code and works through commands, gated tools, plugins, and MCP before adding a core tool. The ordering connects architecture decisions to per-call schema cost.
Source excerpt starting at line 134.134Choose the highest (least-footprint) rung that correctly solves the problem:135 1361. **Extend existing code** — a variation of something that exists. Zero new surface.1372. **CLI command + skill** — config/state/infra expressible as shell commands; the agent runs138 `hermes <subcommand>` guided by a skill. Default for subscriptions, scheduled tasks,139 service setup (`hermes webhook`, `hermes cron`, `hermes tools`).1403. **Service-gated tool (`check_fn`)** — needs structured params/returns AND only appears when141 a prerequisite is configured (Home Assistant tools, memory-provider tools). This rung gates142 reachability/opt-in process-wide; a capability that varies per SESSION (who is watching) is143 a named toolset folded in by the toolset resolver, not a `check_fn` — see "Surface capability144 is a property of the SESSION" below.1454. **Plugin** — third-party/niche/user-specific; lives in `~/.hermes/plugins/` or a pip146 package, discovered at runtime.1475. **MCP server (in the catalog)** — genuinely a tool but not core-fundamental. Zero permanent148 core-schema footprint, reusable by any MCP host, reached via the built-in MCP client.1496. **New core tool** — only when fundamental, broadly useful to nearly every user, and150 unreachable via terminal + file or an MCP server (terminal, read_file, web_search,151 browser_navigate).
03 / Pointing at the source of truth
Make the test wrapper enforce isolation
The required runner clears credential variables, fixes locale and timezone, uses a temporary home, and isolates test files in subprocesses. The guide connects those choices to local-versus-CI incidents.
Source excerpt starting at line 316.316**ALWAYS use `scripts/run_tests.sh`**, never bare `pytest`. It enforces CI parity: credential317vars unset, `TZ=UTC`, `LANG=C.UTF-8`, `HERMES_HOME` → temp dir, and per-file subprocess318isolation via `scripts/run_tests_parallel.py` (no xdist; workers scale with CPU count) so319module-level dicts/ContextVars cannot leak between files. Direct `pytest` on a big machine320with API keys set has caused repeated "works locally, fails in CI" incidents (and the reverse).
04 / Verification by change type
Test on the host that owns the behavior
Platform-specific behavior must run on that operating system with the project’s named markers. The guide explains that alternate skip mechanisms can produce tests that run on no CI platform.
Source excerpt starting at line 368.368**Use the marker, never a bare `skipif`.** `scripts/ci/list_os_marked_tests.py` finds files for369the macOS/Windows lanes by grepping the marker *name*, then filters with `-m <marker>`. A370`skipif(sys.platform != "win32")` test skips on Linux AND is never imported on Windows — it runs371nowhere, silently. A file-local alias (`windows_only = pytest.mark.skipif(...)`) is listed but372`-m windows_only` deselects everything: green over zero coverage. Don't `pytest.skip()` non-host373rows of a platform `@parametrize` — split into one marked test per OS.
05 / Good and bad pairs
Prefer behavioral relationships over changing catalog values
The test guidance contrasts assertions about fixed model names and counts with contracts between data structures. It explains how snapshot-like checks can turn expected catalog updates into maintenance failures.
Source excerpt starting at line 385.385A change-detector fails whenever data *expected to change* is updated — model catalogs,386`_config_version`, enumeration counts, hardcoded model lists. It adds no coverage and taxes387every routine update. Don't: `assert "gemini-2.5-pro" in _PROVIDER_MODELS["gemini"]`,388`assert DEFAULT_CONFIG["_config_version"] == 21`, `assert len(models) == 8`. Do: `assert389"gemini" in _PROVIDER_MODELS and len(_PROVIDER_MODELS["gemini"]) >= 1` (plumbing works);390`assert raw["_config_version"] == DEFAULT_CONFIG["_config_version"]` (migration reaches391latest); `assert not (set(moonshot_models) & coding_plan_only_models)` (no leak); every392catalog model has a context-length entry (relationship). If it reads like a snapshot, delete393it; if it reads like a contract between two pieces of data, keep it. Reviewers reject new394change-detectors; authors convert them before re-review.
06 / Router files
Map each work area to its instruction file
The routing table connects source areas to nested AGENTS.md files and describes what each covers. The root file also documents size targets and truncation behavior for injected area guidance.
Source excerpt starting at line 4.4This root file holds only what applies everywhere. Each area has its own `AGENTS.md` (aim for5~8k chars; `agent/subdirectory_hints.py` delivers up to 32k and truncates head/tail with a warning6past that); see the **routing table** at the end and read the area file before editing in that area.