winter-super-cli 2026.6.26 → 2026.6.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +28 -5
  2. package/README.md +66 -0
  3. package/package.json +5 -1
  4. package/resources/local/gsap-skills/.claude-plugin/marketplace.json +20 -0
  5. package/resources/local/gsap-skills/.claude-plugin/plugin.json +6 -0
  6. package/resources/local/gsap-skills/.cursor-plugin/marketplace.json +13 -0
  7. package/resources/local/gsap-skills/.cursor-plugin/plugin.json +22 -0
  8. package/resources/local/gsap-skills/.github/copilot-instructions.md +17 -0
  9. package/resources/local/gsap-skills/.github/instructions/react.instructions.md +15 -0
  10. package/resources/local/gsap-skills/.github/instructions/scrolltrigger.instructions.md +18 -0
  11. package/resources/local/gsap-skills/AGENTS.md +27 -0
  12. package/resources/local/gsap-skills/CLAUDE.md +1 -0
  13. package/resources/local/gsap-skills/GEMINI.md +1 -0
  14. package/resources/local/gsap-skills/LICENSE +21 -0
  15. package/resources/local/gsap-skills/README.md +163 -0
  16. package/resources/local/gsap-skills/assets/gsap-green.svg +7 -0
  17. package/resources/local/gsap-skills/assets/gsap-icon-inverted.svg +15 -0
  18. package/resources/local/gsap-skills/assets/gsap-icon-square.svg +1 -0
  19. package/resources/local/gsap-skills/assets/gsap-white.svg +7 -0
  20. package/resources/local/gsap-skills/examples/README.md +29 -0
  21. package/resources/local/gsap-skills/examples/nuxt/app/app.vue +3 -0
  22. package/resources/local/gsap-skills/examples/nuxt/app/composables/useGSAP.ts +91 -0
  23. package/resources/local/gsap-skills/examples/nuxt/app/pages/index.vue +55 -0
  24. package/resources/local/gsap-skills/examples/nuxt/nuxt.config.ts +4 -0
  25. package/resources/local/gsap-skills/examples/nuxt/package.json +18 -0
  26. package/resources/local/gsap-skills/examples/react/App.jsx +46 -0
  27. package/resources/local/gsap-skills/examples/react/index.html +12 -0
  28. package/resources/local/gsap-skills/examples/react/main.jsx +9 -0
  29. package/resources/local/gsap-skills/examples/react/package.json +21 -0
  30. package/resources/local/gsap-skills/examples/react/vite.config.js +7 -0
  31. package/resources/local/gsap-skills/examples/vanilla/index.html +33 -0
  32. package/resources/local/gsap-skills/examples/vanilla/main.js +36 -0
  33. package/resources/local/gsap-skills/examples/vue/app.vue +47 -0
  34. package/resources/local/gsap-skills/examples/vue/index.html +15 -0
  35. package/resources/local/gsap-skills/examples/vue/main.js +9 -0
  36. package/resources/local/gsap-skills/examples/vue/package.json +19 -0
  37. package/resources/local/gsap-skills/examples/vue/vite.config.js +7 -0
  38. package/resources/local/gsap-skills/skills/gsap-core/SKILL.md +254 -0
  39. package/resources/local/gsap-skills/skills/gsap-frameworks/SKILL.md +266 -0
  40. package/resources/local/gsap-skills/skills/gsap-performance/SKILL.md +79 -0
  41. package/resources/local/gsap-skills/skills/gsap-plugins/SKILL.md +433 -0
  42. package/resources/local/gsap-skills/skills/gsap-react/SKILL.md +136 -0
  43. package/resources/local/gsap-skills/skills/gsap-scrolltrigger/SKILL.md +296 -0
  44. package/resources/local/gsap-skills/skills/gsap-timeline/SKILL.md +107 -0
  45. package/resources/local/gsap-skills/skills/gsap-utils/SKILL.md +284 -0
  46. package/resources/local/gsap-skills/skills/llms.txt +39 -0
  47. package/resources/local/hermes-agent-core/AGENTS.md +1132 -0
  48. package/resources/local/hermes-agent-core/LICENSE +21 -0
  49. package/resources/local/hermes-agent-core/README.md +215 -0
  50. package/resources/local/hermes-agent-core/docs/2026-05-07-s6-overlay-dynamic-subagent-gateways.md +434 -0
  51. package/resources/local/hermes-agent-core/hermes-already-has-routines.md +160 -0
  52. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/DESCRIPTION.md +3 -0
  53. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/claude-code/SKILL.md +745 -0
  54. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/codex/SKILL.md +130 -0
  55. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/hermes-agent/SKILL.md +1021 -0
  56. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +277 -0
  57. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +57 -0
  58. package/resources/local/hermes-agent-core/skills/autonomous-ai-agents/opencode/SKILL.md +219 -0
  59. package/resources/local/hermes-agent-core/skills/github/DESCRIPTION.md +3 -0
  60. package/resources/local/hermes-agent-core/skills/github/codebase-inspection/SKILL.md +116 -0
  61. package/resources/local/hermes-agent-core/skills/github/github-auth/SKILL.md +247 -0
  62. package/resources/local/hermes-agent-core/skills/github/github-auth/scripts/gh-env.sh +66 -0
  63. package/resources/local/hermes-agent-core/skills/github/github-code-review/SKILL.md +481 -0
  64. package/resources/local/hermes-agent-core/skills/github/github-code-review/references/review-output-template.md +74 -0
  65. package/resources/local/hermes-agent-core/skills/github/github-issues/SKILL.md +370 -0
  66. package/resources/local/hermes-agent-core/skills/github/github-issues/templates/bug-report.md +35 -0
  67. package/resources/local/hermes-agent-core/skills/github/github-issues/templates/feature-request.md +31 -0
  68. package/resources/local/hermes-agent-core/skills/github/github-pr-workflow/SKILL.md +367 -0
  69. package/resources/local/hermes-agent-core/skills/github/github-pr-workflow/references/ci-troubleshooting.md +183 -0
  70. package/resources/local/hermes-agent-core/skills/github/github-pr-workflow/references/conventional-commits.md +71 -0
  71. package/resources/local/hermes-agent-core/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
  72. package/resources/local/hermes-agent-core/skills/github/github-pr-workflow/templates/pr-body-feature.md +33 -0
  73. package/resources/local/hermes-agent-core/skills/github/github-repo-management/SKILL.md +516 -0
  74. package/resources/local/hermes-agent-core/skills/github/github-repo-management/references/github-api-cheatsheet.md +161 -0
  75. package/resources/local/hermes-agent-core/skills/mcp/DESCRIPTION.md +3 -0
  76. package/resources/local/hermes-agent-core/skills/mcp/native-mcp/SKILL.md +357 -0
  77. package/resources/local/hermes-agent-core/skills/software-development/debugging-hermes-tui-commands/SKILL.md +152 -0
  78. package/resources/local/hermes-agent-core/skills/software-development/hermes-agent-skill-authoring/SKILL.md +165 -0
  79. package/resources/local/hermes-agent-core/skills/software-development/hermes-s6-container-supervision/SKILL.md +176 -0
  80. package/resources/local/hermes-agent-core/skills/software-development/node-inspect-debugger/SKILL.md +319 -0
  81. package/resources/local/hermes-agent-core/skills/software-development/plan/SKILL.md +58 -0
  82. package/resources/local/hermes-agent-core/skills/software-development/python-debugpy/SKILL.md +375 -0
  83. package/resources/local/hermes-agent-core/skills/software-development/requesting-code-review/SKILL.md +280 -0
  84. package/resources/local/hermes-agent-core/skills/software-development/spike/SKILL.md +197 -0
  85. package/resources/local/hermes-agent-core/skills/software-development/subagent-driven-development/SKILL.md +352 -0
  86. package/resources/local/hermes-agent-core/skills/software-development/subagent-driven-development/references/context-budget-discipline.md +53 -0
  87. package/resources/local/hermes-agent-core/skills/software-development/subagent-driven-development/references/gates-taxonomy.md +93 -0
  88. package/resources/local/hermes-agent-core/skills/software-development/systematic-debugging/SKILL.md +367 -0
  89. package/resources/local/hermes-agent-core/skills/software-development/test-driven-development/SKILL.md +343 -0
  90. package/resources/local/hermes-agent-core/skills/software-development/writing-plans/SKILL.md +297 -0
  91. package/resources/local/manifest.json +12 -0
  92. package/rule.md +2 -0
  93. package/scripts/audit-pack.js +5 -0
  94. package/scripts/smoke-browser.js +53 -0
  95. package/scripts/smoke-package.js +38 -4
  96. package/skill.md +36 -4
  97. package/skills/gsap.md +26 -0
  98. package/skills/hermes-agent.md +17 -0
  99. package/src/agent/agent-definitions.js +4 -4
  100. package/src/agent/runtime.js +179 -5
  101. package/src/agent/subagent-child.js +44 -0
  102. package/src/ai/capability-scorecard.js +193 -14
  103. package/src/ai/hermes-core.js +77 -0
  104. package/src/ai/model-capabilities.js +42 -2
  105. package/src/ai/prompts/system-prompt.js +16 -2
  106. package/src/ai/small-model-amplifier.js +35 -7
  107. package/src/ai/workflow-selector.js +22 -1
  108. package/src/cli/commands.js +21 -1
  109. package/src/cli/config.js +42 -4
  110. package/src/cli/context-loader.js +253 -9
  111. package/src/cli/conversation-format.js +5 -0
  112. package/src/cli/input-controller.js +79 -10
  113. package/src/cli/prompt-builder.js +45 -8
  114. package/src/cli/repl-commands.js +115 -0
  115. package/src/cli/repl.js +147 -86
  116. package/src/cli/slash-commands.js +3 -1
  117. package/src/cli/tui.js +133 -37
  118. package/src/mcp/client.js +46 -5
  119. package/src/tools/agent.js +316 -25
  120. package/src/tools/executor.js +310 -9
  121. package/src/tools/permission.js +20 -17
  122. package/winter.d.ts +112 -10
@@ -0,0 +1,1132 @@
1
+ # Hermes Agent - Development Guide
2
+
3
+ Instructions for AI coding assistants and developers working on the hermes-agent codebase.
4
+
5
+ ## Development Environment
6
+
7
+ ```bash
8
+ # Prefer .venv; fall back to venv if that's what your checkout has.
9
+ source .venv/bin/activate # or: source venv/bin/activate
10
+ ```
11
+
12
+ `scripts/run_tests.sh` probes `.venv` first, then `venv`, then
13
+ `$HOME/.hermes/hermes-agent/venv` (for worktrees that share a venv with the
14
+ main checkout).
15
+
16
+ ## Project Structure
17
+
18
+ File counts shift constantly — don't treat the tree below as exhaustive.
19
+ The canonical source is the filesystem. The notes call out the load-bearing
20
+ entry points you'll actually edit.
21
+
22
+ ```
23
+ hermes-agent/
24
+ ├── run_agent.py # AIAgent class — core conversation loop (~12k LOC)
25
+ ├── model_tools.py # Tool orchestration, discover_builtin_tools(), handle_function_call()
26
+ ├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list
27
+ ├── cli.py # HermesCLI class — interactive CLI orchestrator (~11k LOC)
28
+ ├── hermes_state.py # SessionDB — SQLite session store (FTS5 search)
29
+ ├── hermes_constants.py # get_hermes_home(), display_hermes_home() — profile-aware paths
30
+ ├── hermes_logging.py # setup_logging() — agent.log / errors.log / gateway.log (profile-aware)
31
+ ├── batch_runner.py # Parallel batch processing
32
+ ├── agent/ # Agent internals (provider adapters, memory, caching, compression, etc.)
33
+ ├── hermes_cli/ # CLI subcommands, setup wizard, plugins loader, skin engine
34
+ ├── tools/ # Tool implementations — auto-discovered via tools/registry.py
35
+ │ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
36
+ ├── gateway/ # Messaging gateway — run.py + session.py + platforms/
37
+ │ ├── platforms/ # Adapter per platform (telegram, discord, slack, whatsapp,
38
+ │ │ # homeassistant, signal, matrix, mattermost, email, sms,
39
+ │ │ # dingtalk, wecom, weixin, feishu, qqbot, bluebubbles,
40
+ │ │ # yuanbao, webhook, api_server, ...). See ADDING_A_PLATFORM.md.
41
+ │ └── builtin_hooks/ # Extension point for always-registered gateway hooks (none shipped)
42
+ ├── plugins/ # Plugin system (see "Plugins" section below)
43
+ │ ├── memory/ # Memory-provider plugins (honcho, mem0, supermemory, ...)
44
+ │ ├── context_engine/ # Context-engine plugins
45
+ │ ├── model-providers/ # Inference backend plugins (openrouter, anthropic, gmi, ...)
46
+ │ ├── kanban/ # Multi-agent board dispatcher + worker plugin
47
+ │ ├── hermes-achievements/ # Gamified achievement tracking
48
+ │ ├── observability/ # Metrics / traces / logs plugin
49
+ │ ├── image_gen/ # Image-generation providers
50
+ │ └── <others>/ # disk-cleanup, example-dashboard, google_meet, platforms,
51
+ │ # spotify, strike-freedom-cockpit, ...
52
+ ├── optional-skills/ # Heavier/niche skills shipped but NOT active by default
53
+ ├── skills/ # Built-in skills bundled with the repo
54
+ ├── ui-tui/ # Ink (React) terminal UI — `hermes --tui`
55
+ │ └── src/ # entry.tsx, app.tsx, gatewayClient.ts + app/components/hooks/lib
56
+ ├── tui_gateway/ # Python JSON-RPC backend for the TUI
57
+ ├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
58
+ ├── cron/ # Scheduler — jobs.py, scheduler.py
59
+ ├── scripts/ # run_tests.sh, release.py, auxiliary scripts
60
+ ├── website/ # Docusaurus docs site
61
+ └── tests/ # Pytest suite (~17k tests across ~900 files as of May 2026)
62
+ ```
63
+
64
+ **User config:** `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys only).
65
+ **Logs:** `~/.hermes/logs/` — `agent.log` (INFO+), `errors.log` (WARNING+),
66
+ `gateway.log` when running the gateway. Profile-aware via `get_hermes_home()`.
67
+ Browse with `hermes logs [--follow] [--level ...] [--session ...]`.
68
+
69
+ ## File Dependency Chain
70
+
71
+ ```
72
+ tools/registry.py (no deps — imported by all tool files)
73
+
74
+ tools/*.py (each calls registry.register() at import time)
75
+
76
+ model_tools.py (imports tools/registry + triggers tool discovery)
77
+
78
+ run_agent.py, cli.py, batch_runner.py, environments/
79
+ ```
80
+
81
+ ---
82
+
83
+ ## AIAgent Class (run_agent.py)
84
+
85
+ The real `AIAgent.__init__` takes ~60 parameters (credentials, routing, callbacks,
86
+ session context, budget, credential pool, etc.). The signature below is the
87
+ minimum subset you'll usually touch — read `run_agent.py` for the full list.
88
+
89
+ ```python
90
+ class AIAgent:
91
+ def __init__(self,
92
+ base_url: str = None,
93
+ api_key: str = None,
94
+ provider: str = None,
95
+ api_mode: str = None, # "chat_completions" | "codex_responses" | ...
96
+ model: str = "", # empty → resolved from config/provider later
97
+ max_iterations: int = 90, # tool-calling iterations (shared with subagents)
98
+ enabled_toolsets: list = None,
99
+ disabled_toolsets: list = None,
100
+ quiet_mode: bool = False,
101
+ save_trajectories: bool = False,
102
+ platform: str = None, # "cli", "telegram", etc.
103
+ session_id: str = None,
104
+ skip_context_files: bool = False,
105
+ skip_memory: bool = False,
106
+ credential_pool=None,
107
+ # ... plus callbacks, thread/user/chat IDs, iteration_budget, fallback_model,
108
+ # checkpoints config, prefill_messages, service_tier, reasoning_config, etc.
109
+ ): ...
110
+
111
+ def chat(self, message: str) -> str:
112
+ """Simple interface — returns final response string."""
113
+
114
+ def run_conversation(self, user_message: str, system_message: str = None,
115
+ conversation_history: list = None, task_id: str = None) -> dict:
116
+ """Full interface — returns dict with final_response + messages."""
117
+ ```
118
+
119
+ ### Agent Loop
120
+
121
+ The core loop is inside `run_conversation()` — entirely synchronous, with
122
+ interrupt checks, budget tracking, and a one-turn grace call:
123
+
124
+ ```python
125
+ while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) \
126
+ or self._budget_grace_call:
127
+ if self._interrupt_requested: break
128
+ response = client.chat.completions.create(model=model, messages=messages, tools=tool_schemas)
129
+ if response.tool_calls:
130
+ for tool_call in response.tool_calls:
131
+ result = handle_function_call(tool_call.name, tool_call.args, task_id)
132
+ messages.append(tool_result_message(result))
133
+ api_call_count += 1
134
+ else:
135
+ return response.content
136
+ ```
137
+
138
+ Messages follow OpenAI format: `{"role": "system/user/assistant/tool", ...}`.
139
+ Reasoning content is stored in `assistant_msg["reasoning"]`.
140
+
141
+ ---
142
+
143
+ ## CLI Architecture (cli.py)
144
+
145
+ - **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete
146
+ - **KawaiiSpinner** (`agent/display.py`) — animated faces during API calls, `┊` activity feed for tool results
147
+ - `load_cli_config()` in cli.py merges hardcoded defaults + user config YAML
148
+ - **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
149
+ - `process_command()` is a method on `HermesCLI` — dispatches on canonical command name resolved via `resolve_command()` from the central registry
150
+ - Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching
151
+
152
+ ### Slash Command Registry (`hermes_cli/commands.py`)
153
+
154
+ All slash commands are defined in a central `COMMAND_REGISTRY` list of `CommandDef` objects. Every downstream consumer derives from this registry automatically:
155
+
156
+ - **CLI** — `process_command()` resolves aliases via `resolve_command()`, dispatches on canonical name
157
+ - **Gateway** — `GATEWAY_KNOWN_COMMANDS` frozenset for hook emission, `resolve_command()` for dispatch
158
+ - **Gateway help** — `gateway_help_lines()` generates `/help` output
159
+ - **Telegram** — `telegram_bot_commands()` generates the BotCommand menu
160
+ - **Slack** — `slack_subcommand_map()` generates `/hermes` subcommand routing
161
+ - **Autocomplete** — `COMMANDS` flat dict feeds `SlashCommandCompleter`
162
+ - **CLI help** — `COMMANDS_BY_CATEGORY` dict feeds `show_help()`
163
+
164
+ ### Adding a Slash Command
165
+
166
+ 1. Add a `CommandDef` entry to `COMMAND_REGISTRY` in `hermes_cli/commands.py`:
167
+ ```python
168
+ CommandDef("mycommand", "Description of what it does", "Session",
169
+ aliases=("mc",), args_hint="[arg]"),
170
+ ```
171
+ 2. Add handler in `HermesCLI.process_command()` in `cli.py`:
172
+ ```python
173
+ elif canonical == "mycommand":
174
+ self._handle_mycommand(cmd_original)
175
+ ```
176
+ 3. If the command is available in the gateway, add a handler in `gateway/run.py`:
177
+ ```python
178
+ if canonical == "mycommand":
179
+ return await self._handle_mycommand(event)
180
+ ```
181
+ 4. For persistent settings, use `save_config_value()` in `cli.py`
182
+
183
+ **CommandDef fields:**
184
+ - `name` — canonical name without slash (e.g. `"background"`)
185
+ - `description` — human-readable description
186
+ - `category` — one of `"Session"`, `"Configuration"`, `"Tools & Skills"`, `"Info"`, `"Exit"`
187
+ - `aliases` — tuple of alternative names (e.g. `("bg",)`)
188
+ - `args_hint` — argument placeholder shown in help (e.g. `"<prompt>"`, `"[name]"`)
189
+ - `cli_only` — only available in the interactive CLI
190
+ - `gateway_only` — only available in messaging platforms
191
+ - `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.
192
+
193
+ **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.
194
+
195
+ ---
196
+
197
+ ## TUI Architecture (ui-tui + tui_gateway)
198
+
199
+ The TUI is a full replacement for the classic (prompt_toolkit) CLI, activated via `hermes --tui` or `HERMES_TUI=1`.
200
+
201
+ ### Process Model
202
+
203
+ ```
204
+ hermes --tui
205
+ └─ Node (Ink) ──stdio JSON-RPC── Python (tui_gateway)
206
+ │ └─ AIAgent + tools + sessions
207
+ └─ renders transcript, composer, prompts, activity
208
+ ```
209
+
210
+ TypeScript owns the screen. Python owns sessions, tools, model calls, and slash command logic.
211
+
212
+ ### Transport
213
+
214
+ Newline-delimited JSON-RPC over stdio. Requests from Ink, events from Python. See `tui_gateway/server.py` for the full method/event catalog.
215
+
216
+ ### Key Surfaces
217
+
218
+ | Surface | Ink component | Gateway method |
219
+ |---------|---------------|----------------|
220
+ | Chat streaming | `app.tsx` + `messageLine.tsx` | `prompt.submit` → `message.delta/complete` |
221
+ | Tool activity | `thinking.tsx` | `tool.start/progress/complete` |
222
+ | Approvals | `prompts.tsx` | `approval.respond` ← `approval.request` |
223
+ | Clarify/sudo/secret | `prompts.tsx`, `maskedPrompt.tsx` | `clarify/sudo/secret.respond` |
224
+ | Session picker | `sessionPicker.tsx` | `session.list/resume` |
225
+ | Slash commands | Local handler + fallthrough | `slash.exec` → `_SlashWorker`, `command.dispatch` |
226
+ | Completions | `useCompletion` hook | `complete.slash`, `complete.path` |
227
+ | Theming | `theme.ts` + `branding.tsx` | `gateway.ready` with skin data |
228
+
229
+ ### Slash Command Flow
230
+
231
+ 1. Built-in client commands (`/help`, `/quit`, `/clear`, `/resume`, `/copy`, `/paste`, etc.) handled locally in `app.tsx`
232
+ 2. Everything else → `slash.exec` (runs in persistent `_SlashWorker` subprocess) → `command.dispatch` fallback
233
+
234
+ ### Dev Commands
235
+
236
+ ```bash
237
+ cd ui-tui
238
+ npm install # first time
239
+ npm run dev # watch mode (rebuilds hermes-ink + tsx --watch)
240
+ npm start # production
241
+ npm run build # full build (hermes-ink + tsc)
242
+ npm run type-check # typecheck only (tsc --noEmit)
243
+ npm run lint # eslint
244
+ npm run fmt # prettier
245
+ npm test # vitest
246
+ ```
247
+
248
+ ### TUI in the Dashboard (`hermes dashboard` → `/chat`)
249
+
250
+ 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`.
251
+
252
+ - 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.
253
+ - `/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).
254
+ - The server spawns whatever `hermes --tui` would spawn, through `ptyprocess` (POSIX PTY — WSL works, native Windows does not).
255
+ - Frames: raw PTY bytes each direction; resize via `\x1b[RESIZE:<cols>;<rows>]` intercepted on the server and applied with `TIOCSWINSZ`.
256
+
257
+ **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.
258
+
259
+ **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.
260
+
261
+ ---
262
+
263
+ ## Adding New Tools
264
+
265
+ For most custom or local-only tools, do **not** edit Hermes core. Use the plugin
266
+ route instead: create `~/.hermes/plugins/<name>/plugin.yaml` and
267
+ `~/.hermes/plugins/<name>/__init__.py`, then register tools with
268
+ `ctx.register_tool(...)`. Plugin toolsets are discovered automatically and can be
269
+ enabled or disabled without touching `tools/` or `toolsets.py`.
270
+
271
+ Use the built-in route below only when the user is explicitly contributing a new
272
+ core Hermes tool that should ship in the base system.
273
+
274
+ Built-in/core tools require changes in **2 files**:
275
+
276
+ **1. Create `tools/your_tool.py`:**
277
+ ```python
278
+ import json, os
279
+ from tools.registry import registry
280
+
281
+ def check_requirements() -> bool:
282
+ return bool(os.getenv("EXAMPLE_API_KEY"))
283
+
284
+ def example_tool(param: str, task_id: str = None) -> str:
285
+ return json.dumps({"success": True, "data": "..."})
286
+
287
+ registry.register(
288
+ name="example_tool",
289
+ toolset="example",
290
+ schema={"name": "example_tool", "description": "...", "parameters": {...}},
291
+ handler=lambda args, **kw: example_tool(param=args.get("param", ""), task_id=kw.get("task_id")),
292
+ check_fn=check_requirements,
293
+ requires_env=["EXAMPLE_API_KEY"],
294
+ )
295
+ ```
296
+
297
+ **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.
298
+
299
+ 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.
300
+
301
+ The registry handles schema collection, dispatch, availability checking, and error wrapping. All handlers MUST return a JSON string.
302
+
303
+ **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`.
304
+
305
+ **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.
306
+
307
+ **Agent-level tools** (todo, memory): intercepted by `run_agent.py` before `handle_function_call()`. See `tools/todo_tool.py` for the pattern.
308
+
309
+ ---
310
+
311
+ ## Dependency Pinning Policy
312
+
313
+ All dependencies must have upper bounds to limit supply-chain attack surface.
314
+ This policy was established after the litellm compromise (PR #2796, #2810) and
315
+ reinforced after the Mini Shai-Hulud worm campaign (May 2026).
316
+
317
+ | Source type | Treatment | Example |
318
+ |---|---|---|
319
+ | PyPI package | `>=floor,<next_major` | `"httpx>=0.28.1,<1"` |
320
+ | Git URL | Commit SHA | `git+https://...@<40-char-sha>` |
321
+ | GitHub Actions | Commit SHA + comment | `uses: actions/checkout@<sha> # v4` |
322
+ | CI-only pip | `==exact` | `pyyaml==6.0.2` |
323
+
324
+ **When adding a new dependency to `pyproject.toml`:**
325
+ 1. Pin to `>=current_version,<next_major` for post-1.0 (e.g. `>=1.5.0,<2`).
326
+ 2. For pre-1.0 packages, use `<0.(current_minor + 2)` (e.g. `>=0.29,<0.32`).
327
+ 3. Never commit a bare `>=X.Y.Z` without a ceiling — CI and reviewers will reject it.
328
+ 4. Run `uv lock` to regenerate `uv.lock` with hashes.
329
+
330
+ Reference: #2810 (bounds pass), #9801 (SHA pinning + audit CI).
331
+
332
+ ---
333
+
334
+ ## Adding Configuration
335
+
336
+ ### config.yaml options:
337
+ 1. Add to `DEFAULT_CONFIG` in `hermes_cli/config.py`
338
+ 2. Bump `_config_version` (check the current value at the top of `DEFAULT_CONFIG`)
339
+ ONLY if you need to actively migrate/transform existing user config
340
+ (renaming keys, changing structure). Adding a new key to an existing
341
+ section is handled automatically by the deep-merge and does NOT require
342
+ a version bump.
343
+
344
+ ### Top-level `config.yaml` sections (non-exhaustive):
345
+
346
+ `model`, `agent`, `terminal`, `compression`, `display`, `stt`, `tts`,
347
+ `memory`, `security`, `delegation`, `smart_model_routing`, `checkpoints`,
348
+ `auxiliary`, `curator`, `skills`, `gateway`, `logging`, `cron`, `profiles`,
349
+ `plugins`, `honcho`.
350
+
351
+ `auxiliary` holds per-task overrides for side-LLM work (curator, vision,
352
+ embedding, title generation, session_search, etc.) — each task can pin
353
+ its own provider/model/base_url/max_tokens/reasoning_effort. See
354
+ `agent/auxiliary_client.py::_resolve_auto` for resolution order.
355
+
356
+ `curator` holds the background skill-maintenance config —
357
+ `enabled`, `interval_hours`, `min_idle_hours`, `stale_after_days`,
358
+ `archive_after_days`, `backup` (nested).
359
+
360
+ ### .env variables (SECRETS ONLY — API keys, tokens, passwords):
361
+ 1. Add to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` with metadata:
362
+ ```python
363
+ "NEW_API_KEY": {
364
+ "description": "What it's for",
365
+ "prompt": "Display name",
366
+ "url": "https://...",
367
+ "password": True,
368
+ "category": "tool", # provider, tool, messaging, setting
369
+ },
370
+ ```
371
+
372
+ Non-secret settings (timeouts, thresholds, feature flags, paths, display
373
+ preferences) belong in `config.yaml`, not `.env`. If internal code needs an
374
+ env var mirror for backward compatibility, bridge it from `config.yaml` to
375
+ the env var in code (see `gateway_timeout`, `terminal.cwd` → `TERMINAL_CWD`).
376
+
377
+ ### Config loaders (three paths — know which one you're in):
378
+
379
+ | Loader | Used by | Location |
380
+ |--------|---------|----------|
381
+ | `load_cli_config()` | CLI mode | `cli.py` — merges CLI-specific defaults + user YAML |
382
+ | `load_config()` | `hermes tools`, `hermes setup`, most CLI subcommands | `hermes_cli/config.py` — merges `DEFAULT_CONFIG` + user YAML |
383
+ | Direct YAML load | Gateway runtime | `gateway/run.py` + `gateway/config.py` — reads user YAML raw |
384
+
385
+ If you add a new key and the CLI sees it but the gateway doesn't (or vice
386
+ versa), you're on the wrong loader. Check `DEFAULT_CONFIG` coverage.
387
+
388
+ ### Working directory:
389
+ - **CLI** — uses the process's current directory (`os.getcwd()`).
390
+ - **Messaging** — uses `terminal.cwd` from `config.yaml`. The gateway bridges this
391
+ to the `TERMINAL_CWD` env var for child tools. **`MESSAGING_CWD` has been
392
+ removed** — the config loader prints a deprecation warning if it's set in
393
+ `.env`. Same for `TERMINAL_CWD` in `.env`; the canonical setting is
394
+ `terminal.cwd` in `config.yaml`.
395
+
396
+ ---
397
+
398
+ ## Skin/Theme System
399
+
400
+ 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.
401
+
402
+ ### Architecture
403
+
404
+ ```
405
+ hermes_cli/skin_engine.py # SkinConfig dataclass, built-in skins, YAML loader
406
+ ~/.hermes/skins/*.yaml # User-installed custom skins (drop-in)
407
+ ```
408
+
409
+ - `init_skin_from_config()` — called at CLI startup, reads `display.skin` from config
410
+ - `get_active_skin()` — returns cached `SkinConfig` for the current skin
411
+ - `set_active_skin(name)` — switches skin at runtime (used by `/skin` command)
412
+ - `load_skin(name)` — loads from user skins first, then built-ins, then falls back to default
413
+ - Missing skin values inherit from the `default` skin automatically
414
+
415
+ ### What skins customize
416
+
417
+ | Element | Skin Key | Used By |
418
+ |---------|----------|---------|
419
+ | Banner panel border | `colors.banner_border` | `banner.py` |
420
+ | Banner panel title | `colors.banner_title` | `banner.py` |
421
+ | Banner section headers | `colors.banner_accent` | `banner.py` |
422
+ | Banner dim text | `colors.banner_dim` | `banner.py` |
423
+ | Banner body text | `colors.banner_text` | `banner.py` |
424
+ | Response box border | `colors.response_border` | `cli.py` |
425
+ | Spinner faces (waiting) | `spinner.waiting_faces` | `display.py` |
426
+ | Spinner faces (thinking) | `spinner.thinking_faces` | `display.py` |
427
+ | Spinner verbs | `spinner.thinking_verbs` | `display.py` |
428
+ | Spinner wings (optional) | `spinner.wings` | `display.py` |
429
+ | Tool output prefix | `tool_prefix` | `display.py` |
430
+ | Per-tool emojis | `tool_emojis` | `display.py` → `get_tool_emoji()` |
431
+ | Agent name | `branding.agent_name` | `banner.py`, `cli.py` |
432
+ | Welcome message | `branding.welcome` | `cli.py` |
433
+ | Response box label | `branding.response_label` | `cli.py` |
434
+ | Prompt symbol | `branding.prompt_symbol` | `cli.py` |
435
+
436
+ ### Built-in skins
437
+
438
+ - `default` — Classic Hermes gold/kawaii (the current look)
439
+ - `ares` — Crimson/bronze war-god theme with custom spinner wings
440
+ - `mono` — Clean grayscale monochrome
441
+ - `slate` — Cool blue developer-focused theme
442
+
443
+ ### Adding a built-in skin
444
+
445
+ Add to `_BUILTIN_SKINS` dict in `hermes_cli/skin_engine.py`:
446
+
447
+ ```python
448
+ "mytheme": {
449
+ "name": "mytheme",
450
+ "description": "Short description",
451
+ "colors": { ... },
452
+ "spinner": { ... },
453
+ "branding": { ... },
454
+ "tool_prefix": "┊",
455
+ },
456
+ ```
457
+
458
+ ### User skins (YAML)
459
+
460
+ Users create `~/.hermes/skins/<name>.yaml`:
461
+
462
+ ```yaml
463
+ name: cyberpunk
464
+ description: Neon-soaked terminal theme
465
+
466
+ colors:
467
+ banner_border: "#FF00FF"
468
+ banner_title: "#00FFFF"
469
+ banner_accent: "#FF1493"
470
+
471
+ spinner:
472
+ thinking_verbs: ["jacking in", "decrypting", "uploading"]
473
+ wings:
474
+ - ["⟨⚡", "⚡⟩"]
475
+
476
+ branding:
477
+ agent_name: "Cyber Agent"
478
+ response_label: " ⚡ Cyber "
479
+
480
+ tool_prefix: "▏"
481
+ ```
482
+
483
+ Activate with `/skin cyberpunk` or `display.skin: cyberpunk` in config.yaml.
484
+
485
+ ---
486
+
487
+ ## Plugins
488
+
489
+ Hermes has two plugin surfaces. Both live under `plugins/` in the repo so
490
+ repo-shipped plugins can be discovered alongside user-installed ones in
491
+ `~/.hermes/plugins/` and pip-installed entry points.
492
+
493
+ ### General plugins (`hermes_cli/plugins.py` + `plugins/<name>/`)
494
+
495
+ `PluginManager` discovers plugins from `~/.hermes/plugins/`, `./.hermes/plugins/`,
496
+ and pip entry points. Each plugin exposes a `register(ctx)` function that
497
+ can:
498
+
499
+ - Register Python-callback lifecycle hooks:
500
+ `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
501
+ `on_session_start`, `on_session_end`
502
+ - Register new tools via `ctx.register_tool(...)`
503
+ - Register CLI subcommands via `ctx.register_cli_command(...)` — the
504
+ plugin's argparse tree is wired into `hermes` at startup so
505
+ `hermes <pluginname> <subcmd>` works with no change to `main.py`
506
+
507
+ Hooks are invoked from `model_tools.py` (pre/post tool) and `run_agent.py`
508
+ (lifecycle). **Discovery timing pitfall:** `discover_plugins()` only runs
509
+ as a side effect of importing `model_tools.py`. Code paths that read plugin
510
+ state without importing `model_tools.py` first must call `discover_plugins()`
511
+ explicitly (it's idempotent).
512
+
513
+ ### Memory-provider plugins (`plugins/memory/<name>/`)
514
+
515
+ Separate discovery system for pluggable memory backends. Current built-in
516
+ providers include **honcho, mem0, supermemory, byterover, hindsight,
517
+ holographic, openviking, retaindb**.
518
+
519
+ Each provider implements the `MemoryProvider` ABC (see `agent/memory_provider.py`)
520
+ and is orchestrated by `agent/memory_manager.py`. Lifecycle hooks include
521
+ `sync_turn(turn_messages)`, `prefetch(query)`, `shutdown()`, and optional
522
+ `post_setup(hermes_home, config)` for setup-wizard integration.
523
+
524
+ **CLI commands via `plugins/memory/<name>/cli.py`:** if a memory plugin
525
+ defines `register_cli(subparser)`, `discover_plugin_cli_commands()` finds
526
+ it at argparse setup time and wires it into `hermes <plugin>`. The
527
+ framework only exposes CLI commands for the **currently active** memory
528
+ provider (read from `memory.provider` in config.yaml), so disabled
529
+ providers don't clutter `hermes --help`.
530
+
531
+ **Rule (Teknium, May 2026):** plugins MUST NOT modify core files
532
+ (`run_agent.py`, `cli.py`, `gateway/run.py`, `hermes_cli/main.py`, etc.).
533
+ If a plugin needs a capability the framework doesn't expose, expand the
534
+ generic plugin surface (new hook, new ctx method) — never hardcode
535
+ plugin-specific logic into core. PR #5295 removed 95 lines of hardcoded
536
+ honcho argparse from `main.py` for exactly this reason.
537
+
538
+ **No new in-tree memory providers (policy, May 2026):** the set of
539
+ built-in memory providers under `plugins/memory/` is closed. New memory
540
+ backends must ship as **standalone plugin repos** that users install
541
+ into `~/.hermes/plugins/` (or via pip entry points) — they implement
542
+ the same `MemoryProvider` ABC, register through the same discovery
543
+ path, and integrate via `hermes memory setup` / `post_setup()` without
544
+ landing in this tree. PRs that add a new directory under
545
+ `plugins/memory/` will be closed with a pointer to publish the
546
+ provider as its own repo. Existing in-tree providers stay; bug fixes
547
+ to them are welcome.
548
+
549
+ ### Model-provider plugins (`plugins/model-providers/<name>/`)
550
+
551
+ Every inference backend (openrouter, anthropic, gmi, deepseek, nvidia, …)
552
+ ships as a plugin here. Each plugin's `__init__.py` calls
553
+ `providers.register_provider(ProviderProfile(...))` at module load.
554
+ `providers/__init__.py._discover_providers()` is a **lazy, separate
555
+ discovery system** — scanned on first `get_provider_profile()` or
556
+ `list_providers()` call, NOT by the general PluginManager.
557
+
558
+ Scan order:
559
+ 1. Bundled: `<repo>/plugins/model-providers/<name>/`
560
+ 2. User: `$HERMES_HOME/plugins/model-providers/<name>/`
561
+ 3. Legacy: `<repo>/providers/<name>.py` (back-compat)
562
+
563
+ User plugins of the same name override bundled ones — `register_provider()`
564
+ is last-writer-wins. This lets third parties swap out any built-in
565
+ profile without a repo patch.
566
+
567
+ The general PluginManager records `kind: model-provider` manifests but does
568
+ NOT import them (would double-instantiate `ProviderProfile`). Plugins
569
+ without an explicit `kind:` get auto-coerced via a source-text heuristic
570
+ (`register_provider` + `ProviderProfile` in `__init__.py`).
571
+
572
+ Full authoring guide: `website/docs/developer-guide/model-provider-plugin.md`.
573
+
574
+ ### Dashboard / context-engine / image-gen plugin directories
575
+
576
+ `plugins/context_engine/`, `plugins/image_gen/`, etc. follow the same
577
+ pattern (ABC + orchestrator + per-plugin directory). Context engines
578
+ plug into `agent/context_engine.py`; image-gen providers into
579
+ `agent/image_gen_provider.py`. Reference / docs-companion plugins
580
+ (`example-dashboard`, `strike-freedom-cockpit`, `plugin-llm-example`,
581
+ `plugin-llm-async-example`) live in the
582
+ [`hermes-example-plugins`](https://github.com/NousResearch/hermes-example-plugins)
583
+ companion repo, not in this tree.
584
+
585
+ ---
586
+
587
+ ## Skills
588
+
589
+ Two parallel surfaces:
590
+
591
+ - **`skills/`** — built-in skills shipped and loadable by default.
592
+ Organized by category directories (e.g. `skills/github/`, `skills/mlops/`).
593
+ - **`optional-skills/`** — heavier or niche skills shipped with the repo but
594
+ NOT active by default. Installed explicitly via
595
+ `hermes skills install official/<category>/<skill>`. Adapter lives in
596
+ `tools/skills_hub.py` (`OptionalSkillSource`). Categories include
597
+ `autonomous-ai-agents`, `blockchain`, `communication`, `creative`,
598
+ `devops`, `email`, `health`, `mcp`, `migration`, `mlops`, `productivity`,
599
+ `research`, `security`, `web-development`.
600
+
601
+ When reviewing skill PRs, check which directory they target — heavy-dep or
602
+ niche skills belong in `optional-skills/`.
603
+
604
+ ### SKILL.md frontmatter
605
+
606
+ Standard fields: `name`, `description`, `version`, `author`, `license`,
607
+ `platforms` (OS-gating list: `[macos]`, `[linux, macos]`, ...),
608
+ `metadata.hermes.tags`, `metadata.hermes.category`,
609
+ `metadata.hermes.related_skills`, `metadata.hermes.config` (config.yaml
610
+ settings the skill needs — stored under `skills.config.<key>`, prompted
611
+ during setup, injected at load time).
612
+
613
+ Top-level `tags:` and `category:` are also accepted and mirrored from
614
+ `metadata.hermes.*` by the loader.
615
+
616
+ ### Skill authoring standards (HARDLINE)
617
+
618
+ Every new or modernized skill — bundled, optional, or contributed —
619
+ must meet these standards before merge. Reviewers reject PRs that
620
+ violate them.
621
+
622
+ 1. **`description` ≤ 60 characters, one sentence, ends with a period.**
623
+ Long descriptions bloat skill listings and dilute the model's
624
+ attention when many skills are loaded. State the capability, not
625
+ the implementation. No marketing words ("powerful",
626
+ "comprehensive", "seamless", "advanced"). Don't repeat the skill
627
+ name. Verify with:
628
+ ```python
629
+ import re, pathlib
630
+ m = re.search(r'^description: (.*)$',
631
+ pathlib.Path('skills/<cat>/<name>/SKILL.md').read_text(),
632
+ re.MULTILINE)
633
+ assert len(m.group(1)) <= 60, len(m.group(1))
634
+ ```
635
+
636
+ 2. **Tools referenced in SKILL.md prose must be native Hermes tools or
637
+ MCP servers the skill explicitly expects.** When the skill needs a
638
+ capability, point at the proper tool by name in backticks
639
+ (`` `terminal` ``, `` `web_extract` ``, `` `read_file` ``,
640
+ `` `patch` ``, `` `search_files` ``, `` `vision_analyze` ``,
641
+ `` `browser_navigate` ``, `` `delegate_task` ``, etc.). Do NOT
642
+ name shell utilities the agent already has wrapped — `grep` →
643
+ `search_files`, `cat`/`head`/`tail` → `read_file`, `sed`/`awk` →
644
+ `patch`, `find`/`ls` → `search_files target='files'`. If the skill
645
+ depends on an MCP server, name the MCP server and document the
646
+ expected setup in `## Prerequisites`. Anything else (third-party
647
+ CLIs, shell pipelines, etc.) is fair game inside script files but
648
+ should not be the headline interaction surface in the prose.
649
+
650
+ 3. **`platforms:` gating audited against actual script imports.**
651
+ Skills that use POSIX-only primitives (`fcntl`, `termios`,
652
+ `os.setsid`, `os.kill(pid, 0)` for liveness, `/proc`, `/tmp`
653
+ hardcoded, `signal.SIGKILL`, bash heredocs, `osascript`, `apt`,
654
+ `systemctl`) must declare their supported platforms. Default
655
+ posture: try to fix it cross-platform first — `tempfile.gettempdir`,
656
+ `pathlib.Path`, `psutil.pid_exists`, Python-level filtering instead
657
+ of `grep`. Gate to a narrower set only when the dependency is
658
+ genuinely platform-bound.
659
+
660
+ 4. **`author` credits the human contributor first.** For external
661
+ contributions, the contributor's real name + GitHub handle goes
662
+ first; "Hermes Agent" is the secondary collaborator. If the
663
+ contributor's commit shows "Hermes Agent" as author (because they
664
+ used Hermes to draft the skill), replace it with their actual name
665
+ — credit the human, not the tool.
666
+
667
+ 5. **SKILL.md body uses the modern section order.** `# <Skill> Skill`
668
+ title, 2-3 sentence intro stating what it does and doesn't do,
669
+ `## When to Use`, `## Prerequisites`, `## How to Run`,
670
+ `## Quick Reference`, `## Procedure`, `## Pitfalls`,
671
+ `## Verification`. Target ~200 lines for a complex skill,
672
+ ~100 lines for a simple one. Cut redundant intro fluff, marketing
673
+ prose, and re-explanations of env vars already in
674
+ `## Prerequisites`.
675
+
676
+ 6. **Scripts go in `scripts/`, references in `references/`,
677
+ templates in `templates/`.** Don't expect the model to inline-write
678
+ parsers, XML walkers, or non-trivial logic every call — ship a
679
+ helper script. Reference it from SKILL.md by path relative to the
680
+ skill directory.
681
+
682
+ 7. **Tests live at `tests/skills/test_<skill>_skill.py`** and use only
683
+ stdlib + pytest + `unittest.mock`. No live network calls. Run via
684
+ `scripts/run_tests.sh tests/skills/test_<skill>_skill.py -q`.
685
+
686
+ 8. **`.env.example` additions are isolated to a clearly delimited
687
+ block.** Don't touch the surrounding file — contributor-supplied
688
+ `.env.example` versions are usually stale and edits outside the
689
+ skill's own block must be dropped during salvage.
690
+
691
+ The full salvage / modernization checklist for external skill PRs
692
+ lives in the `hermes-agent-dev` skill at
693
+ `references/new-skill-pr-salvage.md` — load it before polishing
694
+ contributor skill PRs.
695
+
696
+ ---
697
+
698
+ ## Toolsets
699
+
700
+ All toolsets are defined in `toolsets.py` as a single `TOOLSETS` dict.
701
+ Each platform's adapter picks a base toolset (e.g. Telegram uses
702
+ `"messaging"`); `_HERMES_CORE_TOOLS` is the default bundle most
703
+ platforms inherit from.
704
+
705
+ Current toolset keys: `browser`, `clarify`, `code_execution`, `cronjob`,
706
+ `debugging`, `delegation`, `discord`, `discord_admin`, `feishu_doc`,
707
+ `feishu_drive`, `file`, `homeassistant`, `image_gen`, `kanban`, `memory`,
708
+ `messaging`, `moa`, `rl`, `safe`, `search`, `session_search`, `skills`,
709
+ `spotify`, `terminal`, `todo`, `tts`, `video`, `vision`, `web`, `yuanbao`.
710
+
711
+ Enable/disable per platform via `hermes tools` (the curses UI) or the
712
+ `tools.<platform>.enabled` / `tools.<platform>.disabled` lists in
713
+ `config.yaml`.
714
+
715
+ ---
716
+
717
+ ## Delegation (`delegate_task`)
718
+
719
+ `tools/delegate_tool.py` spawns a subagent with an isolated
720
+ context + terminal session. Synchronous: the parent waits for the
721
+ child's summary before continuing its own loop — if the parent is
722
+ interrupted, the child is cancelled.
723
+
724
+ Two shapes:
725
+
726
+ - **Single:** pass `goal` (+ optional `context`, `toolsets`).
727
+ - **Batch (parallel):** pass `tasks: [...]` — each gets its own subagent
728
+ running concurrently. Concurrency is capped by
729
+ `delegation.max_concurrent_children` (default 3).
730
+
731
+ Roles:
732
+
733
+ - `role="leaf"` (default) — focused worker. Cannot call `delegate_task`,
734
+ `clarify`, `memory`, `send_message`, `execute_code`.
735
+ - `role="orchestrator"` — retains `delegate_task` so it can spawn its
736
+ own workers. Gated by `delegation.orchestrator_enabled` (default true)
737
+ and bounded by `delegation.max_spawn_depth` (default 2).
738
+
739
+ Key config knobs (under `delegation:` in `config.yaml`):
740
+ `max_concurrent_children`, `max_spawn_depth`, `child_timeout_seconds`,
741
+ `orchestrator_enabled`, `subagent_auto_approve`, `inherit_mcp_toolsets`,
742
+ `max_iterations`.
743
+
744
+ Synchronicity rule: delegate_task is **not** durable. For long-running
745
+ work that must outlive the current turn, use `cronjob` or
746
+ `terminal(background=True, notify_on_complete=True)` instead.
747
+
748
+ ---
749
+
750
+ ## Curator (skill lifecycle)
751
+
752
+ Background skill-maintenance system that tracks usage on agent-created
753
+ skills and auto-archives stale ones. Users never lose skills; archives
754
+ go to `~/.hermes/skills/.archive/` and are restorable.
755
+
756
+ - **Core:** `agent/curator.py` (review loop, auto-transitions, LLM review
757
+ prompt) + `agent/curator_backup.py` (pre-run tar.gz snapshots).
758
+ - **CLI:** `hermes_cli/curator.py` wires `hermes curator <verb>` where
759
+ verbs are: `status`, `run`, `pause`, `resume`, `pin`, `unpin`,
760
+ `archive`, `restore`, `prune`, `backup`, `rollback`.
761
+ - **Telemetry:** `tools/skill_usage.py` owns the sidecar
762
+ `~/.hermes/skills/.usage.json` — per-skill `use_count`, `view_count`,
763
+ `patch_count`, `last_activity_at`, `state` (active / stale /
764
+ archived), `pinned`.
765
+
766
+ Invariants:
767
+ - Curator only touches skills with `created_by: "agent"` provenance —
768
+ bundled + hub-installed skills are off-limits.
769
+ - Never deletes; max destructive action is archive.
770
+ - Pinned skills are exempt from every auto-transition and from the
771
+ LLM review pass.
772
+ - `skill_manage(action="delete")` refuses pinned skills; patch/edit/
773
+ write_file/remove_file go through so the agent can keep improving
774
+ pinned skills.
775
+
776
+ Config section (`curator:` in `config.yaml`):
777
+ `enabled`, `interval_hours`, `min_idle_hours`, `stale_after_days`,
778
+ `archive_after_days`, `backup.*`.
779
+
780
+ Full user-facing docs: `website/docs/user-guide/features/curator.md`.
781
+
782
+ ---
783
+
784
+ ## Cron (scheduled jobs)
785
+
786
+ `cron/jobs.py` (job store) + `cron/scheduler.py` (tick loop). Agents
787
+ schedule jobs via the `cronjob` tool; users via `hermes cron <verb>`
788
+ (`list`, `add`, `edit`, `pause`, `resume`, `run`, `remove`) or the
789
+ `/cron` slash command.
790
+
791
+ Supported schedule formats:
792
+ - Duration: `"30m"`, `"2h"`, `"1d"`
793
+ - "every" phrase: `"every 2h"`, `"every monday 9am"`
794
+ - 5-field cron expression: `"0 9 * * *"`
795
+ - ISO timestamp (one-shot): `"2026-06-01T09:00:00Z"`
796
+
797
+ Per-job fields include `skills` (load specific skills), `model` /
798
+ `provider` overrides, `script` (pre-run data-collection script whose
799
+ stdout is injected into the prompt; `no_agent=True` turns the script
800
+ into the entire job), `context_from` (chain job A's last output into
801
+ job B's prompt), `workdir` (run in a specific directory with its
802
+ `AGENTS.md`/`CLAUDE.md` loaded), and multi-platform delivery.
803
+
804
+ Hardening invariants:
805
+ - **3-minute hard interrupt** on cron sessions — runaway agent loops
806
+ cannot monopolize the scheduler.
807
+ - Catchup window: half the job's period, clamped to 120s–2h.
808
+ - Grace window: 120s for one-shot jobs whose fire time was missed.
809
+ - File lock at `~/.hermes/cron/.tick.lock` prevents duplicate ticks
810
+ across processes.
811
+ - Cron sessions pass `skip_memory=True` by default; memory providers
812
+ intentionally do not run during cron.
813
+
814
+ Cron deliveries are **not** mirrored into the target gateway session —
815
+ they land in their own cron session with a header/footer frame so the
816
+ main conversation's message-role alternation stays intact.
817
+
818
+ ---
819
+
820
+ ## Kanban (multi-agent work queue)
821
+
822
+ Durable SQLite-backed board that lets multiple profiles / workers
823
+ collaborate on shared tasks. Users drive it via `hermes kanban <verb>`;
824
+ workers spawned by the dispatcher drive it via a dedicated `kanban_*`
825
+ toolset so their schema footprint is zero when they're not inside a
826
+ kanban task.
827
+
828
+ - **CLI:** `hermes_cli/kanban.py` wires `hermes kanban` with verbs
829
+ `init`, `create`, `list` (alias `ls`), `show`, `assign`, `link`,
830
+ `unlink`, `comment`, `complete`, `block`, `unblock`, `archive`,
831
+ `tail`, plus less-commonly-used `watch`, `stats`, `runs`, `log`,
832
+ `assignees`, `heartbeat`, `notify-*`, `dispatch`, `daemon`, `gc`.
833
+ - **Worker/orchestrator toolset:** `tools/kanban_tools.py` exposes
834
+ `kanban_show`, `kanban_complete`, `kanban_block`, `kanban_heartbeat`,
835
+ `kanban_comment`, `kanban_create`, `kanban_link`; profiles that
836
+ explicitly enable the `kanban` toolset outside a dispatcher-spawned
837
+ task also get `kanban_list` and `kanban_unblock` for board routing.
838
+ - **Dispatcher:** long-lived loop that (default every 60s) reclaims
839
+ stale claims, promotes ready tasks, atomically claims, and spawns
840
+ assigned profiles. Runs **inside the gateway** by default via
841
+ `kanban.dispatch_in_gateway: true`.
842
+ - **Plugin assets:** `plugins/kanban/dashboard/` (web UI) +
843
+ `plugins/kanban/systemd/` (`hermes-kanban-dispatcher.service` for
844
+ standalone dispatcher deployment).
845
+
846
+ Isolation model:
847
+ - **Board** is the hard boundary — workers are spawned with
848
+ `HERMES_KANBAN_BOARD` pinned in their env so they can't see other
849
+ boards.
850
+ - **Tenant** is a soft namespace *within* a board — one specialist
851
+ fleet can serve multiple businesses with workspace-path + memory-key
852
+ isolation.
853
+ - After `kanban.failure_limit` consecutive non-success attempts on the
854
+ same task (default: 2), the dispatcher auto-blocks it to prevent spin
855
+ loops.
856
+
857
+ Full user-facing docs: `website/docs/user-guide/features/kanban.md`.
858
+
859
+ ---
860
+
861
+ ## Important Policies
862
+
863
+ ### Prompt Caching Must Not Break
864
+
865
+ Hermes-Agent ensures caching remains valid throughout a conversation. **Do NOT implement changes that would:**
866
+ - Alter past context mid-conversation
867
+ - Change toolsets mid-conversation
868
+ - Reload memories or rebuild system prompts mid-conversation
869
+
870
+ Cache-breaking forces dramatically higher costs. The ONLY time we alter context is during context compression.
871
+
872
+ Slash commands that mutate system-prompt state (skills, tools, memory, etc.)
873
+ must be **cache-aware**: default to deferred invalidation (change takes
874
+ effect next session), with an opt-in `--now` flag for immediate
875
+ invalidation. See `/skills install --now` for the canonical pattern.
876
+
877
+ ### Background Process Notifications (Gateway)
878
+
879
+ When `terminal(background=true, notify_on_complete=true)` is used, the gateway runs a watcher that
880
+ detects process completion and triggers a new agent turn. Control verbosity of background process
881
+ messages with `display.background_process_notifications`
882
+ in config.yaml (or `HERMES_BACKGROUND_NOTIFICATIONS` env var):
883
+
884
+ - `all` — running-output updates + final message (default)
885
+ - `result` — only the final completion message
886
+ - `error` — only the final message when exit code != 0
887
+ - `off` — no watcher messages at all
888
+
889
+ ---
890
+
891
+ ## Profiles: Multi-Instance Support
892
+
893
+ Hermes supports **profiles** — multiple fully isolated instances, each with its own
894
+ `HERMES_HOME` directory (config, API keys, memory, sessions, skills, gateway, etc.).
895
+
896
+ The core mechanism: `_apply_profile_override()` in `hermes_cli/main.py` sets
897
+ `HERMES_HOME` before any module imports. All `get_hermes_home()` references
898
+ automatically scope to the active profile.
899
+
900
+ ### Rules for profile-safe code
901
+
902
+ 1. **Use `get_hermes_home()` for all HERMES_HOME paths.** Import from `hermes_constants`.
903
+ NEVER hardcode `~/.hermes` or `Path.home() / ".hermes"` in code that reads/writes state.
904
+ ```python
905
+ # GOOD
906
+ from hermes_constants import get_hermes_home
907
+ config_path = get_hermes_home() / "config.yaml"
908
+
909
+ # BAD — breaks profiles
910
+ config_path = Path.home() / ".hermes" / "config.yaml"
911
+ ```
912
+
913
+ 2. **Use `display_hermes_home()` for user-facing messages.** Import from `hermes_constants`.
914
+ This returns `~/.hermes` for default or `~/.hermes/profiles/<name>` for profiles.
915
+ ```python
916
+ # GOOD
917
+ from hermes_constants import display_hermes_home
918
+ print(f"Config saved to {display_hermes_home()}/config.yaml")
919
+
920
+ # BAD — shows wrong path for profiles
921
+ print("Config saved to ~/.hermes/config.yaml")
922
+ ```
923
+
924
+ 3. **Module-level constants are fine** — they cache `get_hermes_home()` at import time,
925
+ which is AFTER `_apply_profile_override()` sets the env var. Just use `get_hermes_home()`,
926
+ not `Path.home() / ".hermes"`.
927
+
928
+ 4. **Tests that mock `Path.home()` must also set `HERMES_HOME`** — since code now uses
929
+ `get_hermes_home()` (reads env var), not `Path.home() / ".hermes"`:
930
+ ```python
931
+ with patch.object(Path, "home", return_value=tmp_path), \
932
+ patch.dict(os.environ, {"HERMES_HOME": str(tmp_path / ".hermes")}):
933
+ ...
934
+ ```
935
+
936
+ 5. **Gateway platform adapters should use token locks** — if the adapter connects with
937
+ a unique credential (bot token, API key), call `acquire_scoped_lock()` from
938
+ `gateway.status` in the `connect()`/`start()` method and `release_scoped_lock()` in
939
+ `disconnect()`/`stop()`. This prevents two profiles from using the same credential.
940
+ See `gateway/platforms/telegram.py` for the canonical pattern.
941
+
942
+ 6. **Profile operations are HOME-anchored, not HERMES_HOME-anchored** — `_get_profiles_root()`
943
+ returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`.
944
+ This is intentional — it lets `hermes -p coder profile list` see all profiles regardless
945
+ of which one is active.
946
+
947
+ ## Known Pitfalls
948
+
949
+ ### DO NOT hardcode `~/.hermes` paths
950
+ Use `get_hermes_home()` from `hermes_constants` for code paths. Use `display_hermes_home()`
951
+ for user-facing print/log messages. Hardcoding `~/.hermes` breaks profiles — each profile
952
+ has its own `HERMES_HOME` directory. This was the source of 5 bugs fixed in PR #3575.
953
+
954
+ ### DO NOT introduce new `simple_term_menu` usage
955
+ Existing call sites in `hermes_cli/main.py` remain for legacy fallback only;
956
+ the preferred UI is curses (stdlib) because `simple_term_menu` has
957
+ ghost-duplication rendering bugs in tmux/iTerm2 with arrow keys. New
958
+ interactive menus must use `hermes_cli/curses_ui.py` — see
959
+ `hermes_cli/tools_config.py` for the canonical pattern.
960
+
961
+ ### DO NOT use `\033[K` (ANSI erase-to-EOL) in spinner/display code
962
+ Leaks as literal `?[K` text under `prompt_toolkit`'s `patch_stdout`. Use space-padding: `f"\r{line}{' ' * pad}"`.
963
+
964
+ ### `_last_resolved_tool_names` is a process-global in `model_tools.py`
965
+ `_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.
966
+
967
+ ### DO NOT hardcode cross-tool references in schema descriptions
968
+ 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.
969
+
970
+ ### The gateway has TWO message guards — both must bypass approval/control commands
971
+ When an agent is running, messages pass through two sequential guards:
972
+ (1) **base adapter** (`gateway/platforms/base.py`) queues messages in
973
+ `_pending_messages` when `session_key in self._active_sessions`, and
974
+ (2) **gateway runner** (`gateway/run.py`) intercepts `/stop`, `/new`,
975
+ `/queue`, `/status`, `/approve`, `/deny` before they reach
976
+ `running_agent.interrupt()`. Any new command that must reach the runner
977
+ while the agent is blocked (e.g. approval prompts) MUST bypass BOTH
978
+ guards and be dispatched inline, not via `_process_message_background()`
979
+ (which races session lifecycle).
980
+
981
+ ### Squash merges from stale branches silently revert recent fixes
982
+ Before squash-merging a PR, ensure the branch is up to date with `main`
983
+ (`git fetch origin main && git reset --hard origin/main` in the worktree,
984
+ then re-apply the PR's commits). A stale branch's version of an unrelated
985
+ file will silently overwrite recent fixes on main when squashed. Verify
986
+ with `git diff HEAD~1..HEAD` after merging — unexpected deletions are a
987
+ red flag.
988
+
989
+ ### Don't wire in dead code without E2E validation
990
+ Unused code that was never shipped was dead for a reason. Before wiring an
991
+ unused module into a live code path, E2E test the real resolution chain
992
+ with actual imports (not mocks) against a temp `HERMES_HOME`.
993
+
994
+ ### Tests must not write to `~/.hermes/`
995
+ The `_isolate_hermes_home` autouse fixture in `tests/conftest.py` redirects `HERMES_HOME` to a temp dir. Never hardcode `~/.hermes/` paths in tests.
996
+
997
+ **Profile tests**: When testing profile features, also mock `Path.home()` so that
998
+ `_get_profiles_root()` and `_get_default_hermes_home()` resolve within the temp dir.
999
+ Use the pattern from `tests/hermes_cli/test_profiles.py`:
1000
+ ```python
1001
+ @pytest.fixture
1002
+ def profile_env(tmp_path, monkeypatch):
1003
+ home = tmp_path / ".hermes"
1004
+ home.mkdir()
1005
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
1006
+ monkeypatch.setenv("HERMES_HOME", str(home))
1007
+ return home
1008
+ ```
1009
+
1010
+ ---
1011
+
1012
+ ## Testing
1013
+
1014
+ **ALWAYS use `scripts/run_tests.sh`** — do not call `pytest` directly. The script enforces
1015
+ hermetic environment parity with CI (unset credential vars, TZ=UTC, LANG=C.UTF-8,
1016
+ `-n auto` xdist workers, in-tree subprocess-isolation plugin). Direct `pytest`
1017
+ on a 16+ core developer machine with API keys set diverges from CI in ways
1018
+ that have caused multiple "works locally, fails in CI" incidents (and the reverse).
1019
+
1020
+ ```bash
1021
+ scripts/run_tests.sh # full suite, CI-parity
1022
+ scripts/run_tests.sh tests/gateway/ # one directory
1023
+ scripts/run_tests.sh tests/agent/test_foo.py::test_x # one test
1024
+ scripts/run_tests.sh -v --tb=long # pass-through pytest flags
1025
+ scripts/run_tests.sh --no-isolate tests/foo/ # disable subprocess isolation (faster, for debugging)
1026
+ ```
1027
+
1028
+ ### Subprocess-per-test isolation
1029
+
1030
+ Every test runs in a freshly-spawned Python subprocess via the in-tree plugin
1031
+ at `tests/_isolate_plugin.py`. This means module-level dicts/sets and
1032
+ ContextVars from one test cannot leak into the next — the historic
1033
+ `_reset_module_state` autouse fixture is gone.
1034
+
1035
+ Implementation notes:
1036
+
1037
+ - The plugin uses `multiprocessing.get_context("spawn")`, which works on
1038
+ Linux, macOS, and Windows alike (POSIX `fork` is not used).
1039
+ - Per-test overhead is ~0.5–1.0s (Python startup + pytest collection). xdist
1040
+ parallelism amortizes this across cores; on a 20-core box the full suite
1041
+ finishes in roughly the same wall time as before, but flake-free.
1042
+ - `isolate_timeout` (configured in `pyproject.toml`) caps each test at 30s.
1043
+ Hangs are killed and surfaced as a failure report.
1044
+ - Pass `--no-isolate` to disable isolation — useful when debugging a single
1045
+ test interactively, or when you specifically want to verify state leakage.
1046
+ - The plugin disables itself in child processes (sentinel envvar
1047
+ `HERMES_ISOLATE_CHILD=1`), so there's no fork-bomb risk.
1048
+
1049
+ ### Why the wrapper (and why the old "just call pytest" doesn't work)
1050
+
1051
+ Five real sources of local-vs-CI drift the script closes:
1052
+
1053
+ | | Without wrapper | With wrapper |
1054
+ |---|---|---|
1055
+ | Provider API keys | Whatever is in your env (auto-detects pool) | All `*_API_KEY`/`*_TOKEN`/etc. unset |
1056
+ | HOME / `~/.hermes/` | Your real config+auth.json | Temp dir per test |
1057
+ | Timezone | Local TZ (PDT etc.) | UTC |
1058
+ | Locale | Whatever is set | C.UTF-8 |
1059
+ | xdist workers | `-n auto` = all cores | `-n auto` (safe — subprocess isolation prevents cross-worker flakes) |
1060
+
1061
+ `tests/conftest.py` also enforces points 1-4 as an autouse fixture so ANY pytest
1062
+ invocation (including IDE integrations) gets hermetic behavior — but the wrapper
1063
+ is belt-and-suspenders.
1064
+
1065
+ ### Running without the wrapper (only if you must)
1066
+
1067
+ If you can't use the wrapper (e.g. inside an IDE that shells pytest directly),
1068
+ at minimum activate the venv. The isolation plugin loads automatically from
1069
+ `addopts` in `pyproject.toml`, so you get the same per-test process isolation
1070
+ either way.
1071
+
1072
+ ```bash
1073
+ source .venv/bin/activate # or: source venv/bin/activate
1074
+ python -m pytest tests/ -q
1075
+ ```
1076
+
1077
+ If you need to bypass isolation for fast feedback while debugging:
1078
+
1079
+ ```bash
1080
+ python -m pytest tests/agent/test_foo.py -q --no-isolate
1081
+ ```
1082
+
1083
+ Always run the full suite before pushing changes.
1084
+
1085
+ ### Don't write change-detector tests
1086
+
1087
+ A test is a **change-detector** if it fails whenever data that is **expected
1088
+ to change** gets updated — model catalogs, config version numbers,
1089
+ enumeration counts, hardcoded lists of provider models. These tests add no
1090
+ behavioral coverage; they just guarantee that routine source updates break
1091
+ CI and cost engineering time to "fix."
1092
+
1093
+ **Do not write:**
1094
+
1095
+ ```python
1096
+ # catalog snapshot — breaks every model release
1097
+ assert "gemini-2.5-pro" in _PROVIDER_MODELS["gemini"]
1098
+ assert "MiniMax-M2.7" in models
1099
+
1100
+ # config version literal — breaks every schema bump
1101
+ assert DEFAULT_CONFIG["_config_version"] == 21
1102
+
1103
+ # enumeration count — breaks every time a skill/provider is added
1104
+ assert len(_PROVIDER_MODELS["huggingface"]) == 8
1105
+ ```
1106
+
1107
+ **Do write:**
1108
+
1109
+ ```python
1110
+ # behavior: does the catalog plumbing work at all?
1111
+ assert "gemini" in _PROVIDER_MODELS
1112
+ assert len(_PROVIDER_MODELS["gemini"]) >= 1
1113
+
1114
+ # behavior: does migration bump the user's version to current latest?
1115
+ assert raw["_config_version"] == DEFAULT_CONFIG["_config_version"]
1116
+
1117
+ # invariant: no plan-only model leaks into the legacy list
1118
+ assert not (set(moonshot_models) & coding_plan_only_models)
1119
+
1120
+ # invariant: every model in the catalog has a context-length entry
1121
+ for m in _PROVIDER_MODELS["huggingface"]:
1122
+ assert m.lower() in DEFAULT_CONTEXT_LENGTHS_LOWER
1123
+ ```
1124
+
1125
+ The rule: if the test reads like a snapshot of current data, delete it. If
1126
+ it reads like a contract about how two pieces of data must relate, keep it.
1127
+ When a PR adds a new provider/model and you want a test, make the test
1128
+ assert the relationship (e.g. "catalog entries all have context lengths"),
1129
+ not the specific names.
1130
+
1131
+ Reviewers should reject new change-detector tests; authors should convert
1132
+ them into invariants before re-requesting review.