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 + + + +

architecture

+
~20 chat platformsCLI (hermes_cli)Gateway (gateway/)TUI (tui_gateway)ACP + DesktopAgent Core (agent/)turn loop · context · cacheProvidersplugins/model-providersTools (tools/)State (hermes_state)Cron (cron/)Skills · Plugins · MCPs (the edges)Any model endpoint
+ + +