AGENTS.md
AGENTS.mdAGENTS.mdroot
Quality
84/100
Scores the file, not the repository.Length
9,766 words
81 headings · 25 code blocksRepository
225k
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.1# Hermes Agent - Development Guide23Instructions for AI coding assistants and developers working on the hermes-agent codebase.45**Never give up on the right solution.**67## What Hermes Is89Hermes is a personal AI agent that runs the same agent core across a CLI, a10messaging gateway (Telegram, Discord, Slack, and ~20 other platforms), a TUI,11and an Electron desktop app. It learns across sessions (memory + skills),12delegates to subagents, runs scheduled jobs, and drives a real terminal and13browser. It is extended primarily through **plugins and skills**, not by14growing the core.1516Two properties shape almost every design decision and are the lens for17reviewing any change:1819- **Per-conversation prompt caching is sacred.** A long-lived conversation20 reuses a cached prefix every turn. Anything that mutates past context,21 swaps toolsets, or rebuilds the system prompt mid-conversation invalidates22 that cache and multiplies the user's cost. We do not do it (the one23 exception is context compression).24- **The core is a narrow waist; capability lives at the edges.** Every model25 tool we add is sent on every API call, so the bar for a new *core* tool is26 high. Most new capability should arrive as a CLI command + skill, a27 service-gated tool, or a plugin — not as core surface.2829## Contribution Rubric — What We Want / What We Don't3031This is the project's intent layer. Use it two ways:32331. **For humans and for your own work** — what gets merged and what gets34 rejected, so a contribution aims at the target.352. **For automated review (the triage sweeper)** — guidance on when a PR is36 safe to close on the three allowed reasons (`implemented_on_main`,37 `cannot_reproduce`, `incoherent`) and, just as important, **when NOT to38 close** one. Taste-based "we don't want this / out of scope" closes are NOT39 an automated decision — those stay with a human maintainer. The sweeper's40 job here is to recognize design intent and *avoid wrongly closing a41 legitimate contribution*, not to make the won't-implement call itself.4243Read the balance right: Hermes ships a **lot** — most merges are bug fixes to44real reported behavior, and the product surface (platforms, channels,45providers, models, desktop/TUI features) expands aggressively and on purpose.46The restraint below is aimed squarely at the **core agent + the model tool47schema**, the one place where every addition is paid for on every API call.48"Smallest footprint" governs *how a capability is wired into the core*, NOT49whether the product is allowed to grow. We are expansive at the edges and50conservative at the waist.5152### What we want5354- **Fix real bugs, well.** The bulk of what lands is `fix(...)` against an55 actual reported symptom. A good fix reproduces the symptom on current56 `main`, points to the exact line where it manifests, and fixes the whole bug57 class — sibling call paths included — not just the one site the reporter hit.58- **Expand reach at the edges.** New platform adapters, channels, providers,59 models, and desktop/TUI/dashboard features are welcome and land routinely,60 including large ones (a new messaging channel, a session-cap feature, a61 Windows PTY bridge). Breadth in the product is a goal, not a footprint62 concern — as long as it integrates with the existing setup/config UX63 (`hermes tools`, `hermes setup`, auto-install) rather than bolting on a raw64 env var.65- **Refactor god-files into clean modules.** Extracting a multi-thousand-line66 cluster out of `cli.py` / `run_agent.py` / `gateway/run.py` into a focused67 mixin or module is wanted work, even when the diff is huge and mechanical68 (large `+N/-N` refactors merge regularly). The "every line traces to the69 request" test applies to *feature* PRs; a declared refactor's request IS the70 extraction.71- **Keep the core narrow.** New *model tools* are the expensive exception —72 every tool ships on every API call. Prefer, in order: extend existing code →73 CLI command + skill → service-gated tool (`check_fn`) → plugin → MCP server74 in the catalog → new core tool (last resort). See "The Footprint Ladder."75- **Extend, don't duplicate.** Before adding a module/manager/hook, check76 whether existing infrastructure already covers the use case. When several PRs77 integrate the same *category*, design one shared interface instead of merging78 them one at a time (see the ABC + orchestrator note under the Footprint79 Ladder).80- **Behavior contracts over snapshots.** Tests should assert how two pieces of81 data must relate (invariants), not freeze a current value (model lists,82 config version literals, enumeration counts). See "Don't write83 change-detector tests."84- **E2E validation, not just green unit mocks.** For anything touching85 resolution chains, config propagation, security boundaries, remote86 backends, or file/network I/O, exercise the real path with real imports87 against a temp `HERMES_HOME`. Mocks hide integration bugs.88- **Cache-, alternation-, and invariant-safe.** Preserve prompt caching, strict89 message role alternation (never two same-role messages in a row; never a90 synthetic user message injected mid-loop), and a system prompt that is91 byte-stable for the life of a conversation.92- **Contributor credit preserved.** Salvage external work by cherry-picking93 (rebase-merge) so authorship survives in git history; don't reimplement from94 scratch when you can build on top.9596### What we don't want (rejected even when well-built)9798- **Speculative infrastructure.** Hooks, callbacks, or extension points with no99 concrete consumer. Adding a hook is easy; removing one after plugins depend100 on it is hard. A hook is NOT speculative if a contributor has a real, stated101 use case — even if the consumer ships separately.102- **New `HERMES_*` env vars for non-secret config.** `.env` is for secrets103 only (API keys, tokens, passwords). All behavioral settings — timeouts,104 thresholds, feature flags, display prefs — go in `config.yaml`. Bridge to an105 internal env var if the mechanism needs one, but user-facing docs point to106 `config.yaml`. Reject PRs that tell users to "set X in your .env" unless X107 is a credential.108- **A new core tool when terminal + file already do the job, or when a skill109 would.** If the only barrier is file visibility on a remote backend, fix the110 mount, not the toolset.111- **Lazy-reading escape hatches on instructional tools.** No `offset`/`limit`112 pagination on tools that load content the agent must read fully (skills,113 prompts, playbooks). Models will read page 1 and skip the rest.114- **"Fixes" that destroy the feature they secure.** A mitigation that kills the115 feature's purpose is the wrong mitigation. Read the original commit's intent116 (`git log -p -S`) before restricting behavior; find a fix that preserves the117 feature.118- **Outbound telemetry / usage attribution without opt-in gating.** No new119 analytics, third-party identifier tagging, or attribution tags until a120 generic user-facing opt-in (config gate + setup prompt + `hermes tools`121 toggle) exists. Park behind a label, do not merge.122- **Change-detector tests, cache-breaking mid-conversation, dead code wired in123 without E2E proof, and plugins that touch core files.** Plugins live in their124 own directory and work within the ABCs/hooks we provide; if a plugin needs125 more, widen the generic plugin surface, don't special-case it in core.126- **Third-party products / other people's projects integrated into the core127 tree.** Observability backends, vendor SaaS integrations, analytics dashboards,128 and similar "someone else's product" plugins do NOT land under `plugins/` in129 this repo. They place an ongoing maintenance burden on us to keep them working130 against a fast-moving core, for a backend we don't own. Ship them as a131 **standalone plugin repo** users install into `~/.hermes/plugins/` (or via a132 pip entry point), and promote them in the Nous Research Discord133 (`#plugins-skills-and-skins`). This is a coupling-and-maintenance decision, not134 a quality bar — the plugin can be excellent and still be a close. PRs that add135 such a directory to the tree are closed with a pointer to publish it as its own136 repo.137138### Before you call it a bug — verify the premise (and when NOT to close)139140The most common reason a well-written PR gets closed is not code quality — it141is that the change is built on a **wrong premise**, or it treats an142**intentional design as a gap**. These patterns cut both ways: they tell a143human reviewer what to scrutinize, and they tell the automated sweeper when a144PR is NOT safe to close as `implemented_on_main` / `cannot_reproduce` (when in145doubt, leave it open for a human). They are distilled from real closes.146147- **"Intentional design, not a gap."** A limitation that looks like an148 oversight is often deliberate. Before "fixing" a missing link or a149 restriction, ask whether the isolation IS the design. Example: profiles are150 independent islands on purpose — a PR adding live config inheritance from the151 default profile was closed because coupling profiles together is exactly what152 the design prevents (the copy-at-creation `--clone` path already covers the153 legitimate "start from my default" case). Read the original commit's intent154 (`git log -p -S "<symbol>"`) before assuming something is unfinished.155- **"The premise doesn't hold against how X actually works."** A PR's156 justification frequently rests on a wrong mental model of an existing157 mechanism. Trace the real code/runtime before accepting the rationale. Two158 real closes: a rate-limit "re-probe during cooldown" PR (the breaker only159 trips on a *confirmed-empty* account bucket, so re-probing just hammers a160 bucket we've already proven empty); a usage-accumulation fix whose new branch161 **never executes at runtime** because an earlier guard already popped the162 state it depended on. If you can't point to the exact line where the bug163 manifests AND show the fix changes that line's behavior, you haven't verified164 the premise.165- **"This fix was wrong — the absence/omission was deliberate."** Adding the166 obvious-looking missing piece can break things the omission was protecting.167 Example: restoring "missing" `__init__.py` files made a test tree importable168 as a dotted package that shadowed the real plugin, deleting its `register()`169 at import time. The absence was load-bearing.170- **"Overreached / resurrected an approach we'd moved past."** Scope creep that171 supersedes an agreed-on base, or revives a direction the maintainers172 deliberately closed, gets rejected even when the code works. Keep the change173 to the narrow piece that was actually agreed; offer the rest as a focused174 follow-up.175176The throughline: **verify the claim AND the intent against the codebase before177writing or merging a fix.** A confirmed reproduction on current `main` plus a178line-level account of where the fix acts beats a plausible-sounding rationale179every time. When in doubt about intent, it is cheaper to ask than to ship a180fix that fights the design.181182### The Footprint Ladder (new capability decision)183184Each rung adds more permanent surface than the one above. Choose the highest185(least-footprint) rung that correctly solves the problem:1861871. **Extend existing code** — the capability is a variation of something that188 already exists. Zero new surface.1892. **CLI command + skill** — manages config/state/infra expressible as shell190 commands. The agent runs `hermes <subcommand>` guided by a skill. Zero191 model-tool footprint. Default choice for subscriptions, scheduled tasks,192 service setup. Examples: `hermes webhook`, `hermes cron`, `hermes tools`.1933. **Service-gated tool (`check_fn`)** — needs structured params/returns AND194 only appears when a prerequisite is configured. Zero footprint otherwise.195 Examples: Home Assistant tools (gated on token), memory-provider tools.1964. **Plugin** — third-party/niche/user-specific capability that doesn't ship in197 core. Lives in `~/.hermes/plugins/` or a pip package, discovered at runtime.1985. **MCP server (in the catalog)** — if the capability genuinely needs to be a199 tool (structured I/O the agent invokes) but isn't core-fundamental, prefer200 building it as an MCP server and adding it to the MCP catalog over growing201 the core toolset. The agent connects to it through the built-in MCP client;202 zero permanent core-schema footprint, and it's reusable by any MCP host.2036. **New core tool** — only when the capability is fundamental, broadly useful204 to nearly every user, and unreachable via terminal + file (or an MCP server).205 Examples of correct core tools: terminal, read_file, web_search,206 browser_navigate.207208When 3+ open PRs try to integrate the same *category* of thing (memory209backends, providers, notifiers), don't merge them one at a time — design an210ABC + orchestrator, wrap the existing built-in as the first provider, and turn211the competing PRs into plugins against that interface.212213## Development Environment214215```bash216# Prefer .venv; fall back to venv if that's what your checkout has.217source .venv/bin/activate # or: source venv/bin/activate218```219220`scripts/run_tests.sh` probes `.venv` first, then `venv`, then221`$HOME/.hermes/hermes-agent/venv` (for worktrees that share a venv with the222main checkout).223224## Project Structure225226File counts shift constantly — don't treat the tree below as exhaustive.227The canonical source is the filesystem. The notes call out the load-bearing228entry points you'll actually edit.229230```231hermes-agent/232├── run_agent.py # AIAgent class — core conversation loop (~12k LOC)233├── model_tools.py # Tool orchestration, discover_builtin_tools(), handle_function_call()234├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list235├── cli.py # HermesCLI class — interactive CLI orchestrator (~11k LOC)236├── hermes_state.py # SessionDB — SQLite session store (FTS5 search)237├── hermes_constants.py # get_hermes_home(), display_hermes_home() — profile-aware paths238├── hermes_logging.py # setup_logging() — agent.log / errors.log / gateway.log (profile-aware)239├── batch_runner.py # Parallel batch processing240├── agent/ # Agent internals (provider adapters, memory, caching, compression, etc.)241├── hermes_cli/ # CLI subcommands, setup wizard, plugins loader, skin engine242├── tools/ # Tool implementations — auto-discovered via tools/registry.py243│ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)244├── gateway/ # Messaging gateway — run.py + session.py + platforms/245│ ├── platforms/ # Adapter per platform (telegram, discord, slack, whatsapp,246│ │ # homeassistant, signal, matrix, mattermost, email, sms,247│ │ # dingtalk, wecom, weixin, feishu, qqbot, bluebubbles,248│ │ # yuanbao, webhook, api_server, ...). See ADDING_A_PLATFORM.md.249│ └── builtin_hooks/ # Extension point for always-registered gateway hooks (none shipped)250├── plugins/ # Plugin system (see "Plugins" section below)251│ ├── memory/ # Memory-provider plugins (honcho, mem0, supermemory, ...)252│ ├── context_engine/ # Context-engine plugins253│ ├── model-providers/ # Inference backend plugins (openrouter, anthropic, gmi, ...)254│ ├── kanban/ # Multi-agent board dispatcher + worker plugin255│ ├── hermes-achievements/ # Gamified achievement tracking256│ ├── observability/ # Metrics / traces / logs plugin257│ ├── image_gen/ # Image-generation providers258│ └── <others>/ # disk-cleanup, google_meet, platforms, spotify,259│ # strike-freedom-cockpit, ...260├── optional-skills/ # Heavier/niche skills shipped but NOT active by default261├── skills/ # Built-in skills bundled with the repo262├── ui-tui/ # Ink (React) terminal UI — `hermes --tui`263│ └── src/ # entry.tsx, app.tsx, gatewayClient.ts + app/components/hooks/lib264├── tui_gateway/ # Python JSON-RPC backend for the TUI265├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)266├── cron/ # Scheduler — jobs.py, scheduler.py267├── scripts/ # run_tests.sh, release.py, auxiliary scripts268├── website/ # Docusaurus docs site269└── tests/ # Pytest suite (~17k tests across ~900 files as of May 2026)270```271272**User config:** `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys only).273**Logs:** `~/.hermes/logs/` — `agent.log` (INFO+), `errors.log` (WARNING+),274`gateway.log` when running the gateway. Profile-aware via `get_hermes_home()`.275Browse with `hermes logs [--follow] [--level ...] [--session ...]`.276277## TypeScript Style278279Applies to TypeScript across Hermes: desktop, TUI, website, and future TS packages.280281- Prefer small nanostores over component state when state is shared, reused, or read by distant UI.282- Let each feature own its atoms. Chat state belongs near chat, shell state near shell, shared state in `src/store`.283- Components that render from an atom should use `useStore`. Non-rendering actions should read with `$atom.get()`.284- Do not pass state through three components when the leaf can subscribe to the atom.285- Keep persistence beside the atom that owns it.286- Keep route roots thin. They compose routes and shell; they should not become controllers.287- No monolithic hooks. A hook should own one narrow job.288- Prefer colocated action modules over hidden god hooks.289- If a callback is pure side effect, use the terse void form:290 `onState={st => void setGatewayState(st)}`.291- Async UI handlers should make intent explicit:292 `onClick={() => void save()}`.293- Prefer interfaces for public props and shared object shapes. Avoid `type X = { ... }` for object props.294- Extend React primitives for props: `React.ComponentProps<'button'>`, `React.ComponentProps<typeof Dialog>`, `Omit<...>`, `Pick<...>`.295- Table-driven beats condition ladders when mapping ids, routes, or views.296- `src/app` owns routes, pages, and page-specific components.297- `src/store` owns shared atoms.298- `src/lib` owns shared pure helpers.299300## File Dependency Chain301302```303tools/registry.py (no deps — imported by all tool files)304 ↑305tools/*.py (each calls registry.register() at import time)306 ↑307model_tools.py (imports tools/registry + triggers tool discovery)308 ↑309run_agent.py, cli.py, batch_runner.py, environments/310```311312---313314## AIAgent Class (run_agent.py)315316The real `AIAgent.__init__` takes ~60 parameters (credentials, routing, callbacks,317session context, budget, credential pool, etc.). The signature below is the318minimum subset you'll usually touch — read `run_agent.py` for the full list.319320```python321class AIAgent:322 def __init__(self,323 base_url: str = None,324 api_key: str = None,325 provider: str = None,326 api_mode: str = None, # "chat_completions" | "codex_responses" | ...327 model: str = "", # empty → resolved from config/provider later328 max_iterations: int = 500, # tool-calling iterations (shared with subagents)329 enabled_toolsets: list = None,330 disabled_toolsets: list = None,331 quiet_mode: bool = False,332 save_trajectories: bool = False,333 platform: str = None, # "cli", "telegram", etc.334 session_id: str = None,335 skip_context_files: bool = False,336 skip_memory: bool = False,337 credential_pool=None,338 # ... plus callbacks, thread/user/chat IDs, iteration_budget, fallback_model,339 # checkpoints config, prefill_messages, service_tier, reasoning_config, etc.340 ): ...341342 def chat(self, message: str) -> str:343 """Simple interface — returns final response string."""344345 def run_conversation(self, user_message: str, system_message: str = None,346 conversation_history: list = None, task_id: str = None) -> dict:347 """Full interface — returns dict with final_response + messages."""348```349350### Agent Loop351352The core loop is inside `run_conversation()` — entirely synchronous, with353interrupt checks, budget tracking, and a one-turn grace call:354355```python356while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) \357 or self._budget_grace_call:358 if self._interrupt_requested: break359 response = client.chat.completions.create(model=model, messages=messages, tools=tool_schemas)360 if response.tool_calls:361 for tool_call in response.tool_calls:362 result = handle_function_call(tool_call.name, tool_call.args, task_id)363 messages.append(tool_result_message(result))364 api_call_count += 1365 else:366 return response.content367```368369Messages follow OpenAI format: `{"role": "system/user/assistant/tool", ...}`.370Reasoning content is stored in `assistant_msg["reasoning"]`.371372---373374## CLI Architecture (cli.py)375376- **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete377- **KawaiiSpinner** (`agent/display.py`) — animated faces during API calls, `┊` activity feed for tool results378- `load_cli_config()` in cli.py merges hardcoded defaults + user config YAML379- **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 text380- `process_command()` is a method on `HermesCLI` — dispatches on canonical command name resolved via `resolve_command()` from the central registry381- Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching382383### Slash Command Registry (`hermes_cli/commands.py`)384385All slash commands are defined in a central `COMMAND_REGISTRY` list of `CommandDef` objects. Every downstream consumer derives from this registry automatically:386387- **CLI** — `process_command()` resolves aliases via `resolve_command()`, dispatches on canonical name388- **Gateway** — `GATEWAY_KNOWN_COMMANDS` frozenset for hook emission, `resolve_command()` for dispatch389- **Gateway help** — `gateway_help_lines()` generates `/help` output390- **Telegram** — `telegram_bot_commands()` generates the BotCommand menu391- **Slack** — `slack_subcommand_map()` generates `/hermes` subcommand routing392- **Autocomplete** — `COMMANDS` flat dict feeds `SlashCommandCompleter`393- **CLI help** — `COMMANDS_BY_CATEGORY` dict feeds `show_help()`394395### Adding a Slash Command3963971. Add a `CommandDef` entry to `COMMAND_REGISTRY` in `hermes_cli/commands.py`:398```python399CommandDef("mycommand", "Description of what it does", "Session",400 aliases=("mc",), args_hint="[arg]"),401```4022. Add handler in `HermesCLI.process_command()` in `cli.py`:403```python404elif canonical == "mycommand":405 self._handle_mycommand(cmd_original)406```4073. If the command is available in the gateway, add a handler in `gateway/run.py`:408```python409if canonical == "mycommand":410 return await self._handle_mycommand(event)411```4124. For persistent settings, use `save_config_value()` in `cli.py`413414**CommandDef fields:**415- `name` — canonical name without slash (e.g. `"background"`)416- `description` — human-readable description417- `category` — one of `"Session"`, `"Configuration"`, `"Tools & Skills"`, `"Info"`, `"Exit"`418- `aliases` — tuple of alternative names (e.g. `("bg",)`)419- `args_hint` — argument placeholder shown in help (e.g. `"<prompt>"`, `"[name]"`)420- `cli_only` — only available in the interactive CLI421- `gateway_only` — only available in messaging platforms422- `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.423424**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.425426---427428## TUI Architecture (ui-tui + tui_gateway)429430The TUI is a full replacement for the classic (prompt_toolkit) CLI, activated via `hermes --tui` or `HERMES_TUI=1`.431432### Process Model433434```435hermes --tui436 └─ Node (Ink) ──stdio JSON-RPC── Python (tui_gateway)437 │ └─ AIAgent + tools + sessions438 └─ renders transcript, composer, prompts, activity439```440441TypeScript owns the screen. Python owns sessions, tools, model calls, and slash command logic.442443### Transport444445Newline-delimited JSON-RPC over stdio. Requests from Ink, events from Python. See `tui_gateway/server.py` for the full method/event catalog.446447### Key Surfaces448449| Surface | Ink component | Gateway method |450|---------|---------------|----------------|451| Chat streaming | `app.tsx` + `messageLine.tsx` | `prompt.submit` → `message.delta/complete` |452| Tool activity | `thinking.tsx` | `tool.start/progress/complete` |453| Approvals | `prompts.tsx` | `approval.respond` ← `approval.request` |454| Clarify/sudo/secret | `prompts.tsx`, `maskedPrompt.tsx` | `clarify/sudo/secret.respond` |455| Session picker | `sessionPicker.tsx` | `session.list/resume` |456| Slash commands | Local handler + fallthrough | `slash.exec` → `_SlashWorker`, `command.dispatch` |457| Completions | `useCompletion` hook | `complete.slash`, `complete.path` |458| Theming | `theme.ts` + `branding.tsx` | `gateway.ready` with skin data |459460### Slash Command Flow4614621. Built-in client commands (`/help`, `/quit`, `/clear`, `/resume`, `/copy`, `/paste`, etc.) handled locally in `app.tsx`4632. Everything else → `slash.exec` (runs in persistent `_SlashWorker` subprocess) → `command.dispatch` fallback464465### Dev Commands466467```bash468cd ui-tui469npm install # first time470npm run dev # watch mode (rebuilds hermes-ink + tsx --watch)471npm start # production472npm run build # full build (hermes-ink + tsc)473npm run typecheck # typecheck only (tsc --noEmit)474npm run lint # eslint475npm run fmt # prettier476npm test # vitest477```478479### TUI in the Dashboard (`hermes dashboard` → `/chat`)480481The 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`.482483- 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.484- `/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).485- The server spawns whatever `hermes --tui` would spawn, through `ptyprocess` (POSIX PTY — WSL works, native Windows does not).486- Frames: raw PTY bytes each direction; resize via `\x1b[RESIZE:<cols>;<rows>]` intercepted on the server and applied with `TIOCSWINSZ`.487488**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.489490**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.491492### Electron Desktop Chat App (`apps/desktop/`)493494A **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`.495496**Slash commands in the desktop app are curated client-side, then dispatched to the backend.** The pipeline:497498- **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.499- **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.500 - `isDesktopSlashCommand(name)` — gates **execution**. Returns true for built-ins AND for any non-built-in (skill / quick command), so typed extension commands run.501 - `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`.502 - `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.)503- **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.504505**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).506507---508509## Adding New Tools510511Before adding any tool, settle the footprint question first (see "The512Footprint Ladder" in the Contribution Rubric): most capabilities should NOT513be core tools. For custom or local-only tools, do **not** edit Hermes core.514Use the plugin route instead: create `~/.hermes/plugins/<name>/plugin.yaml`515and `~/.hermes/plugins/<name>/__init__.py`, then register tools with516`ctx.register_tool(...)`. Plugin toolsets are discovered automatically and can be517enabled or disabled without touching `tools/` or `toolsets.py`.518519Use the built-in route below only when the user is explicitly contributing a new520core Hermes tool that should ship in the base system.521522Built-in/core tools require changes in **2 files**:523524**1. Create `tools/your_tool.py`:**525```python526import json, os527from tools.registry import registry528529def check_requirements() -> bool:530 return bool(os.getenv("EXAMPLE_API_KEY"))531532def example_tool(param: str, task_id: str = None) -> str:533 return json.dumps({"success": True, "data": "..."})534535registry.register(536 name="example_tool",537 toolset="example",538 schema={"name": "example_tool", "description": "...", "parameters": {...}},539 handler=lambda args, **kw: example_tool(param=args.get("param", ""), task_id=kw.get("task_id")),540 check_fn=check_requirements,541 requires_env=["EXAMPLE_API_KEY"],542)543```544545**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.546547Auto-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.548549The registry handles schema collection, dispatch, availability checking, and error wrapping. All handlers MUST return a JSON string.550551**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`.552553**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.554555**Agent-level tools** (todo, memory): intercepted by `run_agent.py` before `handle_function_call()`. See `tools/todo_tool.py` for the pattern.556557---558559## Dependency Pinning Policy560561All dependencies must have upper bounds to limit supply-chain attack surface.562This policy was established after the litellm compromise (PR #2796, #2810) and563reinforced after the Mini Shai-Hulud worm campaign (May 2026).564565| Source type | Treatment | Example |566|---|---|---|567| PyPI package | `>=floor,<next_major` | `"httpx>=0.28.1,<1"` |568| Git URL | Commit SHA | `git+https://...@<40-char-sha>` |569| GitHub Actions | Commit SHA + comment | `uses: actions/checkout@<sha> # v4` |570| CI-only pip | `==exact` | `pyyaml==6.0.2` |571572**When adding a new dependency to `pyproject.toml`:**5731. Pin to `>=current_version,<next_major` for post-1.0 (e.g. `>=1.5.0,<2`).5742. For pre-1.0 packages, use `<0.(current_minor + 2)` (e.g. `>=0.29,<0.32`).5753. Never commit a bare `>=X.Y.Z` without a ceiling — CI and reviewers will reject it.5764. Run `uv lock` to regenerate `uv.lock` with hashes.577578Reference: #2810 (bounds pass), #9801 (SHA pinning + audit CI).579580---581582## Adding Configuration583584### config.yaml options:5851. Add to `DEFAULT_CONFIG` in `hermes_cli/config.py`5862. Bump `_config_version` (check the current value at the top of `DEFAULT_CONFIG`)587 ONLY if you need to actively migrate/transform existing user config588 (renaming keys, changing structure). Adding a new key to an existing589 section is handled automatically by the deep-merge and does NOT require590 a version bump.591592### Top-level `config.yaml` sections (non-exhaustive):593594`model`, `agent`, `terminal`, `compression`, `display`, `stt`, `tts`,595`memory`, `security`, `delegation`, `smart_model_routing`, `checkpoints`,596`auxiliary`, `curator`, `skills`, `gateway`, `logging`, `cron`, `profiles`,597`plugins`, `honcho`.598599`auxiliary` holds per-task overrides for side-LLM work (curator, vision,600embedding, title generation, session_search, etc.) — each task can pin601its own provider/model/base_url/max_tokens/reasoning_effort. See602`agent/auxiliary_client.py::_resolve_auto` for resolution order.603604`curator` holds the background skill-maintenance config —605`enabled`, `interval_hours`, `min_idle_hours`, `stale_after_days`,606`archive_after_days`, `backup` (nested).607608### .env variables (SECRETS ONLY — API keys, tokens, passwords):6091. Add to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` with metadata:610```python611"NEW_API_KEY": {612 "description": "What it's for",613 "prompt": "Display name",614 "url": "https://...",615 "password": True,616 "category": "tool", # provider, tool, messaging, setting617},618```619620Non-secret settings (timeouts, thresholds, feature flags, paths, display621preferences) belong in `config.yaml`, not `.env`. If internal code needs an622env var mirror for backward compatibility, bridge it from `config.yaml` to623the env var in code (see `gateway_timeout`, `terminal.cwd` → `TERMINAL_CWD`).624625### Config loaders (three paths — know which one you're in):626627| Loader | Used by | Location |628|--------|---------|----------|629| `load_cli_config()` | CLI mode | `cli.py` — merges CLI-specific defaults + user YAML |630| `load_config()` | `hermes tools`, `hermes setup`, most CLI subcommands | `hermes_cli/config.py` — merges `DEFAULT_CONFIG` + user YAML |631| Direct YAML load | Gateway runtime | `gateway/run.py` + `gateway/config.py` — reads user YAML raw |632633If you add a new key and the CLI sees it but the gateway doesn't (or vice634versa), you're on the wrong loader. Check `DEFAULT_CONFIG` coverage.635636### Working directory:637- **CLI** — uses the process's current directory (`os.getcwd()`).638- **Messaging** — uses `terminal.cwd` from `config.yaml`. The gateway bridges this639 to the `TERMINAL_CWD` env var for child tools. **`MESSAGING_CWD` has been640 removed** — the config loader prints a deprecation warning if it's set in641 `.env`. Same for `TERMINAL_CWD` in `.env`; the canonical setting is642 `terminal.cwd` in `config.yaml`.643644---645646## Skin/Theme System647648The 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.649650### Architecture651652```653hermes_cli/skin_engine.py # SkinConfig dataclass, built-in skins, YAML loader654~/.hermes/skins/*.yaml # User-installed custom skins (drop-in)655```656657- `init_skin_from_config()` — called at CLI startup, reads `display.skin` from config658- `get_active_skin()` — returns cached `SkinConfig` for the current skin659- `set_active_skin(name)` — switches skin at runtime (used by `/skin` command)660- `load_skin(name)` — loads from user skins first, then built-ins, then falls back to default661- Missing skin values inherit from the `default` skin automatically662663### What skins customize664665| Element | Skin Key | Used By |666|---------|----------|---------|667| Banner panel border | `colors.banner_border` | `banner.py` |668| Banner panel title | `colors.banner_title` | `banner.py` |669| Banner section headers | `colors.banner_accent` | `banner.py` |670| Banner dim text | `colors.banner_dim` | `banner.py` |671| Banner body text | `colors.banner_text` | `banner.py` |672| Response box border | `colors.response_border` | `cli.py` |673| Spinner faces (waiting) | `spinner.waiting_faces` | `display.py` |674| Spinner faces (thinking) | `spinner.thinking_faces` | `display.py` |675| Spinner verbs | `spinner.thinking_verbs` | `display.py` |676| Spinner wings (optional) | `spinner.wings` | `display.py` |677| Tool output prefix | `tool_prefix` | `display.py` |678| Per-tool emojis | `tool_emojis` | `display.py` → `get_tool_emoji()` |679| Agent name | `branding.agent_name` | `banner.py`, `cli.py` |680| Welcome message | `branding.welcome` | `cli.py` |681| Response box label | `branding.response_label` | `cli.py` |682| Prompt symbol | `branding.prompt_symbol` | `cli.py` |683684### Built-in skins685686- `default` — Classic Hermes gold/kawaii (the current look)687- `ares` — Crimson/bronze war-god theme with custom spinner wings688- `mono` — Clean grayscale monochrome689- `slate` — Cool blue developer-focused theme690691### Adding a built-in skin692693Add to `_BUILTIN_SKINS` dict in `hermes_cli/skin_engine.py`:694695```python696"mytheme": {697 "name": "mytheme",698 "description": "Short description",699 "colors": { ... },700 "spinner": { ... },701 "branding": { ... },702 "tool_prefix": "┊",703},704```705706### User skins (YAML)707708Users create `~/.hermes/skins/<name>.yaml`:709710```yaml711name: cyberpunk712description: Neon-soaked terminal theme713714colors:715 banner_border: "#FF00FF"716 banner_title: "#00FFFF"717 banner_accent: "#FF1493"718719spinner:720 thinking_verbs: ["jacking in", "decrypting", "uploading"]721 wings:722 - ["⟨⚡", "⚡⟩"]723724branding:725 agent_name: "Cyber Agent"726 response_label: " ⚡ Cyber "727728tool_prefix: "▏"729```730731Activate with `/skin cyberpunk` or `display.skin: cyberpunk` in config.yaml.732733---734735## Plugins736737Hermes has two plugin surfaces. Both live under `plugins/` in the repo so738repo-shipped plugins can be discovered alongside user-installed ones in739`~/.hermes/plugins/` and pip-installed entry points.740741### General plugins (`hermes_cli/plugins.py` + `plugins/<name>/`)742743`PluginManager` discovers plugins from `~/.hermes/plugins/`, `./.hermes/plugins/`,744and pip entry points. Each plugin exposes a `register(ctx)` function that745can:746747- Register Python-callback lifecycle hooks:748 `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,749 `on_session_start`, `on_session_end`750- Register new tools via `ctx.register_tool(...)`751- Register CLI subcommands via `ctx.register_cli_command(...)` — the752 plugin's argparse tree is wired into `hermes` at startup so753 `hermes <pluginname> <subcmd>` works with no change to `main.py`754755Hooks are invoked from `model_tools.py` (pre/post tool) and `run_agent.py`756(lifecycle). **Discovery timing pitfall:** `discover_plugins()` only runs757as a side effect of importing `model_tools.py`. Code paths that read plugin758state without importing `model_tools.py` first must call `discover_plugins()`759explicitly (it's idempotent).760761### Memory-provider plugins (`plugins/memory/<name>/`)762763Separate discovery system for pluggable memory backends. Current built-in764providers include **honcho, mem0, supermemory, byterover, hindsight,765holographic, openviking, retaindb**.766767Each provider implements the `MemoryProvider` ABC (see `agent/memory_provider.py`)768and is orchestrated by `agent/memory_manager.py`. Lifecycle hooks include769`sync_turn(turn_messages)`, `prefetch(query)`, `shutdown()`, and optional770`post_setup(hermes_home, config)` for setup-wizard integration.771772**CLI commands via `plugins/memory/<name>/cli.py`:** if a memory plugin773defines `register_cli(subparser)`, `discover_plugin_cli_commands()` finds774it at argparse setup time and wires it into `hermes <plugin>`. The775framework only exposes CLI commands for the **currently active** memory776provider (read from `memory.provider` in config.yaml), so disabled777providers don't clutter `hermes --help`.778779**Rule (Teknium, May 2026):** plugins MUST NOT modify core files780(`run_agent.py`, `cli.py`, `gateway/run.py`, `hermes_cli/main.py`, etc.).781If a plugin needs a capability the framework doesn't expose, expand the782generic plugin surface (new hook, new ctx method) — never hardcode783plugin-specific logic into core. PR #5295 removed 95 lines of hardcoded784honcho argparse from `main.py` for exactly this reason.785786**No new in-tree memory providers (policy, May 2026):** the set of787built-in memory providers under `plugins/memory/` is closed. New memory788backends must ship as **standalone plugin repos** that users install789into `~/.hermes/plugins/` (or via pip entry points) — they implement790the same `MemoryProvider` ABC, register through the same discovery791path, and integrate via `hermes memory setup` / `post_setup()` without792landing in this tree. PRs that add a new directory under793`plugins/memory/` will be closed with a pointer to publish the794provider as its own repo. Existing in-tree providers stay; bug fixes795to them are welcome.796797**No new third-party-product plugins in-tree (policy, June 2026):** the798same rule applies beyond memory providers. Plugins that integrate799someone else's product or project — observability/metrics backends,800vendor SaaS connectors, analytics dashboards, paid-service tie-ins —801must ship as **standalone plugin repos** that users install into802`~/.hermes/plugins/` (or via pip entry points). They register through803the existing plugin discovery path and use the ABCs/hooks/ctx surface804we expose; nothing special is needed in core. The reason is805maintenance load: every product we absorb into the tree becomes our806burden to keep working against a fast-moving core, for a backend we807don't own. Promote standalone plugins in the Nous Research Discord808(`#plugins-skills-and-skins`). PRs that add such a directory under809`plugins/` are closed with a pointer to publish it as its own repo —810this is a coupling decision, not a quality judgment. (The811`observability/`, `kanban/`, `disk-cleanup/`, etc. directories already812in the tree are existing precedent, not an invitation to add more813third-party-product plugins alongside them.)814815### Model-provider plugins (`plugins/model-providers/<name>/`)816817Every inference backend (openrouter, anthropic, gmi, deepseek, nvidia, …)818ships as a plugin here. Each plugin's `__init__.py` calls819`providers.register_provider(ProviderProfile(...))` at module load.820`providers/__init__.py._discover_providers()` is a **lazy, separate821discovery system** — scanned on first `get_provider_profile()` or822`list_providers()` call, NOT by the general PluginManager.823824Scan order:8251. Bundled: `<repo>/plugins/model-providers/<name>/`8262. User: `$HERMES_HOME/plugins/model-providers/<name>/`8273. Legacy: `<repo>/providers/<name>.py` (back-compat)828829User plugins of the same name override bundled ones — `register_provider()`830is last-writer-wins. This lets third parties swap out any built-in831profile without a repo patch.832833The general PluginManager records `kind: model-provider` manifests but does834NOT import them (would double-instantiate `ProviderProfile`). Plugins835without an explicit `kind:` get auto-coerced via a source-text heuristic836(`register_provider` + `ProviderProfile` in `__init__.py`).837838Full authoring guide: `website/docs/developer-guide/model-provider-plugin.md`.839840### Dashboard / context-engine / image-gen plugin directories841842`plugins/context_engine/`, `plugins/image_gen/`, etc. follow the same843pattern (ABC + orchestrator + per-plugin directory). Context engines844plug into `agent/context_engine.py`; image-gen providers into845`agent/image_gen_provider.py`. Reference / docs-companion plugins846(`example-dashboard`, `strike-freedom-cockpit`, `plugin-llm-example`,847`plugin-llm-async-example`) live in the848[`hermes-example-plugins`](https://github.com/NousResearch/hermes-example-plugins)849companion repo, not in this tree.850851---852853## Skills854855Two parallel surfaces:856857- **`skills/`** — built-in skills shipped and loadable by default.858 Organized by category directories (e.g. `skills/github/`, `skills/mlops/`).859- **`optional-skills/`** — heavier or niche skills shipped with the repo but860 NOT active by default. Installed explicitly via861 `hermes skills install official/<category>/<skill>`. Adapter lives in862 `tools/skills_hub.py` (`OptionalSkillSource`). Categories include863 `autonomous-ai-agents`, `blockchain`, `communication`, `creative`,864 `devops`, `email`, `health`, `mcp`, `migration`, `mlops`, `productivity`,865 `research`, `security`, `web-development`.866867When reviewing skill PRs, check which directory they target — heavy-dep or868niche skills belong in `optional-skills/`.869870### SKILL.md frontmatter871872Standard fields: `name`, `description`, `version`, `author`, `license`,873`platforms` (OS-gating list: `[macos]`, `[linux, macos]`, ...),874`metadata.hermes.tags`, `metadata.hermes.category`,875`metadata.hermes.related_skills`, `metadata.hermes.config` (config.yaml876settings the skill needs — stored under `skills.config.<key>`, prompted877during setup, injected at load time).878879Top-level `tags:` and `category:` are also accepted and mirrored from880`metadata.hermes.*` by the loader.881882### Skill authoring standards (HARDLINE)883884Every new or modernized skill — bundled, optional, or contributed —885must meet these standards before merge. Reviewers reject PRs that886violate them.8878881. **`description` ≤ 60 characters, one sentence, ends with a period.**889 Long descriptions bloat skill listings and dilute the model's890 attention when many skills are loaded. State the capability, not891 the implementation. No marketing words ("powerful",892 "comprehensive", "seamless", "advanced"). Don't repeat the skill893 name. Verify with:894```python895 import re, pathlib896 m = re.search(r'^description: (.*)$',897 pathlib.Path('skills/<cat>/<name>/SKILL.md').read_text(),898 re.MULTILINE)899 assert len(m.group(1)) <= 60, len(m.group(1))900```9019022. **Tools referenced in SKILL.md prose must be native Hermes tools or903 MCP servers the skill explicitly expects.** When the skill needs a904 capability, point at the proper tool by name in backticks905 (`` `terminal` ``, `` `web_extract` ``, `` `read_file` ``,906 `` `patch` ``, `` `search_files` ``, `` `vision_analyze` ``,907 `` `browser_navigate` ``, `` `delegate_task` ``, etc.). Do NOT908 name shell utilities the agent already has wrapped — `grep` →909 `search_files`, `cat`/`head`/`tail` → `read_file`, `sed`/`awk` →910 `patch`, `find`/`ls` → `search_files target='files'`. If the skill911 depends on an MCP server, name the MCP server and document the912 expected setup in `## Prerequisites`. Anything else (third-party913 CLIs, shell pipelines, etc.) is fair game inside script files but914 should not be the headline interaction surface in the prose.9159163. **`platforms:` gating audited against actual script imports.**917 Skills that use POSIX-only primitives (`fcntl`, `termios`,918 `os.setsid`, `os.kill(pid, 0)` for liveness, `/proc`, `/tmp`919 hardcoded, `signal.SIGKILL`, bash heredocs, `osascript`, `apt`,920 `systemctl`) must declare their supported platforms. Default921 posture: try to fix it cross-platform first — `tempfile.gettempdir`,922 `pathlib.Path`, `psutil.pid_exists`, Python-level filtering instead923 of `grep`. Gate to a narrower set only when the dependency is924 genuinely platform-bound.9259264. **`author` credits the human contributor first.** For external927 contributions, the contributor's real name + GitHub handle goes928 first; "Hermes Agent" is the secondary collaborator. If the929 contributor's commit shows "Hermes Agent" as author (because they930 used Hermes to draft the skill), replace it with their actual name931 — credit the human, not the tool.9329335. **SKILL.md body uses the modern section order.** `# <Skill> Skill`934 title, 2-3 sentence intro stating what it does and doesn't do,935 `## When to Use`, `## Prerequisites`, `## How to Run`,936 `## Quick Reference`, `## Procedure`, `## Pitfalls`,937 `## Verification`. Target ~200 lines for a complex skill,938 ~100 lines for a simple one. Cut redundant intro fluff, marketing939 prose, and re-explanations of env vars already in940 `## Prerequisites`.9419426. **Scripts go in `scripts/`, references in `references/`,943 templates in `templates/`.** Don't expect the model to inline-write944 parsers, XML walkers, or non-trivial logic every call — ship a945 helper script. Reference it from SKILL.md by path relative to the946 skill directory.9479487. **Tests live at `tests/skills/test_<skill>_skill.py`** and use only949 stdlib + pytest + `unittest.mock`. No live network calls. Run via950 `scripts/run_tests.sh tests/skills/test_<skill>_skill.py -q`.9519528. **`.env.example` additions are isolated to a clearly delimited953 block.** Don't touch the surrounding file — contributor-supplied954 `.env.example` versions are usually stale and edits outside the955 skill's own block must be dropped during salvage.956957The full salvage / modernization checklist for external skill PRs958lives in the `hermes-agent-dev` skill at959`references/new-skill-pr-salvage.md` — load it before polishing960contributor skill PRs.961962---963964## Toolsets965966All toolsets are defined in `toolsets.py` as a single `TOOLSETS` dict.967Each platform's adapter picks a base toolset (e.g. Telegram uses968`"messaging"`); `_HERMES_CORE_TOOLS` is the default bundle most969platforms inherit from.970971Current toolset keys: `browser`, `clarify`, `code_execution`, `cronjob`,972`debugging`, `delegation`, `discord`, `discord_admin`, `feishu_doc`,973`feishu_drive`, `file`, `homeassistant`, `image_gen`, `kanban`, `memory`,974`messaging`, `moa`, `rl`, `safe`, `search`, `session_search`, `skills`,975`spotify`, `terminal`, `todo`, `tts`, `video`, `vision`, `web`, `yuanbao`.976977Enable/disable per platform via `hermes tools` (the curses UI) or the978`tools.<platform>.enabled` / `tools.<platform>.disabled` lists in979`config.yaml`.980981---982983## Delegation (`delegate_task`)984985`tools/delegate_tool.py` spawns a subagent with an isolated986context + terminal session. By default the parent waits for the987child's summary before continuing its own loop. With `background=true`,988Hermes returns a delegation id immediately and the result re-enters the989conversation later through the async-delegation completion queue.990991Two shapes:992993- **Single:** pass `goal` (+ optional `context`, `toolsets`).994- **Batch (parallel):** pass `tasks: [...]` — each gets its own subagent995 running concurrently. Concurrency is capped by996 `delegation.max_concurrent_children` (default 3).997998Roles:9991000- `role="leaf"` (default) — focused worker. Cannot call `delegate_task`,1001 `clarify`, `memory`, `send_message`, `cronjob`. Retains `execute_code`1002 (programmatic tool calling).1003- `role="orchestrator"` — retains `delegate_task` so it can spawn its1004 own workers. Gated by `delegation.orchestrator_enabled` (default true)1005 and bounded by `delegation.max_spawn_depth` (default 2).10061007Key config knobs (under `delegation:` in `config.yaml`):1008`max_concurrent_children`, `max_spawn_depth`, `child_timeout_seconds`,1009`orchestrator_enabled`, `subagent_auto_approve`, `inherit_mcp_toolsets`,1010`max_iterations`.10111012Durability rule: background `delegate_task` is detached from the current1013turn but still process-local. For work that must survive process restart, use1014`cronjob` or `terminal(background=True, notify_on_complete=True)` instead.10151016---10171018## Curator (skill lifecycle)10191020Background skill-maintenance system that tracks usage on agent-created1021skills and auto-archives stale ones. Users never lose skills; archives1022go to `~/.hermes/skills/.archive/` and are restorable.10231024- **Core:** `agent/curator.py` (review loop, auto-transitions, LLM review1025 prompt) + `agent/curator_backup.py` (pre-run tar.gz snapshots).1026- **CLI:** `hermes_cli/curator.py` wires `hermes curator <verb>` where1027 verbs are: `status`, `run`, `pause`, `resume`, `pin`, `unpin`,1028 `archive`, `restore`, `prune`, `backup`, `rollback`.1029- **Telemetry:** `tools/skill_usage.py` owns the sidecar1030 `~/.hermes/skills/.usage.json` — per-skill `use_count`, `view_count`,1031 `patch_count`, `last_activity_at`, `state` (active / stale /1032 archived), `pinned`.10331034Invariants:1035- Curator only touches skills with `created_by: "agent"` provenance —1036 bundled + hub-installed skills are off-limits.1037- Never deletes; max destructive action is archive.1038- Pinned skills are exempt from every auto-transition and from the1039 LLM review pass.1040- `skill_manage(action="delete")` refuses pinned skills; patch/edit/1041 write_file/remove_file go through so the agent can keep improving1042 pinned skills.10431044Config section (`curator:` in `config.yaml`):1045`enabled`, `interval_hours`, `min_idle_hours`, `stale_after_days`,1046`archive_after_days`, `backup.*`.10471048Full user-facing docs: `website/docs/user-guide/features/curator.md`.10491050---10511052## Cron (scheduled jobs)10531054`cron/jobs.py` (job store) + `cron/scheduler.py` (tick loop). Agents1055schedule jobs via the `cronjob` tool; users via `hermes cron <verb>`1056(`list`, `add`, `edit`, `pause`, `resume`, `run`, `remove`) or the1057`/cron` slash command.10581059Supported schedule formats:1060- Duration: `"30m"`, `"2h"`, `"1d"`1061- "every" phrase: `"every 2h"`, `"every monday 9am"`1062- 5-field cron expression: `"0 9 * * *"`1063- ISO timestamp (one-shot): `"2026-06-01T09:00:00Z"`10641065Per-job fields include `skills` (load specific skills), `model` /1066`provider` overrides, `script` (pre-run data-collection script whose1067stdout is injected into the prompt; `no_agent=True` turns the script1068into the entire job), `context_from` (chain job A's last output into1069job B's prompt), `workdir` (run in a specific directory with its1070`AGENTS.md`/`CLAUDE.md` loaded), and multi-platform delivery.10711072Hardening invariants:1073- **3-minute hard interrupt** on cron sessions — runaway agent loops1074 cannot monopolize the scheduler.1075- Catchup window: half the job's period, clamped to 120s–2h.1076- Grace window: 120s for one-shot jobs whose fire time was missed.1077- File lock at `~/.hermes/cron/.tick.lock` prevents duplicate ticks1078 across processes.1079- Cron sessions pass `skip_memory=True` by default; memory providers1080 intentionally do not run during cron.10811082Cron deliveries are **not** mirrored into the target gateway session —1083they land in their own cron session with a header/footer frame so the1084main conversation's message-role alternation stays intact.10851086---10871088## Kanban (multi-agent work queue)10891090Durable SQLite-backed board that lets multiple profiles / workers1091collaborate on shared tasks. Users drive it via `hermes kanban <verb>`;1092workers spawned by the dispatcher drive it via a dedicated `kanban_*`1093toolset so their schema footprint is zero when they're not inside a1094kanban task.10951096- **CLI:** `hermes_cli/kanban.py` wires `hermes kanban` with verbs1097 `init`, `create`, `list` (alias `ls`), `show`, `assign`, `link`,1098 `unlink`, `comment`, `attach`, `attachments`, `attach-rm`, `complete`,1099 `block`, `unblock`, `archive`, `tail`, plus less-commonly-used `watch`,1100 `stats`, `runs`, `log`, `assignees`, `heartbeat`, `notify-*`,1101 `dispatch`, `daemon`, `gc`.1102- **Worker/orchestrator toolset:** `tools/kanban_tools.py` exposes1103 `kanban_show`, `kanban_complete`, `kanban_block`, `kanban_heartbeat`,1104 `kanban_comment`, `kanban_create`, `kanban_link`, `kanban_attach`,1105 `kanban_attach_url`, `kanban_attachments`; profiles that explicitly1106 enable the `kanban` toolset outside a dispatcher-spawned task also get1107 `kanban_list` and `kanban_unblock` for board routing.1108- **Dispatcher:** long-lived loop that (default every 60s) reclaims1109 stale claims, promotes ready tasks, atomically claims, and spawns1110 assigned profiles. Runs **inside the gateway** by default via1111 `kanban.dispatch_in_gateway: true`.1112- **Plugin assets:** `plugins/kanban/dashboard/` (web UI) +1113 `plugins/kanban/systemd/` (`hermes-kanban-dispatcher.service` for1114 standalone dispatcher deployment).11151116Isolation model:1117- **Board** is the hard boundary — workers are spawned with1118 `HERMES_KANBAN_BOARD` pinned in their env so they can't see other1119 boards.1120- **Tenant** is a soft namespace *within* a board — one specialist1121 fleet can serve multiple businesses with workspace-path + memory-key1122 isolation.1123- After `kanban.failure_limit` consecutive non-success attempts on the1124 same task (default: 2), the dispatcher auto-blocks it to prevent spin1125 loops.11261127Full user-facing docs: `website/docs/user-guide/features/kanban.md`.11281129---11301131## Important Policies11321133### Prompt Caching Must Not Break11341135Hermes-Agent ensures caching remains valid throughout a conversation. **Do NOT implement changes that would:**1136- Alter past context mid-conversation1137- Change toolsets mid-conversation1138- Reload memories or rebuild system prompts mid-conversation11391140Cache-breaking forces dramatically higher costs. The ONLY time we alter context is during context compression.11411142Slash commands that mutate system-prompt state (skills, tools, memory, etc.)1143must be **cache-aware**: default to deferred invalidation (change takes1144effect next session), with an opt-in `--now` flag for immediate1145invalidation. See `/skills install --now` for the canonical pattern.11461147### Background Process Notifications (Gateway)11481149When `terminal(background=true, notify_on_complete=true)` is used, the gateway runs a watcher that1150detects process completion and triggers a new agent turn. Control verbosity of background process1151messages with `display.background_process_notifications`1152in config.yaml (or `HERMES_BACKGROUND_NOTIFICATIONS` env var):11531154- `all` — running-output updates + final message (default)1155- `result` — only the final completion message1156- `error` — only the final message when exit code != 01157- `off` — no watcher messages at all11581159---11601161## Profiles: Multi-Instance Support11621163Hermes supports **profiles** — multiple fully isolated instances, each with its own1164`HERMES_HOME` directory (config, API keys, memory, sessions, skills, gateway, etc.).11651166The core mechanism: `_apply_profile_override()` in `hermes_cli/main.py` sets1167`HERMES_HOME` before any module imports. All `get_hermes_home()` references1168automatically scope to the active profile.11691170### Rules for profile-safe code117111721. **Use `get_hermes_home()` for all HERMES_HOME paths.** Import from `hermes_constants`.1173 NEVER hardcode `~/.hermes` or `Path.home() / ".hermes"` in code that reads/writes state.1174```python1175 # GOOD1176 from hermes_constants import get_hermes_home1177 config_path = get_hermes_home() / "config.yaml"11781179 # BAD — breaks profiles1180 config_path = Path.home() / ".hermes" / "config.yaml"1181```118211832. **Use `display_hermes_home()` for user-facing messages.** Import from `hermes_constants`.1184 This returns `~/.hermes` for default or `~/.hermes/profiles/<name>` for profiles.1185```python1186 # GOOD1187 from hermes_constants import display_hermes_home1188 print(f"Config saved to {display_hermes_home()}/config.yaml")11891190 # BAD — shows wrong path for profiles1191 print("Config saved to ~/.hermes/config.yaml")1192```119311943. **Module-level constants are fine** — they cache `get_hermes_home()` at import time,1195 which is AFTER `_apply_profile_override()` sets the env var. Just use `get_hermes_home()`,1196 not `Path.home() / ".hermes"`.119711984. **Tests that mock `Path.home()` must also set `HERMES_HOME`** — since code now uses1199 `get_hermes_home()` (reads env var), not `Path.home() / ".hermes"`:1200```python1201 with patch.object(Path, "home", return_value=tmp_path), \1202 patch.dict(os.environ, {"HERMES_HOME": str(tmp_path / ".hermes")}):1203 ...1204```120512065. **Gateway platform adapters should use token locks** — if the adapter connects with1207 a unique credential (bot token, API key), call `acquire_scoped_lock()` from1208 `gateway.status` in the `connect()`/`start()` method and `release_scoped_lock()` in1209 `disconnect()`/`stop()`. This prevents two profiles from using the same credential.1210 See `plugins/platforms/irc/adapter.py` for the canonical pattern.121112126. **Profile operations are HOME-anchored, not HERMES_HOME-anchored** — `_get_profiles_root()`1213 returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`.1214 This is intentional — it lets `hermes -p coder profile list` see all profiles regardless1215 of which one is active.12161217## Known Pitfalls12181219### DO NOT hardcode `~/.hermes` paths1220Use `get_hermes_home()` from `hermes_constants` for code paths. Use `display_hermes_home()`1221for user-facing print/log messages. Hardcoding `~/.hermes` breaks profiles — each profile1222has its own `HERMES_HOME` directory. This was the source of 5 bugs fixed in PR #3575.12231224### DO NOT introduce new `simple_term_menu` usage1225Existing call sites in `hermes_cli/main.py` remain for legacy fallback only;1226the preferred UI is curses (stdlib) because `simple_term_menu` has1227ghost-duplication rendering bugs in tmux/iTerm2 with arrow keys. New1228interactive menus must use `hermes_cli/curses_ui.py` — see1229`hermes_cli/tools_config.py` for the canonical pattern.12301231### DO NOT use `\033[K` (ANSI erase-to-EOL) in spinner/display code1232Leaks as literal `?[K` text under `prompt_toolkit`'s `patch_stdout`. Use space-padding: `f"\r{line}{' ' * pad}"`.12331234### `_last_resolved_tool_names` is a process-global in `model_tools.py`1235`_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.12361237### DO NOT hardcode cross-tool references in schema descriptions1238Tool 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.12391240### The gateway has TWO message guards — both must bypass approval/control commands1241When an agent is running, messages pass through two sequential guards:1242(1) **base adapter** (`gateway/platforms/base.py`) queues messages in1243`_pending_messages` when `session_key in self._active_sessions`, and1244(2) **gateway runner** (`gateway/run.py`) intercepts `/stop`, `/new`,1245`/queue`, `/status`, `/approve`, `/deny` before they reach1246`running_agent.interrupt()`. Any new command that must reach the runner1247while the agent is blocked (e.g. approval prompts) MUST bypass BOTH1248guards and be dispatched inline, not via `_process_message_background()`1249(which races session lifecycle).12501251### Squash merges from stale branches silently revert recent fixes1252Before squash-merging a PR, ensure the branch is up to date with `main`1253(`git fetch origin main && git reset --hard origin/main` in the worktree,1254then re-apply the PR's commits). A stale branch's version of an unrelated1255file will silently overwrite recent fixes on main when squashed. Verify1256with `git diff HEAD~1..HEAD` after merging — unexpected deletions are a1257red flag.12581259### Don't wire in dead code without E2E validation1260Unused code that was never shipped was dead for a reason. Before wiring an1261unused module into a live code path, E2E test the real resolution chain1262with actual imports (not mocks) against a temp `HERMES_HOME`.12631264### Tests must not write to `~/.hermes/`1265The `_isolate_hermes_home` autouse fixture in `tests/conftest.py` redirects `HERMES_HOME` to a temp dir. Never hardcode `~/.hermes/` paths in tests.12661267**Profile tests**: When testing profile features, also mock `Path.home()` so that1268`_get_profiles_root()` and `_get_default_hermes_home()` resolve within the temp dir.1269Use the pattern from `tests/hermes_cli/test_profiles.py`:1270```python1271@pytest.fixture1272def profile_env(tmp_path, monkeypatch):1273 home = tmp_path / ".hermes"1274 home.mkdir()1275 monkeypatch.setattr(Path, "home", lambda: tmp_path)1276 monkeypatch.setenv("HERMES_HOME", str(home))1277 return home1278```12791280---12811282## Testing12831284### Python1285**ALWAYS use `scripts/run_tests.sh`** — do not call `pytest` directly. The script enforces1286hermetic environment parity with CI (unset credential vars, TZ=UTC, LANG=C.UTF-8,1287per-file subprocess isolation via `scripts/run_tests_parallel.py` — no xdist,1288worker count auto-scaled from CPU count). Direct `pytest`1289on a 16+ core developer machine with API keys set diverges from CI in ways1290that have caused multiple "works locally, fails in CI" incidents (and the reverse).12911292```bash1293scripts/run_tests.sh # full suite, CI-parity1294scripts/run_tests.sh tests/gateway/ # one directory1295scripts/run_tests.sh tests/agent/test_foo.py -k test_x # one test (file + -k; the runner is file-granular)1296scripts/run_tests.sh -v --tb=long # pass-through pytest flags1297```12981299**Flake policy:** the runner auto-retries a failing test FILE once in a fresh1300subprocess (`--file-retries`, default 1; `HERMES_TEST_FILE_RETRIES=0` to1301disable). Pass-on-retry counts as green but is printed in a `⚠ FLAKY` summary1302section with both attempts' output. A FLAKY report is a bug to fix, not noise1303to ignore — timing-sensitive tests must not assume a quiet runner (loose1304wall-clock bounds ≥ 2s, event-based sync, no `assert not _wait_until(...)`1305negative-timing races).13061307#### Subprocess-per-test-file isolation13081309Every test file runs in a freshly-spawned Python subprocess via `run_tests_parallel.py`. This means module-level dicts/sets and1310ContextVars from one test file cannot leak into the next.13111312#### Why the wrapper13131314| | Without wrapper | With wrapper |1315| ------------------- | ------------------------------------------- | ----------------------------------------- |1316| Provider API keys | Whatever is in your env (auto-detects pool) | All env vars except a specific few unset. |1317| HOME / `~/.hermes/` | Your real config+auth.json | Temp dir per test |1318| Timezone | Local TZ (PDT etc.) | UTC |1319| Locale | Whatever is set | C.UTF-8 |13201321### Where to place what tests13221323The CI change classifier (`scripts/ci/classify_changes.py`) runs specific jobs based on what files changed. A Python test that asserts1324about the contents of `package.json`, `package-lock.json`, `.ts`/`.tsx`1325source, or any other JS-side artifact will not run on a PR that only touches1326those files. This means a regression can go green on a PR and red on `main` (where the1327classifier fails open and runs everything).13281329Any test that reads or asserts about `package.json`,1330`package-lock.json`, `tsconfig.json`, `.ts`/`.tsx`/`.js`/`.mjs`/`.cjs`1331source files configuration belongs in the JS (vitest) test suite, not in `tests/*.py`.13321333### Don't write change-detector tests13341335A test is a **change-detector** if it fails whenever data that is **expected1336to change** gets updated — model catalogs, config version numbers,1337enumeration counts, hardcoded lists of provider models. These tests add no1338behavioral coverage; they just guarantee that routine source updates break1339CI and cost engineering time to "fix."13401341**Do not write:**13421343```python1344# catalog snapshot — breaks every model release1345assert "gemini-2.5-pro" in _PROVIDER_MODELS["gemini"]1346assert "MiniMax-M2.7" in models13471348# config version literal — breaks every schema bump1349assert DEFAULT_CONFIG["_config_version"] == 2113501351# enumeration count — breaks every time a skill/provider is added1352assert len(_PROVIDER_MODELS["huggingface"]) == 81353```13541355**Do write:**13561357```python1358# behavior: does the catalog plumbing work at all?1359assert "gemini" in _PROVIDER_MODELS1360assert len(_PROVIDER_MODELS["gemini"]) >= 113611362# behavior: does migration bump the user's version to current latest?1363assert raw["_config_version"] == DEFAULT_CONFIG["_config_version"]13641365# invariant: no plan-only model leaks into the legacy list1366assert not (set(moonshot_models) & coding_plan_only_models)13671368# invariant: every model in the catalog has a context-length entry1369for m in _PROVIDER_MODELS["huggingface"]:1370 assert m.lower() in DEFAULT_CONTEXT_LENGTHS_LOWER1371```13721373The rule: if the test reads like a snapshot of current data, delete it. If1374it reads like a contract about how two pieces of data must relate, keep it.1375When a PR adds a new provider/model and you want a test, make the test1376assert the relationship (e.g. "catalog entries all have context lengths"),1377not the specific names.13781379Reviewers should reject new change-detector tests; authors should convert1380them into invariants before re-requesting review.13811382### Never read source code in tests13831384A test that reads a source file's text is testing *the shape of the1385source code*, not its behavior. This is a hard antipattern, banned outright.1386Any test that reads a .py, .ts, .tsx, etc., file is suspect.13871388**Why it's actively harmful, not just weak:**13891390- It passes when the implementation is subtly broken (the regex matches a1391 call site that exists but is wired wrong) and fails when a correct1392 refactor changes formatting, variable names, or control flow with1393 identical runtime behavior. Both directions of failure are wrong.1394- It can't be run against a built/bundled/minified artifact, so it silently1395 stops testing anything the moment code moves, gets renamed, or a1396 dependency reformats it.1397- It actively blocks refactors: reviewers see "keeps a pattern intact" tests1398 fail during pure structural cleanup with no behavior change, and either1399 hand-wave the failure (dangerous) or waste time updating regexes that add1400 nothing (waste).1401- It gives false confidence. a green suite full of source-regex tests1402 looks like coverage but has never once executed the code path it claims1403 to guard.14041405**Do not write:**14061407```ts1408const source = fs.readFileSync(path.join(__dirname, 'main.ts'), 'utf8')14091410test('backend spawn hides the Windows console', () => {1411 assert.match(source, /spawn\(\s*backend\.command,\s*backend\.args[\s\S]{0,300}hiddenWindowsChildOptions/)1412})1413```14141415**Do write — extract the logic into a small pure/DI-testable function and1416call it for real:**14171418```ts1419// backend-spawn.ts1420export function hiddenWindowsChildOptions(options: SpawnOptionsLike = {}, isWindows = process.platform === 'win32') {1421 if (!isWindows || 'windowsHide' in options) return options1422 return { ...options, windowsHide: true }1423}14241425// backend-spawn.test.ts1426test('windowsHide defaults to true on Windows, is left alone elsewhere', () => {1427 assert.equal(hiddenWindowsChildOptions({}, true).windowsHide, true)1428 assert.equal(hiddenWindowsChildOptions({}, false).windowsHide, undefined)1429 assert.equal(hiddenWindowsChildOptions({ windowsHide: false }, true).windowsHide, false)1430})1431```14321433If the logic lives inline in a god-file (`main.ts`, `cli.py`,1434`gateway/run.py`) and extracting it feels disruptive: that's the actual1435signal to do the extraction, not to regex around it.1436
Also in NousResearch/hermes-agent
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| NousResearch/hermes-agentapps/desktop/AGENTS.md · 225k | AGENTS.md | testsecurityagent-behaviour | 44/100 | 3 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| OnlyTerp/prompt-cache-skillsAGENTS.md · 112 | AGENTS.md | setupbuildtestlint-format+5 | 100/100 | 3 days ago | |
| wpscanteam/wpscanAGENTS.md · 9.7k | AGENTS.md | setupbuildteststyle+6 | 100/100 | 2 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| SkeneTechnologies/skene-cookbookAGENTS.md · 51 | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 2 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| trick77/agents-md-syncAGENTS.md · 2 | AGENTS.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| aaif-goose/gooseAGENTS.md · 52k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 3 days ago |
