mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
The background write guard decided ownership from `isinstance(usage_rec, dict)`, so a local skill with NO usage record passed. That successful write called bump_patch(), which created a `created_by: null` record — and the identical write was refused from then on. "Allowed exactly once, then never" is a race with our own bookkeeping, not a policy. Reproduced on main: patch #1 succeeds, patch #2 with the same arguments is refused. Option B from the issue. Option A (split `session_review` from `scheduled_curator` and let the session fork patch user-owned skills it consulted) would widen autonomous write permission onto skills the user owns with no user present to consent — wrong direction for a no-user-present actor. - skill_manager_tool: missing and explicit-null records now resolve IDENTICALLY, both fail closed. The refusal names the reason and points at `hermes curator adopt <name>`. - background_review: both review prompts told the reviewer to patch any skill consulted in the session and claimed pinned skills could be improved, while enforcement refused both. Prompts now list pinned, external, and user-owned skills as protected, and tell the reviewer to RECOMMEND adoption instead of attempting a write that will be refused. - skill_usage: document that `created_by` is a curator-management policy flag, not a provenance claim, and add `is_curator_managed()` so call sites read as the question they ask. Field name retained — it is on disk in every `.usage.json` and renaming would strand those records. - curator CLI: `hermes curator list-unmanaged` itemizes unmanaged skills with the reason each is unmanaged (completes the #67139 spec). Foreground writes are untouched: a user-directed edit to a user-owned skill still works, including on pinned skills. Sibling tests: 9 failures in test_skill_manager_tool.py were fixtures that created record-less skills to exercise OTHER guards (consolidation-delete, read-before-write) and relied on ownership falling through. Fixed at the fixture, since the real curator only ever operates on managed sediment. One test asserted the old "manually authored" wording; rewritten to assert the behavior contract instead of the string. Validation: 274 targeted tests + all 7 background-review files (60 tests) pass. E2E on a temp HERMES_HOME (30 checks) covers the flip, foreground writes, adoption unblocking, pin semantics, prompt/enforcement parity, and the new verb. Each new test sabotage-verified: revert the fix, confirm it goes red. Fixes #67140
354 lines
20 KiB
Markdown
354 lines
20 KiB
Markdown
---
|
|
sidebar_position: 3
|
|
title: "Curator"
|
|
description: "Background maintenance for agent-created skills — usage tracking, staleness, archival, and LLM-driven review"
|
|
---
|
|
|
|
# Curator
|
|
|
|
The curator is a background maintenance pass for **agent-created skills**. It tracks how often each skill is viewed, used, and patched, moves long-unused skills through `active → stale → archived` states, and periodically spawns a short auxiliary-model review that proposes consolidations or patches drift.
|
|
|
|
It exists so that skills created via the [self-improvement loop](/user-guide/features/skills#agent-managed-skills-skill_manage-tool) don't pile up forever. Every time the agent solves a novel problem and saves a skill, that skill lands in `~/.hermes/skills/`. Without maintenance, you end up with dozens of narrow near-duplicates that pollute the catalog and waste tokens.
|
|
|
|
By default (`prune_builtins: true`) the curator can archive **unused bundled built-in skills** (shipped with the repo) after `archive_after_days` of non-use, alongside the agent-created skills it primarily manages. Hub-installed skills (from [agentskills.io](https://agentskills.io)) are always off-limits. Set `curator.prune_builtins: false` to restore the old agent-created-only behavior, where bundled skills are never touched. The curator also **never auto-deletes** — the worst outcome is archival into `~/.hermes/skills/.archive/`, which is recoverable.
|
|
|
|
Tracks [issue #7816](https://github.com/NousResearch/hermes-agent/issues/7816).
|
|
|
|
## How it runs
|
|
|
|
The curator is triggered by an inactivity check, not a cron daemon. On CLI session start, and on a recurring tick inside the gateway's cron-ticker thread, Hermes checks whether:
|
|
|
|
1. Enough time has passed since the last curator run (`interval_hours`, default **7 days**), and
|
|
2. The agent has been idle long enough (`min_idle_hours`, default **2 hours**).
|
|
|
|
If both are true, it spawns a background fork of `AIAgent` — the same pattern used by the memory/skill self-improvement nudges. The fork runs in its own prompt cache and never touches the active conversation.
|
|
|
|
:::info First-run behavior
|
|
On a brand-new install (or the first time a pre-curator install ticks after `hermes update`), the curator **does not run immediately**. The first observation seeds `last_run_at` to "now" and defers the first real pass by one full `interval_hours`. This gives you a full interval to review your skill library, pin anything important, or opt out entirely before the curator ever touches it.
|
|
|
|
If you want to see what the curator *would* do before it runs for real, run `hermes curator run --dry-run` — it produces the same review report without mutating the library.
|
|
:::
|
|
|
|
A run has two phases:
|
|
|
|
1. **Automatic transitions** (deterministic, no LLM). Skills unused for `stale_after_days` (30) become `stale`; skills unused for `archive_after_days` (90) are moved to `~/.hermes/skills/.archive/`. This is the always-on pruning behavior — it runs whenever the curator is enabled, with no aux-model cost.
|
|
2. **LLM consolidation** (single aux-model pass, `max_iterations=8`) — **OFF by default**. When `curator.consolidate: true`, the forked agent surveys the agent-created skills, can read any of them with `skill_view`, and decides per-skill whether to keep, patch (via `skill_manage`), consolidate overlapping ones into class-level umbrellas, or archive via the terminal tool. Consolidation treats a skill as a full package: if a skill has `references/`, `templates/`, `scripts/`, `assets/`, or relative links to those paths, the curator must either keep it standalone, re-home the needed support files and rewrite paths, or archive the entire package unchanged — not flatten only `SKILL.md` into another skill's `references/` file.
|
|
|
|
:::info Consolidation is opt-in
|
|
By default the curator only **prunes** — the deterministic inactivity pass marks skills stale and archives long-unused ones. The opinionated LLM **consolidation** pass (umbrella-building, merging overlapping skills) is off by default because it costs aux-model tokens on every run and makes broad structural changes to your library. Turn it on with `curator.consolidate: true`, or run it once on demand with `hermes curator run --consolidate`.
|
|
:::
|
|
|
|
Pinned skills are off-limits to both the curator's auto-transitions and the agent's own `skill_manage` tool. See [Pinning a skill](#pinning-a-skill) below.
|
|
|
|
## Configuration
|
|
|
|
All settings live in `config.yaml` under `curator:` (not `.env` — this isn't a secret). Defaults:
|
|
|
|
```yaml
|
|
curator:
|
|
enabled: true
|
|
interval_hours: 168 # 7 days
|
|
min_idle_hours: 2
|
|
stale_after_days: 30
|
|
archive_after_days: 90
|
|
consolidate: false # LLM umbrella-building pass — opt-in (prune-only by default)
|
|
prune_builtins: true # archive unused bundled built-in skills too (hub skills always exempt)
|
|
```
|
|
|
|
To disable entirely, set `curator.enabled: false`. To keep the always-on pruning but opt into LLM consolidation, set `curator.consolidate: true`.
|
|
|
|
### Running the review on a cheaper aux model
|
|
|
|
The curator's LLM review pass is a regular auxiliary task slot — `auxiliary.curator` — alongside Vision, Compression, Session Search, etc. "Auto" means "use my main chat model"; override the slot to pin a specific provider + model for the review pass instead.
|
|
|
|
**Easiest — `hermes model`:**
|
|
|
|
```bash
|
|
hermes model # → "Auxiliary models — side-task routing"
|
|
# → pick "Curator" → pick provider → pick model
|
|
```
|
|
|
|
The same picker is available in the web dashboard under the **Models** tab.
|
|
|
|
**Direct config.yaml (equivalent):**
|
|
|
|
```yaml
|
|
auxiliary:
|
|
curator:
|
|
provider: openrouter
|
|
model: google/gemini-3-flash-preview
|
|
timeout: 600 # generous — reviews can take several minutes
|
|
```
|
|
|
|
Leaving `provider: auto` (the default) routes the review pass through whatever your main chat model is, matching the behavior of every other auxiliary task.
|
|
|
|
:::note Legacy config
|
|
Earlier releases used a one-off `curator.auxiliary.{provider,model}` block. That path still works but emits a deprecation log line — please migrate to `auxiliary.curator` above so the curator shares the same plumbing (`hermes model`, dashboard Models tab, `base_url`, `api_key`, `timeout`, `extra_body`) as every other aux task.
|
|
:::
|
|
|
|
## CLI
|
|
|
|
```bash
|
|
hermes curator status # last run, counts, pinned list, LRU top 5
|
|
hermes curator run # trigger a run now (blocks until done). Prune-only unless curator.consolidate: true
|
|
hermes curator run --consolidate # force the LLM consolidation pass on for this run, overriding the config default
|
|
hermes curator run --background # fire-and-forget: start the run in a background thread
|
|
hermes curator run --dry-run # preview only — report without any mutations
|
|
hermes curator backup # take a manual snapshot of ~/.hermes/skills/
|
|
hermes curator rollback # restore from the newest snapshot
|
|
hermes curator rollback --list # list available snapshots
|
|
hermes curator rollback --id <ts> # restore a specific snapshot
|
|
hermes curator rollback -y # skip the confirmation prompt
|
|
hermes curator pause # stop runs until resumed
|
|
hermes curator resume
|
|
hermes curator pin <skill> # never auto-transition this skill
|
|
hermes curator unpin <skill>
|
|
hermes curator adopt <skill> # hand an unmanaged skill to the curator
|
|
hermes curator adopt --all-unmanaged # hand over every unmanaged skill
|
|
hermes curator list-unmanaged # itemize skills with no provenance marker
|
|
hermes curator restore <skill> # move an archived skill back to active
|
|
hermes curator list-archived # list skills currently in ~/.hermes/skills/.archive/
|
|
hermes curator archive <skill> # manually archive a single skill now
|
|
hermes curator prune [--days N] # bulk-archive agent-created skills idle >= N days (default 90)
|
|
```
|
|
|
|
## Backups and rollback
|
|
|
|
Before every real curator pass, Hermes takes a tar.gz snapshot of `~/.hermes/skills/` at `~/.hermes/skills/.curator_backups/<utc-iso>/skills.tar.gz`. If a pass archives or consolidates something you didn't want touched, you can undo the whole run with one command:
|
|
|
|
```bash
|
|
hermes curator rollback # restore newest snapshot (with confirmation)
|
|
hermes curator rollback -y # skip the prompt
|
|
hermes curator rollback --list # see all snapshots with reason + size
|
|
```
|
|
|
|
The rollback itself is reversible: before replacing the skills tree, Hermes takes another snapshot tagged `pre-rollback to <target-id>`, so a mistaken rollback can be undone by rolling forward to that one with `--id`.
|
|
|
|
You can also take manual snapshots at any time with `hermes curator backup --reason "before-refactor"`. The `--reason` string lands in the snapshot's `manifest.json` and is shown in `--list`.
|
|
|
|
Snapshots are pruned to `curator.backup.keep` (default 5) to keep disk usage bounded:
|
|
|
|
```yaml
|
|
curator:
|
|
backup:
|
|
enabled: true
|
|
keep: 5
|
|
```
|
|
|
|
Set `curator.backup.enabled: false` to disable automatic snapshotting. The manual `hermes curator backup` command still works when backups are disabled only if you set `enabled: true` first — the flag gates both paths symmetrically so there's no way to accidentally skip the pre-run snapshot on mutating runs.
|
|
|
|
`hermes curator status` also lists the five least-recently-used skills — a quick way to see what's likely to become stale next.
|
|
|
|
The same subcommands are available as the `/curator` slash command inside a running session (CLI or gateway platforms).
|
|
|
|
## What "agent-created" means
|
|
|
|
The curator only manages skills explicitly marked as **agent-created** in
|
|
`~/.hermes/skills/.usage.json`. A skill qualifies when ALL of the following
|
|
are true:
|
|
|
|
1. Its name is **not** in `~/.hermes/skills/.bundled_manifest` (bundled skills shipped with the repo).
|
|
2. Its name is **not** in `~/.hermes/skills/.hub/lock.json` (hub-installed skills).
|
|
3. Its `.usage.json` entry has `"created_by": "agent"` or `"agent_created": true`.
|
|
|
|
Currently, only the **background self-improvement review fork** sets this marker
|
|
— when it creates a new umbrella skill during its periodic review pass (~every 10
|
|
agent turns). The background fork runs with a write origin of `"background_review"`
|
|
(via `tools/skill_provenance.py`), which is the only path that triggers the
|
|
`mark_agent_created()` call in `skill_manage`.
|
|
|
|
Skills the foreground agent creates via `skill_manage(action="create")` during a
|
|
conversation are **not** marked as agent-created — they are considered
|
|
user-directed and the curator intentionally leaves them alone.
|
|
|
|
:::warning Your hand-written skills are NOT curated
|
|
If you manually created a `SKILL.md` or pointed Hermes at an external skill
|
|
directory, that skill will have a `.usage.json` entry with `created_by: null`
|
|
(or the field absent). The curator will not touch it. The same applies to
|
|
skills the foreground agent created at your request.
|
|
|
|
**To see which skills the curator actually manages**, run `hermes curator status`.
|
|
If the agent-created count is 0, no skills are currently in the curator's
|
|
jurisdiction — the LLM review pass is skipped and the report will show
|
|
`Model: (not resolved) via (not resolved)` with `Duration: 0s`.
|
|
:::
|
|
|
|
### Adopting unmanaged skills
|
|
|
|
`hermes curator status` reports an **unmanaged** count alongside the managed
|
|
one:
|
|
|
|
```
|
|
curator-managed skills: 43 total (agent-created=43 bundled=0)
|
|
active 41
|
|
stale 2
|
|
archived 0
|
|
|
|
unmanaged (no provenance marker): 112 total
|
|
pre-dates marker 34
|
|
foreground-created 78
|
|
never auto-staled or archived — `hermes curator adopt <name>` hands one over
|
|
```
|
|
|
|
Those 112 are curation-*eligible* but permanently invisible to the lifecycle,
|
|
for one of two reasons:
|
|
|
|
- **pre-dates marker** — the record was written before `created_by` existed, so
|
|
it carries no provenance signal at all. Authorship is genuinely unknowable
|
|
from the record.
|
|
- **foreground-created** — a foreground `skill_manage(create)` left the marker
|
|
unset by design, since skills you ask for belong to you.
|
|
|
|
A large library can therefore look fully curated while most of it is
|
|
untouchable. `adopt` closes that gap by **declaration**:
|
|
|
|
```bash
|
|
hermes curator list-unmanaged # itemize them, with reasons
|
|
hermes curator adopt <name> [<name> ...] # hand specific skills over
|
|
hermes curator adopt --all-unmanaged --dry-run # preview the full list
|
|
hermes curator adopt --all-unmanaged # hand over everything (prompts)
|
|
hermes curator adopt --all-unmanaged --yes # skip the prompt
|
|
```
|
|
|
|
Adoption writes the same `created_by: agent` marker the background review fork
|
|
writes. It does **not** reset the inactivity clock — an adopted skill keeps its
|
|
existing `last_activity_at`, so handing over a library you already stopped
|
|
using does not buy it a fresh 90-day window. Expect adopted long-idle skills to
|
|
go `stale` (or `archived`) on the next pass; that is the point.
|
|
|
|
Adoption is also what unblocks autonomous *improvement*. The background review
|
|
fork refuses to patch a skill that isn't curator-managed, so if it notices one
|
|
of your skills is outdated it will say so and recommend adoption rather than
|
|
edit it. Foreground (user-directed) edits are never affected — you and the
|
|
agent can always edit your own skills on request.
|
|
|
|
:::note `created_by` is a policy flag, not a provenance claim
|
|
The stored field is named `created_by`, but it is consumed as "may autonomous
|
|
curation touch this?" — not "who wrote this file". Those are different
|
|
questions, and for records predating the marker the authorship answer is simply
|
|
unrecoverable. The name is kept because it is already on disk in every
|
|
`.usage.json`; read it as policy. `hermes curator adopt` changes the policy, and
|
|
says nothing about who authored the file.
|
|
:::
|
|
|
|
:::note Provenance is declared, never inferred
|
|
Adoption is deliberately manual. Telemetry cannot establish authorship: a skill
|
|
with thousands of patches proves the agent **maintains** it, not that the agent
|
|
**wrote** it — Hermes edits user-authored skills on your behalf constantly. An
|
|
automatic "looks agent-made, adopt it" heuristic would eventually archive
|
|
something you hand-wrote. `adopt` refuses bundled, hub-installed, external, and
|
|
protected built-in skills, which have an owner other than you.
|
|
:::
|
|
|
|
Skills that ARE agent-created follow the full lifecycle:
|
|
|
|
- `active` → (30d unused) `stale` → (90d unused) `archived`
|
|
- Pinned skills bypass all auto-transitions
|
|
- Archives are recoverable via `hermes curator restore <name>`
|
|
|
|
If you want to protect a specific skill from ever being touched — for example a
|
|
hand-authored skill you rely on — use `hermes curator pin <name>`. See the next
|
|
section.
|
|
|
|
## Pinning a skill
|
|
|
|
Pinning protects a skill from deletion — both the curator's automated archive passes and the agent's `skill_manage(action="delete")` tool call. Once a skill is pinned:
|
|
|
|
- The **curator** skips it during auto-transitions (`active → stale → archived`), and its LLM review pass is instructed to leave it alone.
|
|
- The **agent's `skill_manage` tool** refuses `delete` on it, pointing the user at `hermes curator unpin <name>`. Patches and edits still go through, so the agent can improve a pinned skill's content as pitfalls come up without a pin/unpin/re-pin dance.
|
|
|
|
Pin and unpin with:
|
|
|
|
```bash
|
|
hermes curator pin <skill>
|
|
hermes curator unpin <skill>
|
|
```
|
|
|
|
The flag is stored as `"pinned": true` on the skill's entry in `~/.hermes/skills/.usage.json`, so it survives across sessions.
|
|
|
|
Only **agent-created** skills can be pinned — `hermes curator pin` refuses on bundled and hub-installed skills with an explanatory message if you try. Hub-installed skills are never subject to curator mutation. Bundled built-in skills are only touched when `curator.prune_builtins: true` (the default), and even then only archived after `archive_after_days` of non-use — never patched, consolidated, or deleted. Set `curator.prune_builtins: false` to exempt bundled skills entirely.
|
|
|
|
A small set of **protected built-ins** is hardcoded as never-archivable and never-consolidatable, regardless of `curator.prune_builtins`, pin state, or LLM judgment. These back load-bearing UX — for example, `plan` powers the `/plan` slash-command flow — so silently archiving one would turn its slash command into an "Unknown command" error with no signal to you. Protected built-ins are filtered out of the curator's candidate list entirely, so the consolidation pass never sees them.
|
|
|
|
If you want a stronger guarantee than "no deletion" — for instance, freezing a skill's content entirely while the agent still reads it — edit `~/.hermes/skills/<name>/SKILL.md` directly with your editor. The pin guards tool-driven deletion, not your own filesystem access.
|
|
|
|
## Usage telemetry
|
|
|
|
The curator maintains a sidecar at `~/.hermes/skills/.usage.json` with one entry per skill:
|
|
|
|
```json
|
|
{
|
|
"my-skill": {
|
|
"use_count": 12,
|
|
"view_count": 34,
|
|
"last_used_at": "2026-04-24T18:12:03Z",
|
|
"last_viewed_at": "2026-04-23T09:44:17Z",
|
|
"patch_count": 3,
|
|
"last_patched_at": "2026-04-20T22:01:55Z",
|
|
"created_at": "2026-03-01T14:20:00Z",
|
|
"state": "active",
|
|
"pinned": false,
|
|
"archived_at": null
|
|
}
|
|
}
|
|
```
|
|
|
|
Counters increment when:
|
|
|
|
- `view_count`: the agent calls `skill_view` on the skill.
|
|
- `use_count`: the skill is loaded into a conversation's prompt.
|
|
- `patch_count`: `skill_manage patch/edit/write_file/remove_file` runs on the skill.
|
|
|
|
Bundled and hub-installed skills are explicitly excluded from telemetry writes.
|
|
|
|
## Per-run reports
|
|
|
|
Every curator run writes a timestamped directory under `~/.hermes/logs/curator/`:
|
|
|
|
```
|
|
~/.hermes/logs/curator/
|
|
└── 20260429-111512/
|
|
├── run.json # machine-readable: full fidelity, stats, LLM output
|
|
└── REPORT.md # human-readable summary
|
|
```
|
|
|
|
`REPORT.md` is a quick way to see what a given run did — which skills transitioned, what the LLM reviewer said, which skills it patched. Good for auditing without having to grep `agent.log`.
|
|
|
|
:::note No candidates? Report shows `(not resolved)`
|
|
When the curator has **no agent-created skills** to review, the LLM review pass
|
|
is skipped entirely. The report header will show
|
|
`Model: (not resolved) via (not resolved)` with `Duration: 0s` — this does **not**
|
|
indicate a configuration error or model resolution failure. It simply means there
|
|
were no candidates, so no model was ever invoked. The auto-transition phase still
|
|
runs and reports its counts normally.
|
|
:::
|
|
|
|
### Rename map in the summary
|
|
|
|
If a run consolidated multiple skills under an umbrella (or merged near-duplicates), the user-visible summary printed at the end of the run includes an explicit rename map showing every `old-name → new-name` pair the curator applied. This is in addition to per-skill transition lines, so when a wave of renames lands you can spot them at a glance without diffing the JSON report. The hint also surfaces under `hermes curator pin` so you can pin the umbrella name immediately if you want to lock the new label in.
|
|
|
|
## Restoring an archived skill
|
|
|
|
If the curator archived something you still want:
|
|
|
|
```bash
|
|
hermes curator restore <skill-name>
|
|
```
|
|
|
|
This moves the skill back from `~/.hermes/skills/.archive/` to the active tree and resets its state to `active`. The restore refuses if a bundled or hub-installed skill has since been installed under the same name (would shadow upstream).
|
|
|
|
## Disabling per environment
|
|
|
|
The curator is on by default. To turn it off:
|
|
|
|
- **For one profile only:** edit `~/.hermes/config.yaml` (or the active profile's config) and set `curator.enabled: false`.
|
|
- **For just one run:** `hermes curator pause` — the pause persists across sessions; use `resume` to re-enable.
|
|
|
|
The curator also refuses to run if `min_idle_hours` hasn't elapsed, so on an active dev machine it naturally only runs during quiet stretches.
|
|
|
|
## See also
|
|
|
|
- [Skills System](/user-guide/features/skills) — how skills work in general and the self-improvement loop that creates them
|
|
- [Memory](/user-guide/features/memory) — a parallel background review that maintains long-term memory
|
|
- [Bundled Skills Catalog](/reference/skills-catalog)
|
|
- [Issue #7816](https://github.com/NousResearch/hermes-agent/issues/7816) — original proposal and design discussion
|