diff --git a/BLUEPRINT.md b/BLUEPRINT.md
new file mode 100644
index 000000000000..1480745dbf6d
--- /dev/null
+++ b/BLUEPRINT.md
@@ -0,0 +1,64 @@
+# Hermes Agent — BLUEPRINT
+
+
+
+Hermes Agent is a personal AI agent whose single agent core is reused across four
+surfaces: the `hermes` CLI, the messaging gateway (Telegram, Discord, Slack, WhatsApp,
+Signal and ~20 more platforms), the terminal UI, and the Electron desktop app. These are
+the non-negotiable requirements every change is measured against.
+
+1. **One agent core, many surfaces.** `agent/` owns the turn loop, provider adapters and
+ tool dispatch. CLI (`hermes_cli/`), gateway (`gateway/`), TUI (`tui_gateway/`), ACP
+ adapter (`acp_adapter/`) and the desktop app (`apps/desktop`) are transports over that
+ core, never forks of it.
+2. **Per-conversation prompt caching is sacred.** A long-lived conversation reuses a
+ cached prefix on every turn. Mutating past context, swapping toolsets mid-conversation
+ or rebuilding the system prompt invalidates the cache and multiplies the user's cost.
+ Context compression is the single sanctioned exception.
+3. **Narrow waist, capability at the edges.** Every model tool is sent on every API call,
+ so new *core* tools carry a high bar. New capability arrives as a CLI command plus a
+ skill, a service-gated tool, or a plugin under `plugins/`.
+4. **Provider neutrality.** Any OpenAI-compatible endpoint, Nous Portal, OpenRouter,
+ Anthropic or a self-hosted model must work with no core code change — backends ship as
+ plugins under `plugins/model-providers/`, registered through the `providers/` ABC.
+ Switching is a `hermes model` operation.
+5. **Runs anywhere.** Six terminal backends (local, Docker, SSH, Singularity, Modal,
+ Daytona) must stay interchangeable, including the serverless hibernate/wake paths.
+6. **The learning loop stays closed.** Agent-curated memory, autonomous skill creation,
+ in-use skill improvement and FTS5 session search are product-defining; a change that
+ silently degrades any of them is a regression even when tests pass.
+7. **Tests are headless, silent and automated.** `scripts/run_tests.sh` is the only
+ sanctioned Python invocation (it enforces CI parity); `-m 'not integration'` is the
+ default lane, and integration tests are opt-in and must never be required to prove a
+ normal change works.
+
+8. **`.env` is secrets only.** Behavioural settings live in `~/.hermes/config.yaml`,
+ resolved profile-aware via `get_hermes_home()`. New `HERMES_*` env vars for non-secret
+ config are rejected.
+
+
+
+## Scope Boundaries
+
+In scope: the agent core, the transports above, the tool/skill/plugin system, the cron
+scheduler, the documentation site under `website/`, and the packaging/installer paths.
+
+Out of scope: model training, hosted inference infrastructure, and per-user secrets —
+credentials live in the user's environment or config, never in this repository.
+
+## Tech Stack
+
+| Layer | Technology |
+|-------|-----------|
+| Core / CLI / gateway | Python 3.11–3.13, `uv` |
+| Desktop + web UI | Electron, TypeScript, Vite (`apps/desktop`, `web/`) |
+| Docs site | Docusaurus (`website/`) |
+| Packaging | setuptools wheel, Docker, Nix flake, `scripts/install.sh` / `scripts/install.ps1` |
+| Tests | pytest via `scripts/run_tests.sh` (`tests/`), vitest per JS workspace |
+| Lint | ruff (PLW1514 enforced), eslint, hadolint, prettier |
+
+## Delivery Criteria
+
+A change ships when: `pytest` is green on the non-integration lane, the affected CI
+sub-workflows pass, no new core model tool was added without an explicit justification,
+and the docs listed in `docs/INDEX.md` still describe what the code does.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000000..da063d629900
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Changelog
+
+All notable changes to Hermes Agent are recorded here.
+
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
+project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every
+release requires an entry for the version being shipped.
+
+This file was introduced after `0.18.2`; releases before that are recorded in the git
+history and in the GitHub releases page rather than restated here.
+
+## [Unreleased]
+
+### Added
+
+- Canonical documentation set: `BLUEPRINT.md`, `VISION.md`, `TESTING.md`,
+ `GOVERNANCE.md`, `RELEASING.md`, `CLAUDE.md`, `docs/ARCHITECTURE.md`,
+ `docs/ARCHITECTURE_MAP.md`, `docs/INDEX.md`, `docs/DEPLOYMENT_PROCESS.md`, and the
+ architecture diagram under `docs/diagrams/`.
+
+## [0.18.2]
+
+Baseline version at the time this changelog was created. See the git log for the
+changes that led to it.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000000..6fb176f34686
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,1434 @@
+
+# Hermes Agent - Development Guide
+
+Instructions for AI coding assistants and developers working on the hermes-agent codebase.
+
+**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, and ~20 other 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 properties shape almost every design decision and are the lens for
+reviewing any change:
+
+- **Per-conversation prompt caching is sacred.** A long-lived conversation
+ reuses a cached prefix every turn. Anything that mutates past context,
+ swaps toolsets, or rebuilds the system prompt mid-conversation invalidates
+ that cache and multiplies the user's cost. We do not do it (the one
+ exception is context compression).
+- **The core is a narrow waist; capability lives at the edges.** Every model
+ tool we add is sent on every API call, so the bar for a new *core* tool is
+ high. Most new capability should arrive as a CLI command + skill, a
+ service-gated tool, or a plugin — not as core surface.
+
+## Contribution Rubric — What We Want / What We Don't
+
+This is the project's intent layer. Use it two ways:
+
+1. **For humans and for your own work** — what gets merged and what gets
+ rejected, so a contribution aims at the target.
+2. **For automated review (the triage sweeper)** — guidance on when a PR is
+ safe to close on the three allowed reasons (`implemented_on_main`,
+ `cannot_reproduce`, `incoherent`) and, just as important, **when NOT to
+ close** one. Taste-based "we don't want this / out of scope" closes are NOT
+ an automated decision — those stay with a human maintainer. The sweeper's
+ job here is to recognize design intent and *avoid wrongly closing a
+ legitimate contribution*, not to make the won't-implement call itself.
+
+Read the balance right: Hermes ships a **lot** — most merges are bug fixes to
+real reported behavior, and the product surface (platforms, channels,
+providers, models, desktop/TUI features) expands aggressively and on purpose.
+The restraint below is aimed squarely at the **core agent + the model tool
+schema**, the one place where every addition is paid for on every API call.
+"Smallest footprint" governs *how a capability is wired into the core*, NOT
+whether the product is allowed to grow. We are expansive at the edges and
+conservative at the waist.
+
+### What we want
+
+- **Fix real bugs, well.** The bulk of what lands is `fix(...)` against an
+ actual reported symptom. A good fix reproduces the symptom on current
+ `main`, points to the exact line where it manifests, and fixes the whole bug
+ class — sibling call paths included — not just the one site the reporter hit.
+- **Expand reach at the edges.** New platform adapters, channels, providers,
+ models, and desktop/TUI/dashboard features are welcome and land routinely,
+ including large ones (a new messaging channel, a session-cap feature, a
+ Windows PTY bridge). Breadth in the product is a goal, not a footprint
+ concern — as long as it integrates with the existing setup/config UX
+ (`hermes tools`, `hermes setup`, auto-install) rather than bolting on a raw
+ env var.
+- **Refactor god-files into clean modules.** Extracting a multi-thousand-line
+ cluster out of `cli.py` / `run_agent.py` / `gateway/run.py` into a focused
+ mixin or module is wanted work, even when the diff is huge and mechanical
+ (large `+N/-N` refactors merge regularly). The "every line traces to the
+ request" test applies to *feature* PRs; a declared refactor's request IS the
+ extraction.
+- **Keep the core narrow.** New *model tools* are the expensive exception —
+ every tool ships on every API call. Prefer, in order: extend existing code →
+ CLI command + skill → service-gated tool (`check_fn`) → plugin → MCP server
+ in the catalog → new core tool (last resort). See "The Footprint Ladder."
+- **Extend, don't duplicate.** Before adding a module/manager/hook, check
+ whether existing infrastructure already covers the use case. When several PRs
+ integrate the same *category*, design one shared interface instead of merging
+ them one at a time (see the ABC + orchestrator note under the Footprint
+ Ladder).
+- **Behavior contracts over snapshots.** Tests should assert how two pieces of
+ data must relate (invariants), not freeze a current value (model lists,
+ config version literals, enumeration counts). See "Don't write
+ change-detector tests."
+- **E2E validation, not just green unit mocks.** For anything touching
+ resolution chains, config propagation, security boundaries, remote
+ backends, or file/network I/O, exercise the real path with real imports
+ against a temp `HERMES_HOME`. Mocks hide integration bugs.
+- **Cache-, alternation-, and invariant-safe.** Preserve prompt caching, strict
+ message role alternation (never two same-role messages in a row; never a
+ synthetic user message injected mid-loop), and a system prompt that is
+ byte-stable for the life of a conversation.
+- **Contributor credit preserved.** Salvage external work by cherry-picking
+ (rebase-merge) so authorship survives in git history; don't reimplement from
+ scratch when you can build on top.
+
+### What we don't want (rejected even when well-built)
+
+- **Speculative infrastructure.** Hooks, callbacks, or extension points with no
+ concrete consumer. Adding a hook is easy; removing one after plugins depend
+ on it is hard. A hook is NOT speculative if a contributor has a real, stated
+ use case — even if the consumer ships separately.
+- **New `HERMES_*` env vars for non-secret config.** `.env` is for secrets
+ only (API keys, tokens, passwords). All behavioral settings — timeouts,
+ thresholds, feature flags, display prefs — go in `config.yaml`. Bridge to an
+ internal env var if the mechanism needs one, but user-facing docs point to
+ `config.yaml`. Reject PRs that tell users to "set X in your .env" unless X
+ is a credential.
+- **A new core tool when terminal + file already do the job, or when a skill
+ would.** If the only barrier is file visibility on a remote backend, fix the
+ mount, not the toolset.
+- **Lazy-reading escape hatches on instructional tools.** No `offset`/`limit`
+ pagination on tools that load content the agent must read fully (skills,
+ prompts, playbooks). Models will read page 1 and skip the rest.
+- **"Fixes" that destroy the feature they secure.** A mitigation that kills the
+ feature's purpose is the wrong mitigation. Read the original commit's intent
+ (`git log -p -S`) before restricting behavior; find a fix that preserves the
+ feature.
+- **Outbound telemetry / usage attribution without opt-in gating.** No new
+ analytics, third-party identifier tagging, or attribution tags until a
+ generic user-facing opt-in (config gate + setup prompt + `hermes tools`
+ toggle) exists. Park behind a label, do not merge.
+- **Change-detector tests, cache-breaking mid-conversation, dead code wired in
+ without E2E proof, and plugins that touch core files.** Plugins live in their
+ own directory and work within the ABCs/hooks we provide; if a plugin needs
+ more, widen the generic plugin surface, don't special-case it in core.
+- **Third-party products / other people's projects integrated into the core
+ tree.** Observability backends, vendor SaaS integrations, analytics dashboards,
+ and similar "someone else's product" plugins do NOT land under `plugins/` in
+ this repo. They place an ongoing maintenance burden on us to keep them working
+ against a fast-moving core, for a backend we don't own. Ship them as a
+ **standalone plugin repo** users install into `~/.hermes/plugins/` (or via a
+ pip entry point), and promote them in the Nous Research Discord
+ (`#plugins-skills-and-skins`). This is a coupling-and-maintenance decision, not
+ a quality bar — the plugin can be excellent and still be a close. PRs that add
+ such a directory to the tree are closed with a pointer to publish it as its own
+ repo.
+
+### Before you call it a bug — verify the premise (and when NOT to close)
+
+The most common reason a well-written PR gets closed is not code quality — it
+is that the change is built on a **wrong premise**, or it treats an
+**intentional design as a gap**. These patterns cut both ways: they tell a
+human reviewer what to scrutinize, and they tell the automated sweeper when a
+PR is NOT safe to close as `implemented_on_main` / `cannot_reproduce` (when in
+doubt, leave it open for a human). They are distilled from real closes.
+
+- **"Intentional design, not a gap."** A limitation that looks like an
+ oversight is often deliberate. Before "fixing" a missing link or a
+ restriction, ask whether the isolation IS the design. Example: profiles are
+ independent islands on purpose — a PR adding live config inheritance from the
+ default profile was closed because coupling profiles together is exactly what
+ the design prevents (the copy-at-creation `--clone` path already covers the
+ legitimate "start from my default" case). Read the original commit's intent
+ (`git log -p -S ""`) before assuming something is unfinished.
+- **"The premise doesn't hold against how X actually works."** A PR's
+ justification frequently rests on a wrong mental model of an existing
+ mechanism. Trace the real code/runtime before accepting the rationale. Two
+ real closes: a rate-limit "re-probe during cooldown" PR (the breaker only
+ trips on a *confirmed-empty* account bucket, so re-probing just hammers a
+ bucket we've already proven empty); a usage-accumulation fix whose new branch
+ **never executes at runtime** because an earlier guard already popped the
+ state it depended on. If you can't point to the exact line where the bug
+ manifests AND show the fix changes that line's behavior, you haven't verified
+ the premise.
+- **"This fix was wrong — the absence/omission was deliberate."** Adding the
+ obvious-looking missing piece can break things the omission was protecting.
+ Example: restoring "missing" `__init__.py` files made a test tree importable
+ as a dotted package that shadowed the real plugin, deleting its `register()`
+ at import time. The absence was load-bearing.
+- **"Overreached / resurrected an approach we'd moved past."** Scope creep that
+ supersedes an agreed-on base, or revives a direction the maintainers
+ deliberately closed, gets rejected even when the code works. Keep the change
+ to the narrow piece that was actually agreed; offer the rest as a focused
+ follow-up.
+
+The throughline: **verify the claim AND the intent against the codebase before
+writing or merging a fix.** A confirmed reproduction on current `main` plus a
+line-level account of where the fix acts beats a plausible-sounding rationale
+every time. When in doubt about intent, it is cheaper to ask than to ship a
+fix that fights the design.
+
+### The Footprint Ladder (new capability decision)
+
+Each rung adds more permanent surface than the one above. Choose the highest
+(least-footprint) rung that correctly solves the problem:
+
+1. **Extend existing code** — the capability is a variation of something that
+ already exists. Zero new surface.
+2. **CLI command + skill** — manages config/state/infra expressible as shell
+ commands. The agent runs `hermes ` guided by a skill. Zero
+ model-tool footprint. Default choice for subscriptions, scheduled tasks,
+ service setup. Examples: `hermes webhook`, `hermes cron`, `hermes tools`.
+3. **Service-gated tool (`check_fn`)** — needs structured params/returns AND
+ only appears when a prerequisite is configured. Zero footprint otherwise.
+ Examples: Home Assistant tools (gated on token), memory-provider tools.
+4. **Plugin** — third-party/niche/user-specific capability that doesn't ship in
+ core. Lives in `~/.hermes/plugins/` or a pip package, discovered at runtime.
+5. **MCP server (in the catalog)** — if the capability genuinely needs to be a
+ tool (structured I/O the agent invokes) but isn't core-fundamental, prefer
+ building it as an MCP server and adding it to the MCP catalog over growing
+ the core toolset. The agent connects to it through the built-in MCP client;
+ zero permanent core-schema footprint, and it's reusable by any MCP host.
+6. **New core tool** — only when the capability is fundamental, broadly useful
+ to nearly every user, and unreachable via terminal + file (or an MCP server).
+ Examples of correct core tools: terminal, read_file, web_search,
+ browser_navigate.
+
+When 3+ open PRs try to integrate the same *category* of thing (memory
+backends, providers, notifiers), don't merge them one at a time — design an
+ABC + orchestrator, wrap the existing built-in as the first provider, and turn
+the competing PRs into plugins against that interface.
+
+## Development Environment
+
+```bash
+# Prefer .venv; fall back to venv if that's what your checkout has.
+source .venv/bin/activate # or: source venv/bin/activate
+```
+
+`scripts/run_tests.sh` probes `.venv` first, then `venv`, then
+`$HOME/.hermes/hermes-agent/venv` (for worktrees that share a venv with the
+main checkout).
+
+## Project Structure
+
+File counts shift constantly — don't treat the tree below as exhaustive.
+The canonical source is the filesystem. The notes call out the load-bearing
+entry points you'll actually edit.
+
+```
+hermes-agent/
+├── run_agent.py # AIAgent class — core conversation loop (~12k LOC)
+├── model_tools.py # Tool orchestration, discover_builtin_tools(), handle_function_call()
+├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list
+├── cli.py # HermesCLI class — interactive CLI orchestrator (~11k LOC)
+├── hermes_state.py # SessionDB — SQLite session store (FTS5 search)
+├── hermes_constants.py # get_hermes_home(), display_hermes_home() — profile-aware paths
+├── hermes_logging.py # setup_logging() — agent.log / errors.log / gateway.log (profile-aware)
+├── batch_runner.py # Parallel batch processing
+├── agent/ # Agent internals (provider adapters, memory, caching, compression, etc.)
+├── hermes_cli/ # CLI subcommands, setup wizard, plugins loader, skin engine
+├── tools/ # Tool implementations — auto-discovered via tools/registry.py
+│ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
+├── gateway/ # Messaging gateway — run.py + session.py + platforms/
+│ ├── platforms/ # Adapter per platform (telegram, discord, slack, whatsapp,
+│ │ # homeassistant, signal, matrix, mattermost, email, sms,
+│ │ # dingtalk, wecom, weixin, feishu, qqbot, bluebubbles,
+│ │ # yuanbao, webhook, api_server, ...). See ADDING_A_PLATFORM.md.
+│ └── builtin_hooks/ # Extension point for always-registered gateway hooks (none shipped)
+├── plugins/ # Plugin system (see "Plugins" section below)
+│ ├── memory/ # Memory-provider plugins (honcho, mem0, supermemory, ...)
+│ ├── context_engine/ # Context-engine plugins
+│ ├── model-providers/ # Inference backend plugins (openrouter, anthropic, gmi, ...)
+│ ├── kanban/ # Multi-agent board dispatcher + worker plugin
+│ ├── hermes-achievements/ # Gamified achievement tracking
+│ ├── observability/ # Metrics / traces / logs plugin
+│ ├── image_gen/ # Image-generation providers
+│ └── / # disk-cleanup, google_meet, platforms, spotify,
+│ # strike-freedom-cockpit, ...
+├── optional-skills/ # Heavier/niche skills shipped but NOT active by default
+├── skills/ # Built-in skills bundled with the repo
+├── ui-tui/ # Ink (React) terminal UI — `hermes --tui`
+│ └── src/ # entry.tsx, app.tsx, gatewayClient.ts + app/components/hooks/lib
+├── tui_gateway/ # Python JSON-RPC backend for the TUI
+├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
+├── cron/ # Scheduler — jobs.py, scheduler.py
+├── scripts/ # run_tests.sh, release.py, auxiliary scripts
+├── website/ # Docusaurus docs site
+└── tests/ # Pytest suite (~17k tests across ~900 files as of May 2026)
+```
+
+**User config:** `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys only).
+**Logs:** `~/.hermes/logs/` — `agent.log` (INFO+), `errors.log` (WARNING+),
+`gateway.log` when running the gateway. Profile-aware via `get_hermes_home()`.
+Browse with `hermes logs [--follow] [--level ...] [--session ...]`.
+
+## TypeScript Style
+
+Applies to TypeScript across Hermes: desktop, TUI, website, and future TS packages.
+
+- Prefer small nanostores over component state when state is shared, reused, or read by distant UI.
+- Let each feature own its atoms. Chat state belongs near chat, shell state near shell, shared state in `src/store`.
+- Components that render from an atom should use `useStore`. Non-rendering actions should read with `$atom.get()`.
+- Do not pass state through three components when the leaf can subscribe to the atom.
+- Keep persistence beside the atom that owns it.
+- Keep route roots thin. They compose routes and shell; they should not become controllers.
+- No monolithic hooks. A hook should own one narrow job.
+- Prefer colocated action modules over hidden god hooks.
+- If a callback is pure side effect, use the terse void form:
+ `onState={st => void setGatewayState(st)}`.
+- Async UI handlers should make intent explicit:
+ `onClick={() => void save()}`.
+- Prefer interfaces for public props and shared object shapes. Avoid `type X = { ... }` for object props.
+- Extend React primitives for props: `React.ComponentProps<'button'>`, `React.ComponentProps`, `Omit<...>`, `Pick<...>`.
+- Table-driven beats condition ladders when mapping ids, routes, or views.
+- `src/app` owns routes, pages, and page-specific components.
+- `src/store` owns shared atoms.
+- `src/lib` owns shared pure helpers.
+
+## File Dependency Chain
+
+```
+tools/registry.py (no deps — imported by all tool files)
+ ↑
+tools/*.py (each calls registry.register() at import time)
+ ↑
+model_tools.py (imports tools/registry + triggers tool discovery)
+ ↑
+run_agent.py, cli.py, batch_runner.py, environments/
+```
+
+---
+
+## AIAgent Class (run_agent.py)
+
+The real `AIAgent.__init__` takes ~60 parameters (credentials, routing, callbacks,
+session context, budget, credential pool, etc.). The signature below is the
+minimum subset you'll usually touch — read `run_agent.py` for the full list.
+
+```python
+class AIAgent:
+ def __init__(self,
+ base_url: str = None,
+ api_key: str = None,
+ provider: str = None,
+ api_mode: str = None, # "chat_completions" | "codex_responses" | ...
+ model: str = "", # empty → resolved from config/provider later
+ max_iterations: int = 90, # tool-calling iterations (shared with subagents)
+ enabled_toolsets: list = None,
+ disabled_toolsets: list = None,
+ quiet_mode: bool = False,
+ save_trajectories: bool = False,
+ platform: str = None, # "cli", "telegram", etc.
+ session_id: str = None,
+ skip_context_files: bool = False,
+ skip_memory: bool = False,
+ credential_pool=None,
+ # ... plus callbacks, thread/user/chat IDs, iteration_budget, fallback_model,
+ # checkpoints config, prefill_messages, service_tier, reasoning_config, etc.
+ ): ...
+
+ def chat(self, message: str) -> str:
+ """Simple interface — returns final response string."""
+
+ def run_conversation(self, user_message: str, system_message: str = None,
+ conversation_history: list = None, task_id: str = None) -> dict:
+ """Full interface — returns dict with final_response + messages."""
+```
+
+### Agent Loop
+
+The core loop is inside `run_conversation()` — entirely synchronous, with
+interrupt checks, budget tracking, and a one-turn grace call:
+
+```python
+while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) \
+ or self._budget_grace_call:
+ if self._interrupt_requested: break
+ response = client.chat.completions.create(model=model, messages=messages, tools=tool_schemas)
+ if response.tool_calls:
+ for tool_call in response.tool_calls:
+ result = handle_function_call(tool_call.name, tool_call.args, task_id)
+ messages.append(tool_result_message(result))
+ api_call_count += 1
+ else:
+ return response.content
+```
+
+Messages follow OpenAI format: `{"role": "system/user/assistant/tool", ...}`.
+Reasoning content is stored in `assistant_msg["reasoning"]`.
+
+---
+
+## CLI Architecture (cli.py)
+
+- **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete
+- **KawaiiSpinner** (`agent/display.py`) — animated faces during API calls, `┊` activity feed for tool results
+- `load_cli_config()` in cli.py merges hardcoded defaults + user config YAML
+- **Skin engine** (`hermes_cli/skin_engine.py`) — data-driven CLI theming; initialized from `display.skin` config key at startup; skins customize banner colors, spinner faces/verbs/wings, tool prefix, response box, branding text
+- `process_command()` is a method on `HermesCLI` — dispatches on canonical command name resolved via `resolve_command()` from the central registry
+- Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching
+
+### Slash Command Registry (`hermes_cli/commands.py`)
+
+All slash commands are defined in a central `COMMAND_REGISTRY` list of `CommandDef` objects. Every downstream consumer derives from this registry automatically:
+
+- **CLI** — `process_command()` resolves aliases via `resolve_command()`, dispatches on canonical name
+- **Gateway** — `GATEWAY_KNOWN_COMMANDS` frozenset for hook emission, `resolve_command()` for dispatch
+- **Gateway help** — `gateway_help_lines()` generates `/help` output
+- **Telegram** — `telegram_bot_commands()` generates the BotCommand menu
+- **Slack** — `slack_subcommand_map()` generates `/hermes` subcommand routing
+- **Autocomplete** — `COMMANDS` flat dict feeds `SlashCommandCompleter`
+- **CLI help** — `COMMANDS_BY_CATEGORY` dict feeds `show_help()`
+
+### Adding a Slash Command
+
+1. Add a `CommandDef` entry to `COMMAND_REGISTRY` in `hermes_cli/commands.py`:
+```python
+CommandDef("mycommand", "Description of what it does", "Session",
+ aliases=("mc",), args_hint="[arg]"),
+```
+2. Add handler in `HermesCLI.process_command()` in `cli.py`:
+```python
+elif canonical == "mycommand":
+ self._handle_mycommand(cmd_original)
+```
+3. If the command is available in the gateway, add a handler in `gateway/run.py`:
+```python
+if canonical == "mycommand":
+ return await self._handle_mycommand(event)
+```
+4. For persistent settings, use `save_config_value()` in `cli.py`
+
+**CommandDef fields:**
+- `name` — canonical name without slash (e.g. `"background"`)
+- `description` — human-readable description
+- `category` — one of `"Session"`, `"Configuration"`, `"Tools & Skills"`, `"Info"`, `"Exit"`
+- `aliases` — tuple of alternative names (e.g. `("bg",)`)
+- `args_hint` — argument placeholder shown in help (e.g. `""`, `"[name]"`)
+- `cli_only` — only available in the interactive CLI
+- `gateway_only` — only available in messaging platforms
+- `gateway_config_gate` — config dotpath (e.g. `"display.tool_progress_command"`); when set on a `cli_only` command, the command becomes available in the gateway if the config value is truthy. `GATEWAY_KNOWN_COMMANDS` always includes config-gated commands so the gateway can dispatch them; help/menus only show them when the gate is open.
+
+**Adding an alias** requires only adding it to the `aliases` tuple on the existing `CommandDef`. No other file changes needed — dispatch, help text, Telegram menu, Slack mapping, and autocomplete all update automatically.
+
+---
+
+## TUI Architecture (ui-tui + tui_gateway)
+
+The TUI is a full replacement for the classic (prompt_toolkit) CLI, activated via `hermes --tui` or `HERMES_TUI=1`.
+
+### Process Model
+
+```
+hermes --tui
+ └─ Node (Ink) ──stdio JSON-RPC── Python (tui_gateway)
+ │ └─ AIAgent + tools + sessions
+ └─ renders transcript, composer, prompts, activity
+```
+
+TypeScript owns the screen. Python owns sessions, tools, model calls, and slash command logic.
+
+### Transport
+
+Newline-delimited JSON-RPC over stdio. Requests from Ink, events from Python. See `tui_gateway/server.py` for the full method/event catalog.
+
+### Key Surfaces
+
+| Surface | Ink component | Gateway method |
+|---------|---------------|----------------|
+| Chat streaming | `app.tsx` + `messageLine.tsx` | `prompt.submit` → `message.delta/complete` |
+| Tool activity | `thinking.tsx` | `tool.start/progress/complete` |
+| Approvals | `prompts.tsx` | `approval.respond` ← `approval.request` |
+| Clarify/sudo/secret | `prompts.tsx`, `maskedPrompt.tsx` | `clarify/sudo/secret.respond` |
+| Session picker | `sessionPicker.tsx` | `session.list/resume` |
+| Slash commands | Local handler + fallthrough | `slash.exec` → `_SlashWorker`, `command.dispatch` |
+| Completions | `useCompletion` hook | `complete.slash`, `complete.path` |
+| Theming | `theme.ts` + `branding.tsx` | `gateway.ready` with skin data |
+
+### Slash Command Flow
+
+1. Built-in client commands (`/help`, `/quit`, `/clear`, `/resume`, `/copy`, `/paste`, etc.) handled locally in `app.tsx`
+2. Everything else → `slash.exec` (runs in persistent `_SlashWorker` subprocess) → `command.dispatch` fallback
+
+### Dev Commands
+
+```bash
+cd ui-tui
+npm install # first time
+npm run dev # watch mode (rebuilds hermes-ink + tsx --watch)
+npm start # production
+npm run build # full build (hermes-ink + tsc)
+npm run typecheck # typecheck only (tsc --noEmit)
+npm run lint # eslint
+npm run fmt # prettier
+npm test # vitest
+```
+
+### TUI in the Dashboard (`hermes dashboard` → `/chat`)
+
+The dashboard embeds the real `hermes --tui` — **not** a rewrite. See `hermes_cli/pty_bridge.py` + the `@app.websocket("/api/pty")` endpoint in `hermes_cli/web_server.py`.
+
+- Browser loads `web/src/pages/ChatPage.tsx`, which mounts xterm.js's `Terminal` with the WebGL renderer, `@xterm/addon-fit` for container-driven resize, and `@xterm/addon-unicode11` for modern wide-character widths.
+- `/api/pty?token=…` upgrades to a WebSocket; auth uses the same ephemeral `_SESSION_TOKEN` as REST, via query param (browsers can't set `Authorization` on WS upgrade).
+- The server spawns whatever `hermes --tui` would spawn, through `ptyprocess` (POSIX PTY — WSL works, native Windows does not).
+- Frames: raw PTY bytes each direction; resize via `\x1b[RESIZE:;]` intercepted on the server and applied with `TIOCSWINSZ`.
+
+**Do not re-implement the primary chat experience in React.** The main transcript, composer/input flow (including slash-command behavior), and PTY-backed terminal belong to the embedded `hermes --tui` — anything new you add to Ink shows up in the dashboard automatically. If you find yourself rebuilding the transcript or composer for the dashboard, stop and extend Ink instead.
+
+**Structured React UI around the TUI is allowed when it is not a second chat surface.** Sidebar widgets, inspectors, summaries, status panels, and similar supporting views (e.g. `ChatSidebar`, `ModelPickerDialog`, `ToolCall`) are fine when they complement the embedded TUI rather than replacing the transcript / composer / terminal. Keep their state independent of the PTY child's session and surface their failures non-destructively so the terminal pane keeps working unimpaired.
+
+### Electron Desktop Chat App (`apps/desktop/`)
+
+A **separate** chat surface from both the classic CLI and the dashboard's embedded TUI. It is an Electron + React + nanostore renderer (`@assistant-ui/react`) that talks to a `tui_gateway` backend over JSON-RPC (`requestGateway(method, params)`). The WebSocket/JSON-RPC transport lives in the framework-agnostic `apps/shared` package (`@hermes/shared` — `JsonRpcGatewayClient` + WS URL helpers), which the web dashboard (`web/`) also consumes; **desktop has no build/runtime dependency on the dashboard frontend** — it spawns a headless `hermes serve` backend server (the same gateway `dashboard` serves, minus the browser UI entirely: `serve` sets `headless_backend=True`, so `cmd_dashboard` skips `_build_web_ui` AND exports `HERMES_SERVE_HEADLESS=1` so `mount_spa()` disables the SPA even if a stray `web_dist/` exists — only the JSON-RPC/WS/API surface is reachable). `dashboard` and `serve` share `cmd_dashboard`/`start_server` but are independent surfaces — neither launches the other. The one exception is a backward-compat *fallback*: `serve` is newer, so the desktop spawn (`electron/backend-command.ts` + `backendSupportsServe()` in `electron/main.ts`) detects whether the resolved runtime registers `serve` and, only when it does not (an older managed install / PATH `hermes` the app hasn't updated yet), rewrites the argv to the legacy `dashboard --no-open`. Without that, a new app against an un-upgraded runtime would crash on an unknown subcommand and brick every mid-upgrade user. It does NOT embed `hermes --tui` — it has its own composer, transcript, and slash-command pipeline. For scoped Desktop architecture, state, resolver, transport, and testing rules, read `apps/desktop/AGENTS.md`.
+
+**Slash commands in the desktop app are curated client-side, then dispatched to the backend.** The pipeline:
+
+- **Backend already provides everything.** `tui_gateway/server.py` `commands.catalog` (empty-query list) and `complete.slash` (typed-query completions) both include built-in commands, user `quick_commands`, AND skill-derived commands (`scan_skill_commands()` / `get_skill_commands()`). The desktop app does not need a new RPC to see skills.
+- **The renderer curates via `apps/desktop/src/lib/desktop-slash-commands.ts`.** This is the load-bearing file. It holds `DESKTOP_COMMAND_SPECS` (the built-ins and their Desktop surfaces) plus `NO_DESKTOP_SURFACE` block-lists for terminal-only / messaging-only / picker-owned / settings-owned / advanced commands that should NOT clutter the desktop popover.
+ - `isDesktopSlashCommand(name)` — gates **execution**. Returns true for built-ins AND for any non-built-in (skill / quick command), so typed extension commands run.
+ - `isDesktopSlashSuggestion(name)` — gates **discovery/completion**. Used by BOTH completion paths in `app/chat/composer/hooks/use-slash-completions.ts` (empty-query catalog filter + typed-query `complete.slash` filter) and by `filterDesktopCommandsCatalog`.
+ - `isDesktopSlashExtensionCommand(name)` — true when the command is NOT a known Hermes built-in (i.e. a skill or user quick command). Both suggestion and catalog-filter paths allow extensions through so skill commands surface in the palette. (Added when fixing "skill commands missing from the desktop slash palette" — the curated allow-list was silently dropping every skill/quick command from completions even though they executed fine when typed.)
+- **Dispatch** lives in `app/session/hooks/use-prompt-actions/slash.ts` (`runSlash`): built-ins that the desktop owns (`/skin`, `/help`, `/new`, …) are handled locally or via `commands.catalog`; everything else goes to `slash.exec`, falling back to `command.dispatch` (which the gateway resolves into skill / alias / exec directives). A skill command resolves to `{type: "skill", message}` and is submitted as a normal prompt.
+
+**Rule:** the desktop slash palette's curation is about hiding noise (terminal-only / messaging-only built-ins), NOT about hiding user-activated extensions. Skill commands and `quick_commands` are extensions the backend surfaces — they belong in completions. If you tighten `desktop-slash-commands.ts`, keep `isDesktopSlashExtensionCommand` flowing into both the suggestion and catalog-filter paths. Tests: from `apps/desktop`, run `npx vitest run src/lib/desktop-slash-commands.test.ts` (workspace dependencies are installed at the repo root).
+
+---
+
+## Adding New Tools
+
+Before adding any tool, settle the footprint question first (see "The
+Footprint Ladder" in the Contribution Rubric): most capabilities should NOT
+be core tools. For custom or local-only tools, do **not** edit Hermes core.
+Use the plugin route instead: create `~/.hermes/plugins//plugin.yaml`
+and `~/.hermes/plugins//__init__.py`, then register tools with
+`ctx.register_tool(...)`. Plugin toolsets are discovered automatically and can be
+enabled or disabled without touching `tools/` or `toolsets.py`.
+
+Use the built-in route below only when the user is explicitly contributing a new
+core Hermes tool that should ship in the base system.
+
+Built-in/core tools require changes in **2 files**:
+
+**1. Create `tools/your_tool.py`:**
+```python
+import json, os
+from tools.registry import registry
+
+def check_requirements() -> bool:
+ return bool(os.getenv("EXAMPLE_API_KEY"))
+
+def example_tool(param: str, task_id: str = None) -> str:
+ return json.dumps({"success": True, "data": "..."})
+
+registry.register(
+ name="example_tool",
+ toolset="example",
+ schema={"name": "example_tool", "description": "...", "parameters": {...}},
+ handler=lambda args, **kw: example_tool(param=args.get("param", ""), task_id=kw.get("task_id")),
+ check_fn=check_requirements,
+ requires_env=["EXAMPLE_API_KEY"],
+)
+```
+
+**2. Add to `toolsets.py`** — either `_HERMES_CORE_TOOLS` (all platforms) or a new toolset. **This step is required:** auto-discovery imports the tool and registers its schema, but the tool is only *exposed to an agent* if its name appears in a toolset. `_HERMES_CORE_TOOLS` is not dead code — it's the default bundle every platform's base toolset inherits from.
+
+Auto-discovery: any `tools/*.py` file with a top-level `registry.register()` call is imported automatically — no manual import list to maintain. Wiring into a toolset is still a deliberate, manual step.
+
+The registry handles schema collection, dispatch, availability checking, and error wrapping. All handlers MUST return a JSON string.
+
+**Path references in tool schemas**: If the schema description mentions file paths (e.g. default output directories), use `display_hermes_home()` to make them profile-aware. The schema is generated at import time, which is after `_apply_profile_override()` sets `HERMES_HOME`.
+
+**State files**: If a tool stores persistent state (caches, logs, checkpoints), use `get_hermes_home()` for the base directory — never `Path.home() / ".hermes"`. This ensures each profile gets its own state.
+
+**Agent-level tools** (todo, memory): intercepted by `run_agent.py` before `handle_function_call()`. See `tools/todo_tool.py` for the pattern.
+
+---
+
+## Dependency Pinning Policy
+
+All dependencies must have upper bounds to limit supply-chain attack surface.
+This policy was established after the litellm compromise (PR #2796, #2810) and
+reinforced after the Mini Shai-Hulud worm campaign (May 2026).
+
+| Source type | Treatment | Example |
+|---|---|---|
+| PyPI package | `>=floor,=0.28.1,<1"` |
+| Git URL | Commit SHA | `git+https://...@<40-char-sha>` |
+| GitHub Actions | Commit SHA + comment | `uses: actions/checkout@ # v4` |
+| CI-only pip | `==exact` | `pyyaml==6.0.2` |
+
+**When adding a new dependency to `pyproject.toml`:**
+1. Pin to `>=current_version,=1.5.0,<2`).
+2. For pre-1.0 packages, use `<0.(current_minor + 2)` (e.g. `>=0.29,<0.32`).
+3. Never commit a bare `>=X.Y.Z` without a ceiling — CI and reviewers will reject it.
+4. Run `uv lock` to regenerate `uv.lock` with hashes.
+
+Reference: #2810 (bounds pass), #9801 (SHA pinning + audit CI).
+
+---
+
+## Adding Configuration
+
+### config.yaml options:
+1. Add to `DEFAULT_CONFIG` in `hermes_cli/config.py`
+2. Bump `_config_version` (check the current value at the top of `DEFAULT_CONFIG`)
+ ONLY if you need to actively migrate/transform existing user config
+ (renaming keys, changing structure). Adding a new key to an existing
+ section is handled automatically by the deep-merge and does NOT require
+ a version bump.
+
+### Top-level `config.yaml` sections (non-exhaustive):
+
+`model`, `agent`, `terminal`, `compression`, `display`, `stt`, `tts`,
+`memory`, `security`, `delegation`, `smart_model_routing`, `checkpoints`,
+`auxiliary`, `curator`, `skills`, `gateway`, `logging`, `cron`, `profiles`,
+`plugins`, `honcho`.
+
+`auxiliary` holds per-task overrides for side-LLM work (curator, vision,
+embedding, title generation, session_search, etc.) — each task can pin
+its own provider/model/base_url/max_tokens/reasoning_effort. See
+`agent/auxiliary_client.py::_resolve_auto` for resolution order.
+
+`curator` holds the background skill-maintenance config —
+`enabled`, `interval_hours`, `min_idle_hours`, `stale_after_days`,
+`archive_after_days`, `backup` (nested).
+
+### .env variables (SECRETS ONLY — API keys, tokens, passwords):
+1. Add to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` with metadata:
+```python
+"NEW_API_KEY": {
+ "description": "What it's for",
+ "prompt": "Display name",
+ "url": "https://...",
+ "password": True,
+ "category": "tool", # provider, tool, messaging, setting
+},
+```
+
+Non-secret settings (timeouts, thresholds, feature flags, paths, display
+preferences) belong in `config.yaml`, not `.env`. If internal code needs an
+env var mirror for backward compatibility, bridge it from `config.yaml` to
+the env var in code (see `gateway_timeout`, `terminal.cwd` → `TERMINAL_CWD`).
+
+### Config loaders (three paths — know which one you're in):
+
+| Loader | Used by | Location |
+|--------|---------|----------|
+| `load_cli_config()` | CLI mode | `cli.py` — merges CLI-specific defaults + user YAML |
+| `load_config()` | `hermes tools`, `hermes setup`, most CLI subcommands | `hermes_cli/config.py` — merges `DEFAULT_CONFIG` + user YAML |
+| Direct YAML load | Gateway runtime | `gateway/run.py` + `gateway/config.py` — reads user YAML raw |
+
+If you add a new key and the CLI sees it but the gateway doesn't (or vice
+versa), you're on the wrong loader. Check `DEFAULT_CONFIG` coverage.
+
+### Working directory:
+- **CLI** — uses the process's current directory (`os.getcwd()`).
+- **Messaging** — uses `terminal.cwd` from `config.yaml`. The gateway bridges this
+ to the `TERMINAL_CWD` env var for child tools. **`MESSAGING_CWD` has been
+ removed** — the config loader prints a deprecation warning if it's set in
+ `.env`. Same for `TERMINAL_CWD` in `.env`; the canonical setting is
+ `terminal.cwd` in `config.yaml`.
+
+---
+
+## Skin/Theme System
+
+The skin engine (`hermes_cli/skin_engine.py`) provides data-driven CLI visual customization. Skins are **pure data** — no code changes needed to add a new skin.
+
+### Architecture
+
+```
+hermes_cli/skin_engine.py # SkinConfig dataclass, built-in skins, YAML loader
+~/.hermes/skins/*.yaml # User-installed custom skins (drop-in)
+```
+
+- `init_skin_from_config()` — called at CLI startup, reads `display.skin` from config
+- `get_active_skin()` — returns cached `SkinConfig` for the current skin
+- `set_active_skin(name)` — switches skin at runtime (used by `/skin` command)
+- `load_skin(name)` — loads from user skins first, then built-ins, then falls back to default
+- Missing skin values inherit from the `default` skin automatically
+
+### What skins customize
+
+| Element | Skin Key | Used By |
+|---------|----------|---------|
+| Banner panel border | `colors.banner_border` | `banner.py` |
+| Banner panel title | `colors.banner_title` | `banner.py` |
+| Banner section headers | `colors.banner_accent` | `banner.py` |
+| Banner dim text | `colors.banner_dim` | `banner.py` |
+| Banner body text | `colors.banner_text` | `banner.py` |
+| Response box border | `colors.response_border` | `cli.py` |
+| Spinner faces (waiting) | `spinner.waiting_faces` | `display.py` |
+| Spinner faces (thinking) | `spinner.thinking_faces` | `display.py` |
+| Spinner verbs | `spinner.thinking_verbs` | `display.py` |
+| Spinner wings (optional) | `spinner.wings` | `display.py` |
+| Tool output prefix | `tool_prefix` | `display.py` |
+| Per-tool emojis | `tool_emojis` | `display.py` → `get_tool_emoji()` |
+| Agent name | `branding.agent_name` | `banner.py`, `cli.py` |
+| Welcome message | `branding.welcome` | `cli.py` |
+| Response box label | `branding.response_label` | `cli.py` |
+| Prompt symbol | `branding.prompt_symbol` | `cli.py` |
+
+### Built-in skins
+
+- `default` — Classic Hermes gold/kawaii (the current look)
+- `ares` — Crimson/bronze war-god theme with custom spinner wings
+- `mono` — Clean grayscale monochrome
+- `slate` — Cool blue developer-focused theme
+
+### Adding a built-in skin
+
+Add to `_BUILTIN_SKINS` dict in `hermes_cli/skin_engine.py`:
+
+```python
+"mytheme": {
+ "name": "mytheme",
+ "description": "Short description",
+ "colors": { ... },
+ "spinner": { ... },
+ "branding": { ... },
+ "tool_prefix": "┊",
+},
+```
+
+### User skins (YAML)
+
+Users create `~/.hermes/skins/.yaml`:
+
+```yaml
+name: cyberpunk
+description: Neon-soaked terminal theme
+
+colors:
+ banner_border: "#FF00FF"
+ banner_title: "#00FFFF"
+ banner_accent: "#FF1493"
+
+spinner:
+ thinking_verbs: ["jacking in", "decrypting", "uploading"]
+ wings:
+ - ["⟨⚡", "⚡⟩"]
+
+branding:
+ agent_name: "Cyber Agent"
+ response_label: " ⚡ Cyber "
+
+tool_prefix: "▏"
+```
+
+Activate with `/skin cyberpunk` or `display.skin: cyberpunk` in config.yaml.
+
+---
+
+## Plugins
+
+Hermes has two plugin surfaces. Both live under `plugins/` in the repo so
+repo-shipped plugins can be discovered alongside user-installed ones in
+`~/.hermes/plugins/` and pip-installed entry points.
+
+### General plugins (`hermes_cli/plugins.py` + `plugins//`)
+
+`PluginManager` discovers plugins from `~/.hermes/plugins/`, `./.hermes/plugins/`,
+and pip entry points. Each plugin exposes a `register(ctx)` function that
+can:
+
+- Register Python-callback lifecycle hooks:
+ `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
+ `on_session_start`, `on_session_end`
+- Register new tools via `ctx.register_tool(...)`
+- Register CLI subcommands via `ctx.register_cli_command(...)` — the
+ plugin's argparse tree is wired into `hermes` at startup so
+ `hermes ` works with no change to `main.py`
+
+Hooks are invoked from `model_tools.py` (pre/post tool) and `run_agent.py`
+(lifecycle). **Discovery timing pitfall:** `discover_plugins()` only runs
+as a side effect of importing `model_tools.py`. Code paths that read plugin
+state without importing `model_tools.py` first must call `discover_plugins()`
+explicitly (it's idempotent).
+
+### Memory-provider plugins (`plugins/memory//`)
+
+Separate discovery system for pluggable memory backends. Current built-in
+providers include **honcho, mem0, supermemory, byterover, hindsight,
+holographic, openviking, retaindb**.
+
+Each provider implements the `MemoryProvider` ABC (see `agent/memory_provider.py`)
+and is orchestrated by `agent/memory_manager.py`. Lifecycle hooks include
+`sync_turn(turn_messages)`, `prefetch(query)`, `shutdown()`, and optional
+`post_setup(hermes_home, config)` for setup-wizard integration.
+
+**CLI commands via `plugins/memory//cli.py`:** if a memory plugin
+defines `register_cli(subparser)`, `discover_plugin_cli_commands()` finds
+it at argparse setup time and wires it into `hermes `. The
+framework only exposes CLI commands for the **currently active** memory
+provider (read from `memory.provider` in config.yaml), so disabled
+providers don't clutter `hermes --help`.
+
+**Rule (Teknium, May 2026):** plugins MUST NOT modify core files
+(`run_agent.py`, `cli.py`, `gateway/run.py`, `hermes_cli/main.py`, etc.).
+If a plugin needs a capability the framework doesn't expose, expand the
+generic plugin surface (new hook, new ctx method) — never hardcode
+plugin-specific logic into core. PR #5295 removed 95 lines of hardcoded
+honcho argparse from `main.py` for exactly this reason.
+
+**No new in-tree memory providers (policy, May 2026):** the set of
+built-in memory providers under `plugins/memory/` is closed. New memory
+backends must ship as **standalone plugin repos** that users install
+into `~/.hermes/plugins/` (or via pip entry points) — they implement
+the same `MemoryProvider` ABC, register through the same discovery
+path, and integrate via `hermes memory setup` / `post_setup()` without
+landing in this tree. PRs that add a new directory under
+`plugins/memory/` will be closed with a pointer to publish the
+provider as its own repo. Existing in-tree providers stay; bug fixes
+to them are welcome.
+
+**No new third-party-product plugins in-tree (policy, June 2026):** the
+same rule applies beyond memory providers. Plugins that integrate
+someone else's product or project — observability/metrics backends,
+vendor SaaS connectors, analytics dashboards, paid-service tie-ins —
+must ship as **standalone plugin repos** that users install into
+`~/.hermes/plugins/` (or via pip entry points). They register through
+the existing plugin discovery path and use the ABCs/hooks/ctx surface
+we expose; nothing special is needed in core. The reason is
+maintenance load: every product we absorb into the tree becomes our
+burden to keep working against a fast-moving core, for a backend we
+don't own. Promote standalone plugins in the Nous Research Discord
+(`#plugins-skills-and-skins`). PRs that add such a directory under
+`plugins/` are closed with a pointer to publish it as its own repo —
+this is a coupling decision, not a quality judgment. (The
+`observability/`, `kanban/`, `disk-cleanup/`, etc. directories already
+in the tree are existing precedent, not an invitation to add more
+third-party-product plugins alongside them.)
+
+### Model-provider plugins (`plugins/model-providers//`)
+
+Every inference backend (openrouter, anthropic, gmi, deepseek, nvidia, …)
+ships as a plugin here. Each plugin's `__init__.py` calls
+`providers.register_provider(ProviderProfile(...))` at module load.
+`providers/__init__.py._discover_providers()` is a **lazy, separate
+discovery system** — scanned on first `get_provider_profile()` or
+`list_providers()` call, NOT by the general PluginManager.
+
+Scan order:
+1. Bundled: `/plugins/model-providers//`
+2. User: `$HERMES_HOME/plugins/model-providers//`
+3. Legacy: `/providers/.py` (back-compat)
+
+User plugins of the same name override bundled ones — `register_provider()`
+is last-writer-wins. This lets third parties swap out any built-in
+profile without a repo patch.
+
+The general PluginManager records `kind: model-provider` manifests but does
+NOT import them (would double-instantiate `ProviderProfile`). Plugins
+without an explicit `kind:` get auto-coerced via a source-text heuristic
+(`register_provider` + `ProviderProfile` in `__init__.py`).
+
+Full authoring guide: `website/docs/developer-guide/model-provider-plugin.md`.
+
+### Dashboard / context-engine / image-gen plugin directories
+
+`plugins/context_engine/`, `plugins/image_gen/`, etc. follow the same
+pattern (ABC + orchestrator + per-plugin directory). Context engines
+plug into `agent/context_engine.py`; image-gen providers into
+`agent/image_gen_provider.py`. Reference / docs-companion plugins
+(`example-dashboard`, `strike-freedom-cockpit`, `plugin-llm-example`,
+`plugin-llm-async-example`) live in the
+[`hermes-example-plugins`](https://github.com/NousResearch/hermes-example-plugins)
+companion repo, not in this tree.
+
+---
+
+## Skills
+
+Two parallel surfaces:
+
+- **`skills/`** — built-in skills shipped and loadable by default.
+ Organized by category directories (e.g. `skills/github/`, `skills/mlops/`).
+- **`optional-skills/`** — heavier or niche skills shipped with the repo but
+ NOT active by default. Installed explicitly via
+ `hermes skills install official//`. Adapter lives in
+ `tools/skills_hub.py` (`OptionalSkillSource`). Categories include
+ `autonomous-ai-agents`, `blockchain`, `communication`, `creative`,
+ `devops`, `email`, `health`, `mcp`, `migration`, `mlops`, `productivity`,
+ `research`, `security`, `web-development`.
+
+When reviewing skill PRs, check which directory they target — heavy-dep or
+niche skills belong in `optional-skills/`.
+
+### SKILL.md frontmatter
+
+Standard fields: `name`, `description`, `version`, `author`, `license`,
+`platforms` (OS-gating list: `[macos]`, `[linux, macos]`, ...),
+`metadata.hermes.tags`, `metadata.hermes.category`,
+`metadata.hermes.related_skills`, `metadata.hermes.config` (config.yaml
+settings the skill needs — stored under `skills.config.`, prompted
+during setup, injected at load time).
+
+Top-level `tags:` and `category:` are also accepted and mirrored from
+`metadata.hermes.*` by the loader.
+
+### Skill authoring standards (HARDLINE)
+
+Every new or modernized skill — bundled, optional, or contributed —
+must meet these standards before merge. Reviewers reject PRs that
+violate them.
+
+1. **`description` ≤ 60 characters, one sentence, ends with a period.**
+ Long descriptions bloat skill listings and dilute the model's
+ attention when many skills are loaded. State the capability, not
+ the implementation. No marketing words ("powerful",
+ "comprehensive", "seamless", "advanced"). Don't repeat the skill
+ name. Verify with:
+ ```python
+ import re, pathlib
+ m = re.search(r'^description: (.*)$',
+ pathlib.Path('skills///SKILL.md').read_text(),
+ re.MULTILINE)
+ assert len(m.group(1)) <= 60, len(m.group(1))
+ ```
+
+2. **Tools referenced in SKILL.md prose must be native Hermes tools or
+ MCP servers the skill explicitly expects.** When the skill needs a
+ capability, point at the proper tool by name in backticks
+ (`` `terminal` ``, `` `web_extract` ``, `` `read_file` ``,
+ `` `patch` ``, `` `search_files` ``, `` `vision_analyze` ``,
+ `` `browser_navigate` ``, `` `delegate_task` ``, etc.). Do NOT
+ name shell utilities the agent already has wrapped — `grep` →
+ `search_files`, `cat`/`head`/`tail` → `read_file`, `sed`/`awk` →
+ `patch`, `find`/`ls` → `search_files target='files'`. If the skill
+ depends on an MCP server, name the MCP server and document the
+ expected setup in `## Prerequisites`. Anything else (third-party
+ CLIs, shell pipelines, etc.) is fair game inside script files but
+ should not be the headline interaction surface in the prose.
+
+3. **`platforms:` gating audited against actual script imports.**
+ Skills that use POSIX-only primitives (`fcntl`, `termios`,
+ `os.setsid`, `os.kill(pid, 0)` for liveness, `/proc`, `/tmp`
+ hardcoded, `signal.SIGKILL`, bash heredocs, `osascript`, `apt`,
+ `systemctl`) must declare their supported platforms. Default
+ posture: try to fix it cross-platform first — `tempfile.gettempdir`,
+ `pathlib.Path`, `psutil.pid_exists`, Python-level filtering instead
+ of `grep`. Gate to a narrower set only when the dependency is
+ genuinely platform-bound.
+
+4. **`author` credits the human contributor first.** For external
+ contributions, the contributor's real name + GitHub handle goes
+ first; "Hermes Agent" is the secondary collaborator. If the
+ contributor's commit shows "Hermes Agent" as author (because they
+ used Hermes to draft the skill), replace it with their actual name
+ — credit the human, not the tool.
+
+5. **SKILL.md body uses the modern section order.** `# Skill`
+ title, 2-3 sentence intro stating what it does and doesn't do,
+ `## When to Use`, `## Prerequisites`, `## How to Run`,
+ `## Quick Reference`, `## Procedure`, `## Pitfalls`,
+ `## Verification`. Target ~200 lines for a complex skill,
+ ~100 lines for a simple one. Cut redundant intro fluff, marketing
+ prose, and re-explanations of env vars already in
+ `## Prerequisites`.
+
+6. **Scripts go in `scripts/`, references in `references/`,
+ templates in `templates/`.** Don't expect the model to inline-write
+ parsers, XML walkers, or non-trivial logic every call — ship a
+ helper script. Reference it from SKILL.md by path relative to the
+ skill directory.
+
+7. **Tests live at `tests/skills/test__skill.py`** and use only
+ stdlib + pytest + `unittest.mock`. No live network calls. Run via
+ `scripts/run_tests.sh tests/skills/test__skill.py -q`.
+
+8. **`.env.example` additions are isolated to a clearly delimited
+ block.** Don't touch the surrounding file — contributor-supplied
+ `.env.example` versions are usually stale and edits outside the
+ skill's own block must be dropped during salvage.
+
+The full salvage / modernization checklist for external skill PRs
+lives in the `hermes-agent-dev` skill at
+`references/new-skill-pr-salvage.md` — load it before polishing
+contributor skill PRs.
+
+---
+
+## Toolsets
+
+All toolsets are defined in `toolsets.py` as a single `TOOLSETS` dict.
+Each platform's adapter picks a base toolset (e.g. Telegram uses
+`"messaging"`); `_HERMES_CORE_TOOLS` is the default bundle most
+platforms inherit from.
+
+Current toolset keys: `browser`, `clarify`, `code_execution`, `cronjob`,
+`debugging`, `delegation`, `discord`, `discord_admin`, `feishu_doc`,
+`feishu_drive`, `file`, `homeassistant`, `image_gen`, `kanban`, `memory`,
+`messaging`, `moa`, `rl`, `safe`, `search`, `session_search`, `skills`,
+`spotify`, `terminal`, `todo`, `tts`, `video`, `vision`, `web`, `yuanbao`.
+
+Enable/disable per platform via `hermes tools` (the curses UI) or the
+`tools..enabled` / `tools..disabled` lists in
+`config.yaml`.
+
+---
+
+## Delegation (`delegate_task`)
+
+`tools/delegate_tool.py` spawns a subagent with an isolated
+context + terminal session. By default the parent waits for the
+child's summary before continuing its own loop. With `background=true`,
+Hermes returns a delegation id immediately and the result re-enters the
+conversation later through the async-delegation completion queue.
+
+Two shapes:
+
+- **Single:** pass `goal` (+ optional `context`, `toolsets`).
+- **Batch (parallel):** pass `tasks: [...]` — each gets its own subagent
+ running concurrently. Concurrency is capped by
+ `delegation.max_concurrent_children` (default 3).
+
+Roles:
+
+- `role="leaf"` (default) — focused worker. Cannot call `delegate_task`,
+ `clarify`, `memory`, `send_message`, `execute_code`.
+- `role="orchestrator"` — retains `delegate_task` so it can spawn its
+ own workers. Gated by `delegation.orchestrator_enabled` (default true)
+ and bounded by `delegation.max_spawn_depth` (default 2).
+
+Key config knobs (under `delegation:` in `config.yaml`):
+`max_concurrent_children`, `max_spawn_depth`, `child_timeout_seconds`,
+`orchestrator_enabled`, `subagent_auto_approve`, `inherit_mcp_toolsets`,
+`max_iterations`.
+
+Durability rule: background `delegate_task` is detached from the current
+turn but still process-local. For work that must survive process restart, use
+`cronjob` or `terminal(background=True, notify_on_complete=True)` instead.
+
+---
+
+## Curator (skill lifecycle)
+
+Background skill-maintenance system that tracks usage on agent-created
+skills and auto-archives stale ones. Users never lose skills; archives
+go to `~/.hermes/skills/.archive/` and are restorable.
+
+- **Core:** `agent/curator.py` (review loop, auto-transitions, LLM review
+ prompt) + `agent/curator_backup.py` (pre-run tar.gz snapshots).
+- **CLI:** `hermes_cli/curator.py` wires `hermes curator ` where
+ verbs are: `status`, `run`, `pause`, `resume`, `pin`, `unpin`,
+ `archive`, `restore`, `prune`, `backup`, `rollback`.
+- **Telemetry:** `tools/skill_usage.py` owns the sidecar
+ `~/.hermes/skills/.usage.json` — per-skill `use_count`, `view_count`,
+ `patch_count`, `last_activity_at`, `state` (active / stale /
+ archived), `pinned`.
+
+Invariants:
+- Curator only touches skills with `created_by: "agent"` provenance —
+ bundled + hub-installed skills are off-limits.
+- Never deletes; max destructive action is archive.
+- Pinned skills are exempt from every auto-transition and from the
+ LLM review pass.
+- `skill_manage(action="delete")` refuses pinned skills; patch/edit/
+ write_file/remove_file go through so the agent can keep improving
+ pinned skills.
+
+Config section (`curator:` in `config.yaml`):
+`enabled`, `interval_hours`, `min_idle_hours`, `stale_after_days`,
+`archive_after_days`, `backup.*`.
+
+Full user-facing docs: `website/docs/user-guide/features/curator.md`.
+
+---
+
+## Cron (scheduled jobs)
+
+`cron/jobs.py` (job store) + `cron/scheduler.py` (tick loop). Agents
+schedule jobs via the `cronjob` tool; users via `hermes cron `
+(`list`, `add`, `edit`, `pause`, `resume`, `run`, `remove`) or the
+`/cron` slash command.
+
+Supported schedule formats:
+- Duration: `"30m"`, `"2h"`, `"1d"`
+- "every" phrase: `"every 2h"`, `"every monday 9am"`
+- 5-field cron expression: `"0 9 * * *"`
+- ISO timestamp (one-shot): `"2026-06-01T09:00:00Z"`
+
+Per-job fields include `skills` (load specific skills), `model` /
+`provider` overrides, `script` (pre-run data-collection script whose
+stdout is injected into the prompt; `no_agent=True` turns the script
+into the entire job), `context_from` (chain job A's last output into
+job B's prompt), `workdir` (run in a specific directory with its
+`AGENTS.md`/`CLAUDE.md` loaded), and multi-platform delivery.
+
+Hardening invariants:
+- **3-minute hard interrupt** on cron sessions — runaway agent loops
+ cannot monopolize the scheduler.
+- Catchup window: half the job's period, clamped to 120s–2h.
+- Grace window: 120s for one-shot jobs whose fire time was missed.
+- File lock at `~/.hermes/cron/.tick.lock` prevents duplicate ticks
+ across processes.
+- Cron sessions pass `skip_memory=True` by default; memory providers
+ intentionally do not run during cron.
+
+Cron deliveries are **not** mirrored into the target gateway session —
+they land in their own cron session with a header/footer frame so the
+main conversation's message-role alternation stays intact.
+
+---
+
+## Kanban (multi-agent work queue)
+
+Durable SQLite-backed board that lets multiple profiles / workers
+collaborate on shared tasks. Users drive it via `hermes kanban `;
+workers spawned by the dispatcher drive it via a dedicated `kanban_*`
+toolset so their schema footprint is zero when they're not inside a
+kanban task.
+
+- **CLI:** `hermes_cli/kanban.py` wires `hermes kanban` with verbs
+ `init`, `create`, `list` (alias `ls`), `show`, `assign`, `link`,
+ `unlink`, `comment`, `attach`, `attachments`, `attach-rm`, `complete`,
+ `block`, `unblock`, `archive`, `tail`, plus less-commonly-used `watch`,
+ `stats`, `runs`, `log`, `assignees`, `heartbeat`, `notify-*`,
+ `dispatch`, `daemon`, `gc`.
+- **Worker/orchestrator toolset:** `tools/kanban_tools.py` exposes
+ `kanban_show`, `kanban_complete`, `kanban_block`, `kanban_heartbeat`,
+ `kanban_comment`, `kanban_create`, `kanban_link`, `kanban_attach`,
+ `kanban_attach_url`, `kanban_attachments`; profiles that explicitly
+ enable the `kanban` toolset outside a dispatcher-spawned task also get
+ `kanban_list` and `kanban_unblock` for board routing.
+- **Dispatcher:** long-lived loop that (default every 60s) reclaims
+ stale claims, promotes ready tasks, atomically claims, and spawns
+ assigned profiles. Runs **inside the gateway** by default via
+ `kanban.dispatch_in_gateway: true`.
+- **Plugin assets:** `plugins/kanban/dashboard/` (web UI) +
+ `plugins/kanban/systemd/` (`hermes-kanban-dispatcher.service` for
+ standalone dispatcher deployment).
+
+Isolation model:
+- **Board** is the hard boundary — workers are spawned with
+ `HERMES_KANBAN_BOARD` pinned in their env so they can't see other
+ boards.
+- **Tenant** is a soft namespace *within* a board — one specialist
+ fleet can serve multiple businesses with workspace-path + memory-key
+ isolation.
+- After `kanban.failure_limit` consecutive non-success attempts on the
+ same task (default: 2), the dispatcher auto-blocks it to prevent spin
+ loops.
+
+Full user-facing docs: `website/docs/user-guide/features/kanban.md`.
+
+---
+
+## Important Policies
+
+### Prompt Caching Must Not Break
+
+Hermes-Agent ensures caching remains valid throughout a conversation. **Do NOT implement changes that would:**
+- Alter past context mid-conversation
+- Change toolsets mid-conversation
+- Reload memories or rebuild system prompts mid-conversation
+
+Cache-breaking forces dramatically higher costs. The ONLY time we alter context is during context compression.
+
+Slash commands that mutate system-prompt state (skills, tools, memory, etc.)
+must be **cache-aware**: default to deferred invalidation (change takes
+effect next session), with an opt-in `--now` flag for immediate
+invalidation. See `/skills install --now` for the canonical pattern.
+
+### Background Process Notifications (Gateway)
+
+When `terminal(background=true, notify_on_complete=true)` is used, the gateway runs a watcher that
+detects process completion and triggers a new agent turn. Control verbosity of background process
+messages with `display.background_process_notifications`
+in config.yaml (or `HERMES_BACKGROUND_NOTIFICATIONS` env var):
+
+- `all` — running-output updates + final message (default)
+- `result` — only the final completion message
+- `error` — only the final message when exit code != 0
+- `off` — no watcher messages at all
+
+---
+
+## Profiles: Multi-Instance Support
+
+Hermes supports **profiles** — multiple fully isolated instances, each with its own
+`HERMES_HOME` directory (config, API keys, memory, sessions, skills, gateway, etc.).
+
+The core mechanism: `_apply_profile_override()` in `hermes_cli/main.py` sets
+`HERMES_HOME` before any module imports. All `get_hermes_home()` references
+automatically scope to the active profile.
+
+### Rules for profile-safe code
+
+1. **Use `get_hermes_home()` for all HERMES_HOME paths.** Import from `hermes_constants`.
+ NEVER hardcode `~/.hermes` or `Path.home() / ".hermes"` in code that reads/writes state.
+ ```python
+ # GOOD
+ from hermes_constants import get_hermes_home
+ config_path = get_hermes_home() / "config.yaml"
+
+ # BAD — breaks profiles
+ config_path = Path.home() / ".hermes" / "config.yaml"
+ ```
+
+2. **Use `display_hermes_home()` for user-facing messages.** Import from `hermes_constants`.
+ This returns `~/.hermes` for default or `~/.hermes/profiles/` for profiles.
+ ```python
+ # GOOD
+ from hermes_constants import display_hermes_home
+ print(f"Config saved to {display_hermes_home()}/config.yaml")
+
+ # BAD — shows wrong path for profiles
+ print("Config saved to ~/.hermes/config.yaml")
+ ```
+
+3. **Module-level constants are fine** — they cache `get_hermes_home()` at import time,
+ which is AFTER `_apply_profile_override()` sets the env var. Just use `get_hermes_home()`,
+ not `Path.home() / ".hermes"`.
+
+4. **Tests that mock `Path.home()` must also set `HERMES_HOME`** — since code now uses
+ `get_hermes_home()` (reads env var), not `Path.home() / ".hermes"`:
+ ```python
+ with patch.object(Path, "home", return_value=tmp_path), \
+ patch.dict(os.environ, {"HERMES_HOME": str(tmp_path / ".hermes")}):
+ ...
+ ```
+
+5. **Gateway platform adapters should use token locks** — if the adapter connects with
+ a unique credential (bot token, API key), call `acquire_scoped_lock()` from
+ `gateway.status` in the `connect()`/`start()` method and `release_scoped_lock()` in
+ `disconnect()`/`stop()`. This prevents two profiles from using the same credential.
+ See `plugins/platforms/irc/adapter.py` for the canonical pattern.
+
+6. **Profile operations are HOME-anchored, not HERMES_HOME-anchored** — `_get_profiles_root()`
+ returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`.
+ This is intentional — it lets `hermes -p coder profile list` see all profiles regardless
+ of which one is active.
+
+## Known Pitfalls
+
+### DO NOT hardcode `~/.hermes` paths
+Use `get_hermes_home()` from `hermes_constants` for code paths. Use `display_hermes_home()`
+for user-facing print/log messages. Hardcoding `~/.hermes` breaks profiles — each profile
+has its own `HERMES_HOME` directory. This was the source of 5 bugs fixed in PR #3575.
+
+### DO NOT introduce new `simple_term_menu` usage
+Existing call sites in `hermes_cli/main.py` remain for legacy fallback only;
+the preferred UI is curses (stdlib) because `simple_term_menu` has
+ghost-duplication rendering bugs in tmux/iTerm2 with arrow keys. New
+interactive menus must use `hermes_cli/curses_ui.py` — see
+`hermes_cli/tools_config.py` for the canonical pattern.
+
+### DO NOT use `\033[K` (ANSI erase-to-EOL) in spinner/display code
+Leaks as literal `?[K` text under `prompt_toolkit`'s `patch_stdout`. Use space-padding: `f"\r{line}{' ' * pad}"`.
+
+### `_last_resolved_tool_names` is a process-global in `model_tools.py`
+`_run_single_child()` in `delegate_tool.py` saves and restores this global around subagent execution. If you add new code that reads this global, be aware it may be temporarily stale during child agent runs.
+
+### DO NOT hardcode cross-tool references in schema descriptions
+Tool schema descriptions must not mention tools from other toolsets by name (e.g., `browser_navigate` saying "prefer web_search"). Those tools may be unavailable (missing API keys, disabled toolset), causing the model to hallucinate calls to non-existent tools. If a cross-reference is needed, add it dynamically in `get_tool_definitions()` in `model_tools.py` — see the `browser_navigate` / `execute_code` post-processing blocks for the pattern.
+
+### The gateway has TWO message guards — both must bypass approval/control commands
+When an agent is running, messages pass through two sequential guards:
+(1) **base adapter** (`gateway/platforms/base.py`) queues messages in
+`_pending_messages` when `session_key in self._active_sessions`, and
+(2) **gateway runner** (`gateway/run.py`) intercepts `/stop`, `/new`,
+`/queue`, `/status`, `/approve`, `/deny` before they reach
+`running_agent.interrupt()`. Any new command that must reach the runner
+while the agent is blocked (e.g. approval prompts) MUST bypass BOTH
+guards and be dispatched inline, not via `_process_message_background()`
+(which races session lifecycle).
+
+### Squash merges from stale branches silently revert recent fixes
+Before squash-merging a PR, ensure the branch is up to date with `main`
+(`git fetch origin main && git reset --hard origin/main` in the worktree,
+then re-apply the PR's commits). A stale branch's version of an unrelated
+file will silently overwrite recent fixes on main when squashed. Verify
+with `git diff HEAD~1..HEAD` after merging — unexpected deletions are a
+red flag.
+
+### Don't wire in dead code without E2E validation
+Unused code that was never shipped was dead for a reason. Before wiring an
+unused module into a live code path, E2E test the real resolution chain
+with actual imports (not mocks) against a temp `HERMES_HOME`.
+
+### Tests must not write to `~/.hermes/`
+The `_isolate_hermes_home` autouse fixture in `tests/conftest.py` redirects `HERMES_HOME` to a temp dir. Never hardcode `~/.hermes/` paths in tests.
+
+**Profile tests**: When testing profile features, also mock `Path.home()` so that
+`_get_profiles_root()` and `_get_default_hermes_home()` resolve within the temp dir.
+Use the pattern from `tests/hermes_cli/test_profiles.py`:
+```python
+@pytest.fixture
+def profile_env(tmp_path, monkeypatch):
+ home = tmp_path / ".hermes"
+ home.mkdir()
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
+ monkeypatch.setenv("HERMES_HOME", str(home))
+ return home
+```
+
+---
+
+## Testing
+
+### Python
+**ALWAYS use `scripts/run_tests.sh`** — do not call `pytest` directly. The script enforces
+hermetic environment parity with CI (unset credential vars, TZ=UTC, LANG=C.UTF-8,
+`-n auto` xdist workers, in-tree subprocess-isolation plugin). Direct `pytest`
+on a 16+ core developer machine with API keys set diverges from CI in ways
+that have caused multiple "works locally, fails in CI" incidents (and the reverse).
+
+```bash
+scripts/run_tests.sh # full suite, CI-parity
+scripts/run_tests.sh tests/gateway/ # one directory
+scripts/run_tests.sh tests/agent/test_foo.py::test_x # one test
+scripts/run_tests.sh -v --tb=long # pass-through pytest flags
+```
+
+**Flake policy:** the runner auto-retries a failing test FILE once in a fresh
+subprocess (`--file-retries`, default 1; `HERMES_TEST_FILE_RETRIES=0` to
+disable). Pass-on-retry counts as green but is printed in a `⚠ FLAKY` summary
+section with both attempts' output. A FLAKY report is a bug to fix, not noise
+to ignore — timing-sensitive tests must not assume a quiet runner (loose
+wall-clock bounds ≥ 2s, event-based sync, no `assert not _wait_until(...)`
+negative-timing races).
+
+#### Subprocess-per-test-file isolation
+
+Every test file runs in a freshly-spawned Python subprocess via `run_tests_parallel.py`. This means module-level dicts/sets and
+ContextVars from one test file cannot leak into the next.
+
+#### Why the wrapper
+
+| | Without wrapper | With wrapper |
+| ------------------- | ------------------------------------------- | ----------------------------------------- |
+| Provider API keys | Whatever is in your env (auto-detects pool) | All env vars except a specific few unset. |
+| HOME / `~/.hermes/` | Your real config+auth.json | Temp dir per test |
+| Timezone | Local TZ (PDT etc.) | UTC |
+| Locale | Whatever is set | C.UTF-8 |
+
+### Where to place what tests
+
+The CI change classifier (`scripts/ci/classify_changes.py`) runs specific jobs based on what files changed. A Python test that asserts
+about the contents of `package.json`, `package-lock.json`, `.ts`/`.tsx`
+source, or any other JS-side artifact will not run on a PR that only touches
+those files. This means a regression can go green on a PR and red on `main` (where the
+classifier fails open and runs everything).
+
+Any test that reads or asserts about `package.json`,
+`package-lock.json`, `tsconfig.json`, `.ts`/`.tsx`/`.js`/`.mjs`/`.cjs`
+source files configuration belongs in the JS (vitest) test suite, not in `tests/*.py`.
+
+### Don't write change-detector tests
+
+A test is a **change-detector** if it fails whenever data that is **expected
+to change** gets updated — model catalogs, config version numbers,
+enumeration counts, hardcoded lists of provider models. These tests add no
+behavioral coverage; they just guarantee that routine source updates break
+CI and cost engineering time to "fix."
+
+**Do not write:**
+
+```python
+# catalog snapshot — breaks every model release
+assert "gemini-2.5-pro" in _PROVIDER_MODELS["gemini"]
+assert "MiniMax-M2.7" in models
+
+# config version literal — breaks every schema bump
+assert DEFAULT_CONFIG["_config_version"] == 21
+
+# enumeration count — breaks every time a skill/provider is added
+assert len(_PROVIDER_MODELS["huggingface"]) == 8
+```
+
+**Do write:**
+
+```python
+# behavior: does the catalog plumbing work at all?
+assert "gemini" in _PROVIDER_MODELS
+assert len(_PROVIDER_MODELS["gemini"]) >= 1
+
+# behavior: does migration bump the user's version to current latest?
+assert raw["_config_version"] == DEFAULT_CONFIG["_config_version"]
+
+# invariant: no plan-only model leaks into the legacy list
+assert not (set(moonshot_models) & coding_plan_only_models)
+
+# invariant: every model in the catalog has a context-length entry
+for m in _PROVIDER_MODELS["huggingface"]:
+ assert m.lower() in DEFAULT_CONTEXT_LENGTHS_LOWER
+```
+
+The rule: if the test reads like a snapshot of current data, delete it. If
+it reads like a contract about how two pieces of data must relate, keep it.
+When a PR adds a new provider/model and you want a test, make the test
+assert the relationship (e.g. "catalog entries all have context lengths"),
+not the specific names.
+
+Reviewers should reject new change-detector tests; authors should convert
+them into invariants before re-requesting review.
+
+### Never read source code in tests
+
+A test that reads a source file's text is testing *the shape of the
+source code*, not its behavior. This is a hard antipattern, banned outright.
+Any test that reads a .py, .ts, .tsx, etc., file is suspect.
+
+**Why it's actively harmful, not just weak:**
+
+- It passes when the implementation is subtly broken (the regex matches a
+ call site that exists but is wired wrong) and fails when a correct
+ refactor changes formatting, variable names, or control flow with
+ identical runtime behavior. Both directions of failure are wrong.
+- It can't be run against a built/bundled/minified artifact, so it silently
+ stops testing anything the moment code moves, gets renamed, or a
+ dependency reformats it.
+- It actively blocks refactors: reviewers see "keeps a pattern intact" tests
+ fail during pure structural cleanup with no behavior change, and either
+ hand-wave the failure (dangerous) or waste time updating regexes that add
+ nothing (waste).
+- It gives false confidence. a green suite full of source-regex tests
+ looks like coverage but has never once executed the code path it claims
+ to guard.
+
+**Do not write:**
+
+```ts
+const source = fs.readFileSync(path.join(__dirname, 'main.ts'), 'utf8')
+
+test('backend spawn hides the Windows console', () => {
+ assert.match(source, /spawn\(\s*backend\.command,\s*backend\.args[\s\S]{0,300}hiddenWindowsChildOptions/)
+})
+```
+
+**Do write — extract the logic into a small pure/DI-testable function and
+call it for real:**
+
+```ts
+// backend-spawn.ts
+export function hiddenWindowsChildOptions(options: SpawnOptionsLike = {}, isWindows = process.platform === 'win32') {
+ if (!isWindows || 'windowsHide' in options) return options
+ return { ...options, windowsHide: true }
+}
+
+// backend-spawn.test.ts
+test('windowsHide defaults to true on Windows, is left alone elsewhere', () => {
+ assert.equal(hiddenWindowsChildOptions({}, true).windowsHide, true)
+ assert.equal(hiddenWindowsChildOptions({}, false).windowsHide, undefined)
+ assert.equal(hiddenWindowsChildOptions({ windowsHide: false }, true).windowsHide, false)
+})
+```
+
+If the logic lives inline in a god-file (`main.ts`, `cli.py`,
+`gateway/run.py`) and extracting it feels disruptive: that's the actual
+signal to do the extraction, not to regex around it.
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
new file mode 100644
index 000000000000..67617e9aa359
--- /dev/null
+++ b/GOVERNANCE.md
@@ -0,0 +1,50 @@
+# Governance
+
+## Decision-making
+
+Hermes Agent is maintained by [Nous Research](https://nousresearch.com). Maintainers
+hold final say on merges, releases and scope. Contributions are welcome and arrive
+through pull requests; see [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
+
+## The rubric decides
+
+`AGENTS.md` carries the contribution rubric — what gets merged and what gets rejected —
+and it is the project's intent layer, not a style guide. Two rules follow from it:
+
+- The product surface (platforms, providers, desktop and TUI features) expands
+ aggressively and on purpose.
+- The **core agent and the model tool schema** are conservative, because every core tool
+ is sent on every API call for every user.
+
+A change that grows the waist needs an argued case. A change that grows the edges needs
+only to be correct.
+
+## Automated triage
+
+An automated sweeper may close pull requests on exactly three grounds:
+`implemented_on_main`, `cannot_reproduce`, and `incoherent`. Taste-based
+"we don't want this" or "out of scope" closures are **not** an automated decision — those
+stay with a human maintainer. When in doubt, the sweeper leaves the PR open.
+
+## Roles
+
+| Role | Can |
+|------|-----|
+| Contributor | Open issues and pull requests |
+| Reviewer | Review and approve changes in their area |
+| Maintainer | Merge, cut releases, set scope, resolve disputes |
+
+Contributors are recorded in `.mailmap` and `contributors/`; `scripts/add_contributor.py`
+and `scripts/contributor_audit.py` maintain that record.
+
+## Changing the rules
+
+Amendments to this document, to `AGENTS.md`, or to the release process are proposed as a
+pull request and require maintainer approval. Security matters follow
+[SECURITY.md](SECURITY.md) and are handled privately until a fix ships.
+
+## Code of conduct
+
+Discussion happens in the issue tracker and on
+[Discord](https://discord.gg/NousResearch). Behaviour that makes the project unpleasant
+to contribute to is grounds for removal from those channels at maintainer discretion.
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 000000000000..585f8e1a1ec9
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,45 @@
+# Releasing
+
+Hermes Agent versions live in `pyproject.toml` (`[project] version`) and follow
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+- **Patch** — bug fixes, no behaviour change for a working setup.
+- **Minor** — new platforms, providers, tools, skills, or CLI commands.
+- **Major** — a change that breaks existing configs, stored state, or the plugin/skill
+ contract.
+
+## Checklist
+
+1. **Green baseline.** `scripts/run_tests.sh` passes (never bare `pytest` — see
+ [TESTING.md](TESTING.md)), the per-workspace JS suites pass
+ (`npm test --workspace ui-tui`, `--workspace apps/desktop`, `--workspace web`), and
+ CI's `all-checks-pass` gate is green on `main`.
+2. **Bump the version** in `pyproject.toml`. Nothing else hardcodes it.
+3. **Update [CHANGELOG.md](CHANGELOG.md)** — move `Unreleased` items under the new
+ version heading with the release date. A release without an entry is not releasable.
+4. **Refresh the docs that track behaviour**: `README.md`, `docs/ARCHITECTURE.md`,
+ `docs/ARCHITECTURE_MAP.md`, `docs/INDEX.md`, `docs/DEPLOYMENT_PROCESS.md`,
+ `TESTING.md`, and re-export `docs/diagrams/architecture.html` if the source diagram
+ changed. If `AGENTS.md` changed, re-copy it over `CLAUDE.md` (keeping the
+ do-not-hand-edit header line) — no generator or CI check does this for you.
+5. **Verify the install paths** still work end to end: `scripts/install.sh`
+ (Linux/macOS/WSL2), `scripts/install.ps1` (native Windows), the Docker image, and the
+ Nix flake.
+6. **Tag and release.** `git tag vX.Y.Z && git push origin vX.Y.Z`, then publish the
+ GitHub release with the changelog section as its body.
+7. **Publish artifacts** — wheel and container image via the packaging workflows.
+8. **Verify after the fact**: a clean install of the published version starts, `hermes
+ model` lists providers, and the gateway reaches ready state.
+
+## Hotfixes
+
+Branch from the release tag, apply the minimal fix, bump the patch version, add a
+changelog entry, release, then merge back to `main`. Hotfixes skip nothing in the
+checklist above except step 4 where no documented behaviour changed.
+
+## Rollback
+
+Releases are additive: reinstall the previous version from PyPI or pull the previous
+container tag. Stored session state is forward-compatible within a major version, so a
+downgrade inside the same major line is safe. Downgrading across a major version requires
+the migration note published with that major release.
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 000000000000..b09b2e4bce67
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,100 @@
+# Testing
+
+All tests are headless, silent and automated — no prompts, no manual steps, no browser
+windows opening on a developer's desktop.
+
+## Running the suites
+
+**Always use `scripts/run_tests.sh`. Never call `pytest` directly.** The wrapper is what
+gives you CI parity: it unsets credential env vars, pins `TZ=UTC`, `LANG=C.UTF-8` and
+`PYTHONHASHSEED=0`, redirects `HERMES_HOME` to a temp dir, and runs each test file in its
+own freshly-spawned subprocess so module-level state cannot leak between files. Bare
+`pytest` on a developer machine with API keys set has repeatedly produced
+works-locally-fails-in-CI incidents (and the reverse).
+
+```bash
+# Full suite, CI parity
+scripts/run_tests.sh
+
+# One directory or one test — pytest paths pass straight through
+scripts/run_tests.sh tests/gateway/
+scripts/run_tests.sh tests/agent/test_foo.py::test_x
+
+# pytest flags pass through too
+scripts/run_tests.sh -v --tb=long
+
+# Integration tests (external services, API keys, Modal) — opt in explicitly
+scripts/run_tests.sh -m integration
+```
+
+`pyproject.toml` sets `testpaths = ["tests"]` and `addopts = "-m 'not integration'"`, so
+the default run is the fast lane. Integration tests are never a requirement for proving
+an ordinary change works.
+
+JavaScript and TypeScript are per-workspace — there is no root `test` script:
+
+```bash
+npm test --workspace ui-tui
+npm test --workspace apps/desktop # or: cd apps/desktop && npx vitest run
+npm test --workspace web
+```
+
+Workspace dependencies install from the repo root (`npm run install:tui`,
+`install:desktop`, `install:web`).
+
+## Layout
+
+| Path | Covers |
+|------|--------|
+| `tests/agent` | Agent core: turn loop, context assembly, compression |
+| `tests/gateway` | Gateway sessions, platform adapters, delivery, authz |
+| `tests/hermes_cli` | CLI commands and argument parsing |
+| `tests/cron` | Scheduler, jobs, lifecycle guard |
+| `tests/acp`, `tests/acp_adapter` | ACP adapter and permissions |
+| `tests/hermes_state` | Persistence |
+| `tests/computer_use`, `tests/dashboard`, `tests/docker` | Subsystem suites |
+| `tests/e2e`, `tests/integration` | End-to-end and external-service paths |
+| `tests/fakes`, `tests/fixtures`, `conftest.py` | Shared doubles and fixtures |
+| `tests-js/` | Node-side tests for the desktop/web code |
+
+## Markers
+
+- `integration` — requires external services or API keys; excluded by default.
+- `real_concurrent_gate` — opts out of the autouse stub disabling concurrent-instance
+ detection.
+- `real_agent_prewarm` — opts out of the autouse stub disabling the TUI pre-warm timer.
+
+## Flake policy
+
+The runner auto-retries a failing test **file** once in a fresh subprocess
+(`--file-retries`, default 1; `HERMES_TEST_FILE_RETRIES=0` disables it). A pass on retry
+counts as green but is printed in a `⚠ FLAKY` summary with both attempts' output. FLAKY is
+a bug to fix, not noise to ignore — timing-sensitive tests must not assume a quiet runner:
+use loose wall-clock bounds (≥ 2s), event-based synchronisation, and no negative-timing
+races like `assert not _wait_until(...)`.
+
+## What a change must prove
+
+1. The non-integration suite is green before and after, run through
+ `scripts/run_tests.sh`.
+2. New behaviour has a test that fails without the change. A passing suite proves nothing
+ about code no test touches.
+3. Name one thing that should *not* have changed and verify it — regressions in the
+ learning loop (memory, skill creation, session search) are the ones tests miss.
+4. Cache behaviour: if the change touches prompt assembly, toolsets or system prompt
+ construction, confirm the cached prefix still survives a multi-turn conversation.
+
+## Lint and static checks
+
+```bash
+uv run ruff check . # PLW1514 (explicit encoding) is enforced
+npm run --ws check # eslint + prettier across the JS workspaces
+hadolint Dockerfile # container lint
+```
+
+## CI
+
+`.github/workflows/ci.yml` is the orchestrator: it classifies the diff once and calls
+only the sub-workflows a change can affect (`js-tests`, `lint`, `docker`, `docs-site-checks`
+and others), then aggregates into a single `all-checks-pass` gate for branch protection.
+On `push` the classifier fails open so post-merge validation is never weakened.
diff --git a/VISION.md b/VISION.md
new file mode 100644
index 000000000000..23e4e49d6b73
--- /dev/null
+++ b/VISION.md
@@ -0,0 +1,36 @@
+# Vision
+
+## The point
+
+Most AI assistants forget you the moment the tab closes. Hermes is built the other way
+round: it is a personal agent that accumulates. It writes skills from what it just did,
+improves those skills while using them, curates its own memory, searches its past
+conversations, and builds a model of the person it works for across sessions.
+
+## Where it lives
+
+An agent tied to one laptop is a tool. An agent reachable from Telegram while it works
+on a cloud VM is a colleague. Hermes runs on a $5 VPS, a GPU cluster, or serverless
+infrastructure that costs close to nothing while idle, and speaks to you over whichever
+channel you already use — CLI, TUI, desktop app, or any of ~20 messaging platforms
+through a single gateway process.
+
+## What we refuse to do
+
+- **Lock you to a model.** Any provider, any endpoint, switched with `hermes model`.
+- **Grow the waist.** Every core model tool is paid for on every API call. Capability
+ belongs in skills and plugins at the edges, where it costs nothing until used.
+- **Break the cache.** Cheap long conversations are a product feature, not an
+ optimisation. Design decisions that invalidate the cached prefix are rejected.
+
+## Three-year horizon
+
+Hermes should be the agent a person keeps for years — where the value is not the model
+behind it but the accumulated skills, memory and habits layered on top, all of which
+survive swapping that model out for whatever is best next month.
+
+## How we measure it
+
+Sessions per user over time rather than sessions per week; skills created and still in
+use after a month; the share of capability shipped as plugins and skills instead of core
+tools; and the cost of a long conversation staying flat as it lengthens.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 000000000000..829739f8d9bf
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,84 @@
+# Architecture
+
+Hermes is one agent core with several transports bolted onto it. Everything below is a
+description of what is in the tree today, not an aspiration.
+
+## Layers
+
+**Entry points** (`pyproject.toml [project.scripts]`)
+
+| Command | Module | Role |
+|---------|--------|------|
+| `hermes` | `hermes_cli.main:main` | User-facing CLI: setup, model selection, gateway control, backups |
+| `hermes-agent` | `run_agent:main` | Runs a single agent session directly |
+| `hermes-acp` | `acp_adapter.entry:main` | Agent Client Protocol adapter for external editors |
+
+**Agent core** (`agent/`) — the turn loop, provider adapters
+(`anthropic_adapter.py` and friends), context assembly, compression, subagent
+delegation, and usage accounting. Sibling top-level modules carry the shared state and
+schema: `hermes_state.py` (persistence), `model_tools.py` / `toolsets.py` (the tool
+schema sent to the model), `hermes_constants.py`, `trajectory_compressor.py`.
+
+**Providers** — provider neutrality is split across two places. `providers/` is the
+registry and the `ProviderProfile` ABC (`__init__.py`, `base.py`) plus back-compat for
+legacy `providers/.py` modules. The 33 actual inference backends (openrouter,
+anthropic, gmi, deepseek, nvidia, vertex, …) ship as plugins under
+`plugins/model-providers//`, each calling `providers.register_provider(...)` at
+module load. `providers/__init__.py::_discover_providers()` scans them lazily on the
+first `get_provider_profile()` / `list_providers()` call — this is a separate discovery
+system from the general `PluginManager`. Add a new provider under
+`plugins/model-providers/`, never to `providers/`.
+
+**Tools** (`tools/`) — the implementations behind the model-visible tool schema: shell,
+browser (Camoufox), file editing, delegation, approval flows.
+
+**Transports**
+
+- `gateway/` — the long-running multi-platform process. `run.py` is the loop,
+ `session.py` holds per-conversation state, `platforms/` has the ~20 channel adapters,
+ `relay/` the remote connector, `slash_commands.py` the command surface, plus delivery
+ ledger, pairing, drain control, scale-to-zero and shutdown watchdogs.
+- `tui_gateway/` — terminal UI host and event publisher.
+- `acp_adapter/` — editor integration over ACP with its own permission/approval model.
+- `apps/desktop`, `web/` — Electron shell and web frontend (TypeScript/Vite).
+
+**Scheduling** (`cron/`) — jobs, executions, lifecycle guard and scheduler providers, so
+automations run unattended and deliver to any platform.
+
+**Extension surface** (`skills/`, `optional-skills/`, `plugins/`,
+`optional-mcps/`) — including `plugins/model-providers/` above — where new capability is meant to land. Skills follow the
+agentskills.io layout; plugins register their own tools and hooks.
+
+**Docs site** (`website/`) — Docusaurus source for the published documentation.
+
+## Request path
+
+A message arrives on a platform adapter in `gateway/platforms/`, is normalised into a
+gateway session (`gateway/session.py`), and handed to the agent core. The core assembles
+the cached prompt prefix plus the new turn, calls the provider, and dispatches any tool
+calls into `tools/` (or a plugin). Output streams back through the delivery layer to the
+originating channel. The CLI and TUI paths skip the gateway and call the core directly.
+
+## Invariants
+
+1. The cached prompt prefix is never invalidated mid-conversation except by explicit
+ context compression.
+2. Transports do not implement agent behaviour — behaviour changes go in `agent/`.
+3. A new capability defaults to a skill or plugin; a new core model tool needs an
+ argued case, because it costs tokens on every call for every user.
+
+## Persistence and configuration
+
+Session and agent state live in `hermes_state.py`-managed local storage (SQLite with FTS5
+session search). Runtime configuration is `~/.hermes/config.yaml`, resolved profile-aware
+through `get_hermes_home()` in `hermes_constants.py` — `cli-config.yaml.example` in the
+repo root is a reference sample, not the file the runtime reads.
+
+**`.env` is secrets only** — API keys, tokens, passwords. Every behavioural setting
+(timeouts, thresholds, feature flags, display preferences) belongs in `config.yaml`. A
+change that tells users to "set `HERMES_X` in your `.env`" for non-secret config is
+rejected on review; bridge from `config.yaml` to an internal env var in code instead.
+Secrets are never committed. Deployment topologies (local, Docker, SSH, Singularity,
+Modal, Daytona) are selected per terminal backend and share the same core.
+
+See `docs/ARCHITECTURE_MAP.md` for the directory-to-responsibility map and the diagram.
diff --git a/docs/ARCHITECTURE_MAP.md b/docs/ARCHITECTURE_MAP.md
new file mode 100644
index 000000000000..d00428902a1d
--- /dev/null
+++ b/docs/ARCHITECTURE_MAP.md
@@ -0,0 +1,61 @@
+# Architecture Map
+
+Directory-to-responsibility map for the repository. Companion to
+[ARCHITECTURE.md](ARCHITECTURE.md).
+
+## Diagram
+
+- Source: [`diagrams/architecture.excalidraw`](diagrams/architecture.excalidraw)
+- Export: [`diagrams/architecture.html`](diagrams/architecture.html) (self-contained,
+ no external CDN — open it directly in a browser)
+
+Regenerate the export after editing the source:
+
+```bash
+python3 ~/.agents/skills/creative/excalidraw/scripts/export_html.py \
+ docs/diagrams/architecture.excalidraw docs/diagrams/architecture.html
+```
+
+## Top-level map
+
+| Path | Responsibility |
+|------|----------------|
+| `agent/` | Agent core: turn loop, provider adapters, context assembly, delegation |
+| `providers/` | Provider **registry + `ProviderProfile` ABC** only (`__init__.py`, `base.py`), plus back-compat for legacy `providers/.py`. Do not add providers here |
+| `plugins/model-providers//` | The 33 real inference backends (openrouter, anthropic, gmi, deepseek, nvidia, vertex, …). Each calls `providers.register_provider(...)` at load; discovered lazily by `providers/__init__.py::_discover_providers()`. **This is where a new provider goes** |
+| `tools/` | Implementations behind the model-visible tool schema |
+| `model_tools.py`, `toolsets.py`, `toolset_distributions.py` | The tool schema itself and how it is bundled per surface |
+| `hermes_state.py` | Session/agent persistence |
+| `trajectory_compressor.py` | Context compression — the one sanctioned cache-invalidating path |
+| `hermes_cli/` | `hermes` CLI: setup, auth, model switch, gateway control, backup |
+| `gateway/` | Long-running multi-platform process (see below) |
+| `tui_gateway/` | Terminal UI host, compute host, event publisher |
+| `acp_adapter/` | Agent Client Protocol adapter for editors, with its own approval model |
+| `apps/desktop`, `apps/shared`, `web/` | Electron desktop shell and web frontend |
+| `cron/` | Scheduler, jobs, executions, lifecycle guard |
+| `skills/`, `optional-skills/` | Agent skills (agentskills.io layout) |
+| `plugins/`, `optional-mcps/` | Plugin and MCP extension points |
+| `locales/` | i18n catalogues (shipped as wheel data-files) |
+| `website/` | Docusaurus documentation site |
+| `packaging/`, `docker/`, `nix/`, `scripts/install.*` | Distribution and installers |
+| `tests/`, `tests-js/` | Python and JavaScript test suites |
+| `scripts/` | Developer and CI utilities |
+
+## Inside `gateway/`
+
+| Path | Responsibility |
+|------|----------------|
+| `run.py` | Gateway main loop |
+| `session.py`, `session_context.py` | Per-conversation state and context |
+| `platforms/` | ~20 channel adapters (Telegram, Discord, Slack, Signal, WhatsApp, …) |
+| `relay/` | Remote relay connector |
+| `slash_commands.py`, `slash_access.py` | Command surface and access control |
+| `delivery.py`, `delivery_ledger.py` | Outbound delivery and idempotency |
+| `authz_mixin.py`, `pairing.py` | Authorisation and device pairing |
+| `drain_control.py`, `scale_to_zero.py`, `shutdown_watchdog.py` | Lifecycle, hibernation, safe shutdown |
+| `builtin_hooks/` | Extension point for always-registered gateway hooks (none shipped today) |
+
+## Cross-cutting docs
+
+`docs/security/`, `docs/observability/`, `docs/middleware/`, `docs/design/`,
+`docs/kanban/` hold subsystem detail; `docs/INDEX.md` lists the full canonical set.
diff --git a/docs/DEPLOYMENT_PROCESS.md b/docs/DEPLOYMENT_PROCESS.md
new file mode 100644
index 000000000000..8f03518c3b97
--- /dev/null
+++ b/docs/DEPLOYMENT_PROCESS.md
@@ -0,0 +1,78 @@
+# Deployment Process
+
+Hermes deploys as a user-installed process, not a hosted service. "Deploying" means
+getting the CLI and (optionally) the long-running gateway onto a machine — a laptop, a
+$5 VPS, a GPU box, or serverless infrastructure — and keeping it running.
+
+## Install paths
+
+| Target | Command |
+|--------|---------|
+| Linux / macOS / WSL2 / Termux | `curl -fsSL https://hermes-agent.nousresearch.com/install.sh \| bash` |
+| Windows (native, PowerShell) | `iex (irm https://hermes-agent.nousresearch.com/install.ps1)` |
+| Docker | `docker compose up -d` (see `docker-compose.yml`, `docker/`) |
+| Nix | `nix develop` / the flake in `flake.nix` |
+| From source | `uv sync && uv run hermes` |
+
+The one-liners pipe a remote script straight into a shell. If your environment forbids
+that, download the installer and read it before running, or skip it entirely and install
+from a pinned source tag (`git clone`, `git checkout vX.Y.Z`, `uv sync`) — the from-source
+path is equivalent and auditable.
+
+The installer provisions uv, Python 3.11, Node.js, ripgrep and ffmpeg; on Windows it also
+unpacks a portable MinGit under `%LOCALAPPDATA%\hermes\git` rather than touching a system
+Git install.
+
+## Configure
+
+Run `hermes setup` for the guided path. What it configures:
+
+1. **Secrets** in `~/.hermes/.env` — API keys, tokens, passwords, and nothing else.
+ `.env.example` lists the recognised variables. Never commit them.
+2. **Settings** in `~/.hermes/config.yaml` — every behavioural option (timeouts,
+ thresholds, feature flags, display preferences). This is the file the runtime reads,
+ resolved profile-aware through `get_hermes_home()`; the repo's
+ `cli-config.yaml.example` is a reference sample, not a live config.
+3. Pick a model with `hermes model` — no code change, any supported provider.
+
+## Run the gateway
+
+Order matters on a fresh host — `start` alone fails before the gateway is configured:
+
+```bash
+hermes gateway setup # configure messaging platforms (required first)
+hermes gateway install # optional: install the service unit for auto-start
+hermes gateway start # long-running multi-platform process
+hermes gateway status # readiness and channel state
+hermes gateway restart # reload after a config change
+hermes gateway stop # drains in-flight work before exiting
+```
+
+Under Docker, `scripts/hermes-gateway` and the compose file own the lifecycle. The
+gateway supports drain control and scale-to-zero, so a serverless or hibernating host
+wakes on demand instead of idling at full cost.
+
+## Terminal backends
+
+The agent's execution environment is selected per session: `local`, `docker`, `ssh`,
+`singularity`, `modal`, `daytona`. Modal and Daytona hibernate between sessions. Choosing
+a backend never changes agent behaviour — that is the point of the single core.
+
+## Upgrade
+
+Re-run the installer, or `uv sync` from source at the new tag. Check
+[`../CHANGELOG.md`](../CHANGELOG.md) for the version being installed. Session state is
+forward-compatible within a major version.
+
+## Verify after deploying
+
+1. `hermes --version` reports the version you intended.
+2. `hermes gateway status` reaches ready with every paired channel connected.
+3. Send one message on a real channel and confirm a reply — a started process is not a
+ working one.
+4. Check the logs for provider auth failures; a bad key surfaces as a silent non-reply.
+
+## Rollback
+
+Reinstall the previous version or pull the previous container tag, then re-run the
+verification steps above. See [`../RELEASING.md`](../RELEASING.md) for release mechanics.
diff --git a/docs/INDEX.md b/docs/INDEX.md
new file mode 100644
index 000000000000..ed0354a236c0
--- /dev/null
+++ b/docs/INDEX.md
@@ -0,0 +1,56 @@
+# Documentation Index
+
+Canonical index of the Hermes Agent documentation set. The published site lives at
+ and is built from `website/`.
+
+## Start here
+
+| Document | What it answers |
+|----------|-----------------|
+| [README.md](../README.md) | What Hermes is and how to install it |
+| [VISION.md](../VISION.md) | Why it exists and what we refuse to build |
+| [BLUEPRINT.md](../BLUEPRINT.md) | The non-negotiable project requirements and scope |
+| [AGENTS.md](../AGENTS.md) | Development guide and contribution rubric (canonical) |
+| [CLAUDE.md](../CLAUDE.md) | Copy of AGENTS.md for Claude-based tooling — re-copy after editing AGENTS.md; nothing enforces it automatically |
+
+## Building and shipping
+
+| Document | What it answers |
+|----------|-----------------|
+| [docs/ARCHITECTURE.md](ARCHITECTURE.md) | How the system is put together |
+| [docs/ARCHITECTURE_MAP.md](ARCHITECTURE_MAP.md) | Which directory owns what, plus the diagram |
+| [TESTING.md](../TESTING.md) | How to run the suites and what a change must prove |
+| [docs/DEPLOYMENT_PROCESS.md](DEPLOYMENT_PROCESS.md) | Installing, configuring and running the gateway |
+| [RELEASING.md](../RELEASING.md) | Versioning, release checklist, rollback |
+| [CHANGELOG.md](../CHANGELOG.md) | What changed in each version |
+| [CONTRIBUTING.md](../CONTRIBUTING.md) | Pull request workflow |
+
+## Policy
+
+| Document | What it answers |
+|----------|-----------------|
+| [GOVERNANCE.md](../GOVERNANCE.md) | Who decides what, and how automated triage is bounded |
+| [SECURITY.md](../SECURITY.md) | Reporting vulnerabilities and the security model |
+| [LICENSE](../LICENSE) | MIT licence |
+
+## Diagrams
+
+- [docs/diagrams/architecture.excalidraw](diagrams/architecture.excalidraw) — source
+- [docs/diagrams/architecture.html](diagrams/architecture.html) — self-contained export
+
+## Subsystem detail
+
+- [`docs/security/`](security/) — including network egress isolation
+- [`docs/observability/`](observability/)
+- [`docs/middleware/`](middleware/)
+- [`docs/design/`](design/), [`docs/kanban/`](kanban/), [`docs/plans/`](plans/)
+- [`docs/session-lifecycle.md`](session-lifecycle.md),
+ [`docs/billing-lifecycle.md`](billing-lifecycle.md),
+ [`docs/profile-routing.md`](profile-routing.md),
+ [`docs/relay-connector-contract.md`](relay-connector-contract.md),
+ [`docs/chronos-managed-cron-contract.md`](chronos-managed-cron-contract.md)
+
+## Translations
+
+`README.es.md`, `README.zh-CN.md`, `README.ur-pk.md`, `CONTRIBUTING.es.md`,
+`SECURITY.es.md`.
diff --git a/docs/diagrams/architecture.excalidraw b/docs/diagrams/architecture.excalidraw
new file mode 100644
index 000000000000..670b71f23153
--- /dev/null
+++ b/docs/diagrams/architecture.excalidraw
@@ -0,0 +1,1500 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/bbudiono/hermes-agent",
+ "elements": [
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 78253642,
+ "isDeleted": false,
+ "id": "r-chans",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 240,
+ "y": -60,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#fff3bf",
+ "width": 180,
+ "height": 50,
+ "seed": 78253642,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-chans"
+ },
+ {
+ "type": "arrow",
+ "id": "a-chans-gw"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 72933897,
+ "isDeleted": false,
+ "id": "t-chans",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 248,
+ "y": -45.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 72933897,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "~20 chat platforms",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-chans",
+ "originalText": "~20 chat platforms",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 70633293,
+ "isDeleted": false,
+ "id": "r-cli",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 40,
+ "y": 40,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#d3f9d8",
+ "width": 180,
+ "height": 60,
+ "seed": 70633293,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-cli"
+ },
+ {
+ "type": "arrow",
+ "id": "a-cli-core"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 20687736,
+ "isDeleted": false,
+ "id": "t-cli",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 48,
+ "y": 60.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 20687736,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "CLI (hermes_cli)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-cli",
+ "originalText": "CLI (hermes_cli)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 9148726,
+ "isDeleted": false,
+ "id": "r-gw",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 240,
+ "y": 40,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#d3f9d8",
+ "width": 180,
+ "height": 60,
+ "seed": 9148726,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-gw"
+ },
+ {
+ "type": "arrow",
+ "id": "a-chans-gw"
+ },
+ {
+ "type": "arrow",
+ "id": "a-gw-core"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 46892746,
+ "isDeleted": false,
+ "id": "t-gw",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 248,
+ "y": 60.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 46892746,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Gateway (gateway/)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-gw",
+ "originalText": "Gateway (gateway/)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 41681109,
+ "isDeleted": false,
+ "id": "r-tui",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 440,
+ "y": 40,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#d3f9d8",
+ "width": 180,
+ "height": 60,
+ "seed": 41681109,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-tui"
+ },
+ {
+ "type": "arrow",
+ "id": "a-tui-core"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 93693426,
+ "isDeleted": false,
+ "id": "t-tui",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 448,
+ "y": 60.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 93693426,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "TUI (tui_gateway)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-tui",
+ "originalText": "TUI (tui_gateway)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 68308845,
+ "isDeleted": false,
+ "id": "r-acp",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 640,
+ "y": 40,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#d3f9d8",
+ "width": 180,
+ "height": 60,
+ "seed": 68308845,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-acp"
+ },
+ {
+ "type": "arrow",
+ "id": "a-acp-core"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 44642695,
+ "isDeleted": false,
+ "id": "t-acp",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 648,
+ "y": 60.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 44642695,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "ACP + Desktop",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-acp",
+ "originalText": "ACP + Desktop",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 79859277,
+ "isDeleted": false,
+ "id": "r-core",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 240,
+ "y": 180,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#e7f5ff",
+ "width": 380,
+ "height": 80,
+ "seed": 79859277,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-core"
+ },
+ {
+ "type": "arrow",
+ "id": "a-cli-core"
+ },
+ {
+ "type": "arrow",
+ "id": "a-gw-core"
+ },
+ {
+ "type": "arrow",
+ "id": "a-tui-core"
+ },
+ {
+ "type": "arrow",
+ "id": "a-acp-core"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-prov"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-tools"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-state"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-cron"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-edges"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 60808769,
+ "isDeleted": false,
+ "id": "t-core",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 248,
+ "y": 210.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 364,
+ "height": 20,
+ "seed": 60808769,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Agent Core (agent/)\nturn loop \u00b7 context \u00b7 cache",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-core",
+ "originalText": "Agent Core (agent/)\nturn loop \u00b7 context \u00b7 cache",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 3728419,
+ "isDeleted": false,
+ "id": "r-prov",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 40,
+ "y": 340,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#f3f0ff",
+ "width": 180,
+ "height": 60,
+ "seed": 3728419,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-prov"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-prov"
+ },
+ {
+ "type": "arrow",
+ "id": "a-prov-models"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 13493787,
+ "isDeleted": false,
+ "id": "t-prov",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 48,
+ "y": 360.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 13493787,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Providers\nplugins/model-providers",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-prov",
+ "originalText": "Providers\nplugins/model-providers",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 20569532,
+ "isDeleted": false,
+ "id": "r-tools",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 240,
+ "y": 340,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#f3f0ff",
+ "width": 180,
+ "height": 60,
+ "seed": 20569532,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-tools"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-tools"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 878872,
+ "isDeleted": false,
+ "id": "t-tools",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 248,
+ "y": 360.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 878872,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Tools (tools/)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-tools",
+ "originalText": "Tools (tools/)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 10845736,
+ "isDeleted": false,
+ "id": "r-state",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 440,
+ "y": 340,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#f3f0ff",
+ "width": 180,
+ "height": 60,
+ "seed": 10845736,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-state"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-state"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 43557367,
+ "isDeleted": false,
+ "id": "t-state",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 448,
+ "y": 360.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 43557367,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "State (hermes_state)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-state",
+ "originalText": "State (hermes_state)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 38432469,
+ "isDeleted": false,
+ "id": "r-cron",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 640,
+ "y": 340,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#f3f0ff",
+ "width": 180,
+ "height": 60,
+ "seed": 38432469,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-cron"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-cron"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 84328427,
+ "isDeleted": false,
+ "id": "t-cron",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 648,
+ "y": 360.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 84328427,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Cron (cron/)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-cron",
+ "originalText": "Cron (cron/)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 80984919,
+ "isDeleted": false,
+ "id": "r-edges",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 240,
+ "y": 460,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffe3e3",
+ "width": 380,
+ "height": 60,
+ "seed": 80984919,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-edges"
+ },
+ {
+ "type": "arrow",
+ "id": "a-core-edges"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 42353,
+ "isDeleted": false,
+ "id": "t-edges",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 248,
+ "y": 480.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 364,
+ "height": 20,
+ "seed": 42353,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Skills \u00b7 Plugins \u00b7 MCPs (the edges)",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-edges",
+ "originalText": "Skills \u00b7 Plugins \u00b7 MCPs (the edges)",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "rectangle",
+ "version": 1,
+ "versionNonce": 39481326,
+ "isDeleted": false,
+ "id": "r-models",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 40,
+ "y": 460,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#fff3bf",
+ "width": 180,
+ "height": 60,
+ "seed": 39481326,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "boundElements": [
+ {
+ "type": "text",
+ "id": "t-models"
+ },
+ {
+ "type": "arrow",
+ "id": "a-prov-models"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "type": "text",
+ "version": 1,
+ "versionNonce": 16106854,
+ "isDeleted": false,
+ "id": "t-models",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 48,
+ "y": 480.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 164,
+ "height": 20,
+ "seed": 16106854,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "fontSize": 16,
+ "fontFamily": 1,
+ "text": "Any model endpoint",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "r-models",
+ "originalText": "Any model endpoint",
+ "lineHeight": 1.25
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 91239064,
+ "isDeleted": false,
+ "id": "a-chans-gw",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 330.0,
+ "y": -10,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 0.0,
+ "height": 50,
+ "seed": 91239064,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 50
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-chans",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-gw",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 74059502,
+ "isDeleted": false,
+ "id": "a-cli-core",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 220,
+ "y": 70.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 20,
+ "height": 150.0,
+ "seed": 74059502,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 20,
+ 150.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-cli",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 25297825,
+ "isDeleted": false,
+ "id": "a-gw-core",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 330.0,
+ "y": 100,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 100.0,
+ "height": 80,
+ "seed": 25297825,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 100.0,
+ 80
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-gw",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 15121308,
+ "isDeleted": false,
+ "id": "a-tui-core",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 530.0,
+ "y": 100,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 100.0,
+ "height": 80,
+ "seed": 15121308,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -100.0,
+ 80
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-tui",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 82177578,
+ "isDeleted": false,
+ "id": "a-acp-core",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 640,
+ "y": 70.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 20,
+ "height": 150.0,
+ "seed": 82177578,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -20,
+ 150.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-acp",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 45033317,
+ "isDeleted": false,
+ "id": "a-core-prov",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 240,
+ "y": 220.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 20,
+ "height": 150.0,
+ "seed": 45033317,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -20,
+ 150.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-prov",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 82766512,
+ "isDeleted": false,
+ "id": "a-core-tools",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 430.0,
+ "y": 260,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 100.0,
+ "height": 80,
+ "seed": 82766512,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -100.0,
+ 80
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-tools",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 55201750,
+ "isDeleted": false,
+ "id": "a-core-state",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 430.0,
+ "y": 260,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 100.0,
+ "height": 80,
+ "seed": 55201750,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 100.0,
+ 80
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-state",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 56536232,
+ "isDeleted": false,
+ "id": "a-core-cron",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 620,
+ "y": 220.0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 20,
+ "height": 150.0,
+ "seed": 56536232,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 20,
+ 150.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-cron",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 76234960,
+ "isDeleted": false,
+ "id": "a-core-edges",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 430.0,
+ "y": 260,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 0.0,
+ "height": 200,
+ "seed": 76234960,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 200
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-core",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-edges",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ },
+ {
+ "type": "arrow",
+ "version": 1,
+ "versionNonce": 2831145,
+ "isDeleted": false,
+ "id": "a-prov-models",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "angle": 0,
+ "x": 130.0,
+ "y": 400,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "width": 0.0,
+ "height": 60,
+ "seed": 2831145,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "r-prov",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "r-models",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow"
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/diagrams/architecture.html b/docs/diagrams/architecture.html
new file mode 100644
index 000000000000..64b681c8a354
--- /dev/null
+++ b/docs/diagrams/architecture.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+architecture
+
+
+
+