Skip to content

feat(curator): background skill maintenance (issue #7816)#16049

Closed
teknium1 wants to merge 6 commits into
mainfrom
hermes/curator-infra
Closed

feat(curator): background skill maintenance (issue #7816)#16049
teknium1 wants to merge 6 commits into
mainfrom
hermes/curator-infra

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

@teknium1 teknium1 commented Apr 26, 2026

Summary

Adds the Curator — an auxiliary-model background task that periodically reviews agent-created skills and keeps the collection tidy. Tracks usage, transitions unused skills through active → stale → archived, and spawns a forked AIAgent that uses skill_manage + terminal to consolidate overlaps and patch drift.

Paired with the class-first review prompt from #16026, this closes the loop on issue #7816: creation-side class-first thinking + retirement-side pruning.

Invariants (all load-bearing)

  • Never touches bundled or hub-installed skills. Double-filtered against .bundled_manifest AND .hub/lock.json.
  • Never auto-deletes. Archive only — archives live in ~/.hermes/skills/.archive/ and are recoverable via hermes curator restore <skill>.
  • Pinned skills bypass all auto-transitions. Pinning is user-initiated; the model never pins autonomously.
  • Uses the aux client. Never touches the main session's prompt cache.

How it works

Default: enabled, inactivity-triggered (no cron daemon). On CLI startup and gateway boot:

  1. Check config + .curator_state — run if last_run_at is older than interval_hours (default 24) AND agent has been idle ≥ min_idle_hours (default 2).

  2. Automatic state transitions (pure, no LLM): mark stale (>30d unused), archive (>90d unused), reactivate stale skills that were used again.

  3. LLM review pass — spawns forked AIAgent (max_iterations=8, quiet) with the agent-created candidate list + usage stats. The model uses existing tools only:

    • skills_list / skill_view — survey the landscape
    • skill_manage action=patch — fix drift, absorb a weaker overlapping skill
    • terminalmv a skill directory into ~/.hermes/skills/.archive/ to archive it

    Decision list: keep / patch / consolidate / archive. No pin (that's a user-only opt-out).

  4. Persist .curator_state with the summary.

Changes

File What
tools/skill_usage.py (new) Sidecar .usage.json I/O, atomic writes, provenance filter, archive/restore helpers (used by the CLI, not exposed to the model)
agent/curator.py (new) Orchestrator: config, idle gating, state-machine transitions, forked-agent LLM pass
hermes_cli/curator.py (new) hermes curator {status,run,pause,resume,pin,unpin,restore} subcommand
tests/tools/test_skill_usage.py (new) 29 unit tests
tests/agent/test_curator.py (new) 27 unit tests (LLM pass stubbed — fully offline)
tools/skills_tool.py Wrap skill_view registry handler to bump view_count on success
tools/skill_manager_tool.py Bump patch_count on patch/edit/write_file/remove_file; forget() record on delete
hermes_cli/config.py Add curator: section to DEFAULT_CONFIG (additive — no version bump needed)
hermes_cli/commands.py Add /curator CommandDef with subcommand hints
hermes_cli/main.py Register hermes curator argparse subparser via register_cli()
cli.py /curator slash-command dispatch + CLI-startup hook
gateway/run.py Gateway-boot hook — mirrors CLI

Lifecycle

           ┌─ user pins (bypasses cycle)
           │
  CREATE ──► ACTIVE ──── 30d idle ────► STALE
               ▲                           │
               │                      90d idle
               │                           │
        recent use                         ▼
               │                       ARCHIVED ──► hermes curator restore
               └───────────────────────────┘

Configuration (new)

curator:
  enabled: true
  interval_hours: 24
  min_idle_hours: 2
  stale_after_days: 30
  archive_after_days: 90
  auxiliary:
    provider: null
    model: null

CLI surface

hermes curator status             # last run, lifecycle stats, least-used skills
hermes curator run [--sync]       # trigger a pass now (LLM runs in background unless --sync)
hermes curator pause / resume     # toggle the schedule
hermes curator pin <skill>        # user opt-out: exempt from auto-transitions
hermes curator unpin <skill>
hermes curator restore <skill>    # move back from .archive/
/curator                          # same commands via slash

Validation

Test set Result
test_skill_usage.py (new, 29 tests) 29/29 pass
test_curator.py (new, 27 tests) 27/27 pass
Touched-file neighbors (346 tests across skills/manager/review) 346/346 pass
Full tests/hermes_cli/ + gateway sweep 2783/2783 pass
hermes curator status/pause/resume/--help all work end-to-end on isolated HERMES_HOME

Out of scope (follow-ups)

  • Recurring timer in the gateway (currently only boot-time — the interval_hours gate keeps it from over-running). A simple threading.Timer or APScheduler hook is a clean follow-up.
  • Telegram/Slack BotCommand menu sync — the CommandDef is in the registry so gateway /help will pick it up automatically, but platform-specific menus may need a refresh pass.

Refs #7816.

Adds the Curator — an auxiliary-model background task that periodically
reviews AGENT-CREATED skills and keeps the collection tidy: tracks usage,
transitions unused skills through active → stale → archived, and spawns
a forked AIAgent to consolidate overlaps and patch drift.

Default: enabled, inactivity-triggered (no cron daemon). Runs on CLI
startup and gateway boot when the last run is older than interval_hours
(default 24) AND the agent has been idle for min_idle_hours (default 2).

Invariants (all load-bearing):
- Never touches bundled or hub-installed skills (.bundled_manifest +
  .hub/lock.json double-filter)
- Never auto-deletes — archive only. Archives are recoverable
  via `hermes curator restore <skill>`
- Pinned skills bypass all auto-transitions
- Uses the aux client; never touches the main session's prompt cache

New files:
- tools/skill_usage.py — sidecar .usage.json telemetry, atomic writes,
  provenance filter
- agent/curator.py — orchestrator: config, idle gating, state-machine
  transitions (pure, no LLM), forked-agent review prompt
- hermes_cli/curator.py — `hermes curator {status,run,pause,resume,
  pin,unpin,restore}` subcommand
- tests/tools/test_skill_usage.py — 29 tests
- tests/agent/test_curator.py — 25 tests

Modified files (surgical patches):
- tools/skills_tool.py — bump view_count on successful skill_view
- tools/skill_manager_tool.py — bump patch_count on skill_manage
  patch/edit/write_file/remove_file; forget record on delete
- hermes_cli/config.py — add curator: section to DEFAULT_CONFIG
- hermes_cli/commands.py — add /curator CommandDef with subcommands
- hermes_cli/main.py — register `hermes curator` subparser via
  register_cli() from hermes_cli.curator
- cli.py — /curator slash-command dispatch + startup hook
- gateway/run.py — gateway-boot hook (mirrors CLI)

Validation:
- 54 new tests across skill_usage + curator, all passing in 3s
- 346 tests across all touched files' neighbors green
- 2783 tests across hermes_cli/ + gateway/test_run_progress_topics.py green
- CLI smoke: `hermes curator status/pause/resume` work end-to-end

Companion to PR #16026 (class-first skill review prompt) — together
they form a loop: the review prompt stops near-duplicate skill creation
at the source, and the curator prunes/consolidates what still accumulates.

Refs #7816.
Comment thread gateway/run.py Fixed
Comment thread hermes_cli/curator.py
print("curator: running review pass...")

def _on_summary(msg: str) -> None:
print(msg)
Comment thread tools/skill_usage.py
data[skill_name] = rec
save_usage(data)
except Exception as e:
logger.debug("skill_usage._mutate(%s) failed: %s", skill_name, e, exc_info=True)
The LLM review prompt mentioned bespoke `archive_skill` and `pin_skill`
tools that are not registered as model tools. Swap the prompt to rely
on the real surface:

  - skill_manage action=patch  — for patching and consolidation
  - terminal                   — to `mv` skill dirs into .archive/

Also drop `pin` from the model's decision list — pinning is a user
opt-out for `hermes curator pin <skill>`, not something the model
should do autonomously.

Decision list is now: keep / patch / consolidate / archive.

Tests updated: prompt-invariant test now asserts the existing tools
are referenced and that bespoke tool names do NOT appear. New test
prevents `pin` from being re-added as a model decision.
Previous invariants only gated the primary entry points
(apply_automatic_transitions, archive_skill, CLI pin). Several paths
were unprotected:

  - bump_view / bump_use / bump_patch / set_state / set_pinned wrote
    usage records unconditionally, which is confusing noise in
    .usage.json even though the review list filtered them out
  - restore_skill did not check whether a bundled skill now shadows
    the archived name
  - CLI unpin was asymmetric with CLI pin — it had no gate

Fixes:
  - _mutate() (the shared counter / state writer) now drops silently
    when the skill is not agent-created. .usage.json never gains a
    record for a bundled or hub-installed skill.
  - restore_skill() refuses to restore under a name that is now
    bundled or hub-installed (would shadow upstream).
  - CLI unpin gate matches CLI pin.

New tests:
  - 5 provenance-guard tests on skill_usage (one per mutator)
  - 1 end-to-end test that hammers every mutator at a bundled skill
    and a hub skill, asserts both are untouched on disk, and asserts
    the sidecar stays clean
  - 2 CLI tests proving pin/unpin refuse bundled skills symmetrically

64/64 tests passing (29 skill_usage + 27 curator + 8 new guards).
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent loop, run_agent.py, prompt builder comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery tool/skills Skills system (list, view, manage) labels Apr 26, 2026
Weekly is closer to how skill churn actually works — most agent-created
skills don't change multiple times per day, so a daily review is pure
cost without benefit. Bumping the default to 7 days reduces aux-model
spend while still catching drift and staleness on the timescales that
matter (30d stale, 90d archive).

Changes:
- DEFAULT_INTERVAL_HOURS: 24 -> 168 (7 days)
- config.yaml default: interval_hours: 24 -> 24 * 7
- CLI status line renders as '7d' when interval is a whole-day multiple
- Test `test_old_run_eligible` decoupled from the exact default: it now
  uses 2 * get_interval_hours() so future tweaks don't break it
Long-running gateways need the curator to fire on cadence without
restarts. Piggy-back on the existing cron ticker thread (which already
runs image/document cache cleanup every hour on the same pattern)
instead of spawning a dedicated timer thread.

- New CURATOR_EVERY = 60 ticks (poll hourly at default 60s interval).
  The inner config.interval_hours gate controls the real cadence, so
  60 of these 60 hourly pokes are cheap no-ops and one runs the review.
- Removed the boot-time call added in the prior commit — the ticker
  covers boot + every hour thereafter. Avoids double-running.

Handles the weekly-default-on-24/7-gateway gap flagged in review.
Comment thread gateway/run.py
from agent.curator import maybe_run_curator
maybe_run_curator(
idle_for_seconds=float("inf"),
on_summary=lambda msg: logger.info("curator: %s", msg),
@teknium1
Copy link
Copy Markdown
Contributor Author

Opened a higher-level RFC for design feedback: #16077. Code-level concerns welcome here on the PR; broader design questions (defaults, scope, enabled-by-default trade-off, etc.) please leave on the issue.

@teknium1
Copy link
Copy Markdown
Contributor Author

Superseded by #17277 which salvaged all 5 commits from this branch onto current main (your per-commit authorship preserved via rebase-merge) plus a follow-up commit with the umbrella-first prompt, parent-config inheritance fix, and unbounded iteration ceiling validated via 3 live runs on real skill collections. Closing this branch; the feature is now on main.

@teknium1 teknium1 closed this Apr 29, 2026
teknium1 added a commit that referenced this pull request Apr 29, 2026
…17563)

Skill catalog pages (bundled/optional) were drowning out real user-guide
and reference docs in search results. There are ~3100 of them and they
match on almost every generic term.

- Add `ignoreFiles` regexes to docusaurus-search-local for
  `user-guide/skills/bundled/` and `user-guide/skills/optional/`.
  The two human-written catalog indexes (`reference/skills-catalog`,
  `reference/optional-skills-catalog`) remain indexed.
- Add a new feature page `user-guide/features/curator.md` covering the
  curator subsystem merged in #16049 and refined in #17307 (per-run
  reports): how it runs, config, CLI (`hermes curator status/run/pin/
  restore/...`), `.usage.json` telemetry, archival semantics, and
  recovery. Slotted into the Core features sidebar next to Skills.

Search index size dropped from 5822 docs to 2704 in the main section;
`user-guide/features/curator` is indexed.
teknium1 added a commit that referenced this pull request Apr 30, 2026
… fire import (#17927)

Three fixes bundled for curator reliability on existing installs and
broken/partial installs:

1. run_agent.py: defer `import fire` into the __main__ block. `fire` is
   only used by `fire.Fire(main)` when running run_agent.py directly as
   a CLI — it is NOT needed for library usage. Importing it at module
   top made `from run_agent import AIAgent` from a daemon thread (e.g.
   the curator's forked review agent) crash with ModuleNotFoundError
   on broken/partial installs where `fire` isn't present.

2. hermes_cli/config.py: add version 22 → 23 migration that writes the
   `curator` + `auxiliary.curator` sections to config.yaml with their
   defaults, only filling keys the user hasn't overridden. Existing
   configs from before PR #16049 / the April 2026 `auxiliary.curator`
   unification had neither section on disk, so users couldn't see or
   edit the settings in their config.yaml (runtime deep-merge papered
   over it at read time, but the file never reflected reality).

3. hermes_cli/config.py: `ensure_hermes_home()` now pre-creates
   `~/.hermes/logs/curator/` alongside cron/sessions/logs/memories on
   every CLI launch. Managed-mode (NixOS) variant mkdir's it
   defensively after the activation-script existence checks, since the
   activation script may not know about this subpath.

4. agent/curator.py: `_reports_root()` mkdir's the dir at call time as
   belt-and-suspenders for entry paths that bypass both
   ensure_hermes_home() and the v23 migration (gateway-only installs,
   bare library use).

E2E validated in isolated HERMES_HOME: fresh install gets full defaults
seeded; partial-override config keeps user's `enabled: false` and
custom `interval_hours` while filling the missing keys; re-running the
migration is a no-op.
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
…ousResearch#17563)

Skill catalog pages (bundled/optional) were drowning out real user-guide
and reference docs in search results. There are ~3100 of them and they
match on almost every generic term.

- Add `ignoreFiles` regexes to docusaurus-search-local for
  `user-guide/skills/bundled/` and `user-guide/skills/optional/`.
  The two human-written catalog indexes (`reference/skills-catalog`,
  `reference/optional-skills-catalog`) remain indexed.
- Add a new feature page `user-guide/features/curator.md` covering the
  curator subsystem merged in NousResearch#16049 and refined in NousResearch#17307 (per-run
  reports): how it runs, config, CLI (`hermes curator status/run/pin/
  restore/...`), `.usage.json` telemetry, archival semantics, and
  recovery. Slotted into the Core features sidebar next to Skills.

Search index size dropped from 5822 docs to 2704 in the main section;
`user-guide/features/curator` is indexed.
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
… fire import (NousResearch#17927)

Three fixes bundled for curator reliability on existing installs and
broken/partial installs:

1. run_agent.py: defer `import fire` into the __main__ block. `fire` is
   only used by `fire.Fire(main)` when running run_agent.py directly as
   a CLI — it is NOT needed for library usage. Importing it at module
   top made `from run_agent import AIAgent` from a daemon thread (e.g.
   the curator's forked review agent) crash with ModuleNotFoundError
   on broken/partial installs where `fire` isn't present.

2. hermes_cli/config.py: add version 22 → 23 migration that writes the
   `curator` + `auxiliary.curator` sections to config.yaml with their
   defaults, only filling keys the user hasn't overridden. Existing
   configs from before PR NousResearch#16049 / the April 2026 `auxiliary.curator`
   unification had neither section on disk, so users couldn't see or
   edit the settings in their config.yaml (runtime deep-merge papered
   over it at read time, but the file never reflected reality).

3. hermes_cli/config.py: `ensure_hermes_home()` now pre-creates
   `~/.hermes/logs/curator/` alongside cron/sessions/logs/memories on
   every CLI launch. Managed-mode (NixOS) variant mkdir's it
   defensively after the activation-script existence checks, since the
   activation script may not know about this subpath.

4. agent/curator.py: `_reports_root()` mkdir's the dir at call time as
   belt-and-suspenders for entry paths that bypass both
   ensure_hermes_home() and the v23 migration (gateway-only installs,
   bare library use).

E2E validated in isolated HERMES_HOME: fresh install gets full defaults
seeded; partial-override config keeps user's `enabled: false` and
custom `interval_hours` while filling the missing keys; re-running the
migration is a no-op.
nickdlkk pushed a commit to nickdlkk/hermes-agent that referenced this pull request May 11, 2026
…ousResearch#17563)

Skill catalog pages (bundled/optional) were drowning out real user-guide
and reference docs in search results. There are ~3100 of them and they
match on almost every generic term.

- Add `ignoreFiles` regexes to docusaurus-search-local for
  `user-guide/skills/bundled/` and `user-guide/skills/optional/`.
  The two human-written catalog indexes (`reference/skills-catalog`,
  `reference/optional-skills-catalog`) remain indexed.
- Add a new feature page `user-guide/features/curator.md` covering the
  curator subsystem merged in NousResearch#16049 and refined in NousResearch#17307 (per-run
  reports): how it runs, config, CLI (`hermes curator status/run/pin/
  restore/...`), `.usage.json` telemetry, archival semantics, and
  recovery. Slotted into the Core features sidebar next to Skills.

Search index size dropped from 5822 docs to 2704 in the main section;
`user-guide/features/curator` is indexed.
nickdlkk pushed a commit to nickdlkk/hermes-agent that referenced this pull request May 11, 2026
… fire import (NousResearch#17927)

Three fixes bundled for curator reliability on existing installs and
broken/partial installs:

1. run_agent.py: defer `import fire` into the __main__ block. `fire` is
   only used by `fire.Fire(main)` when running run_agent.py directly as
   a CLI — it is NOT needed for library usage. Importing it at module
   top made `from run_agent import AIAgent` from a daemon thread (e.g.
   the curator's forked review agent) crash with ModuleNotFoundError
   on broken/partial installs where `fire` isn't present.

2. hermes_cli/config.py: add version 22 → 23 migration that writes the
   `curator` + `auxiliary.curator` sections to config.yaml with their
   defaults, only filling keys the user hasn't overridden. Existing
   configs from before PR NousResearch#16049 / the April 2026 `auxiliary.curator`
   unification had neither section on disk, so users couldn't see or
   edit the settings in their config.yaml (runtime deep-merge papered
   over it at read time, but the file never reflected reality).

3. hermes_cli/config.py: `ensure_hermes_home()` now pre-creates
   `~/.hermes/logs/curator/` alongside cron/sessions/logs/memories on
   every CLI launch. Managed-mode (NixOS) variant mkdir's it
   defensively after the activation-script existence checks, since the
   activation script may not know about this subpath.

4. agent/curator.py: `_reports_root()` mkdir's the dir at call time as
   belt-and-suspenders for entry paths that bypass both
   ensure_hermes_home() and the v23 migration (gateway-only installs,
   bare library use).

E2E validated in isolated HERMES_HOME: fresh install gets full defaults
seeded; partial-override config keeps user's `enabled: false` and
custom `interval_hours` while filling the missing keys; re-running the
migration is a no-op.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants