feat(curator): make skill consolidation opt-in (prune stays default-on) (#47840)

The curator now defaults to prune-only: the deterministic inactivity pass
(mark stale / archive long-unused skills) still runs whenever the curator is
enabled, but the opinionated LLM umbrella-building consolidation fork is OFF
by default.

- agent/curator.py: add DEFAULT_CONSOLIDATE=False + get_consolidate(); gate
  the forked aux-model review in run_curator_review behind it (new consolidate
  param, None=read config). When off, the LLM pass is skipped entirely (no
  aux-model cost); the run is still recorded and reported.
- config.py: add curator.consolidate (default false); v29->v30 migration seeds
  the key for existing installs without clobbering a user-set value.
- hermes_cli/curator.py: 'hermes curator run --consolidate' override; status
  shows consolidate state; prune-only notice on run.
- docs + tests.
This commit is contained in:
Teknium 2026-06-17 05:20:32 -07:00 committed by GitHub
parent e48803daec
commit 7bbffceb9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 217 additions and 11 deletions

View file

@ -1895,6 +1895,14 @@ DEFAULT_CONFIG = {
# Archive a skill (move to skills/.archive/) after this many days
# without use. Archived skills are recoverable — no auto-deletion.
"archive_after_days": 90,
# Run the LLM consolidation (umbrella-building) pass. OFF by default.
# When off, a curator run does ONLY the deterministic inactivity prune
# (mark stale / archive long-unused skills) and skips the forked
# aux-model review entirely — no umbrella-building, no aux-model cost.
# Set to true to opt back into merging overlapping skills into
# class-level umbrellas. `hermes curator run --consolidate` overrides
# this for a single invocation.
"consolidate": False,
# Also prune (archive) bundled built-in skills after the inactivity
# period, not just agent-created ones. ON by default. Built-ins are
# normally restored on every `hermes update`, so pruning them only
@ -2569,7 +2577,7 @@ DEFAULT_CONFIG = {
# Config schema version - bump this when adding new required fields
"_config_version": 29,
"_config_version": 30,
}
# =============================================================================
@ -4858,6 +4866,29 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
if not quiet:
print(" ✓ Renamed write_mode → write_approval (boolean gate)")
# ── Version 29 → 30: seed curator.consolidate (default false) ──
# Consolidation (the LLM umbrella-building fork) is now an opt-in toggle,
# OFF by default. The deterministic inactivity prune still runs whenever
# the curator is enabled; only the opinionated, aux-model-cost LLM pass is
# gated. The runtime deep-merge already supplies the default, but we seed
# the key so it's visible/editable in config.yaml. Existing installs that
# WANT the old always-consolidate behavior must set it to true explicitly.
# Only add the key when a curator section exists and lacks it — never
# clobber a value the user already set.
if current_ver < 30:
config = read_raw_config()
raw_curator = config.get("curator")
if isinstance(raw_curator, dict) and "consolidate" not in raw_curator:
raw_curator["consolidate"] = False
config["curator"] = raw_curator
save_config(config)
results["config_added"].append("curator.consolidate=false")
if not quiet:
print(
" ✓ Seeded curator.consolidate: false "
"(LLM consolidation is now opt-in; pruning stays on)"
)
# ── Post-migration: disable exfiltration-shaped MCP stdio entries ──
# Users can hand-edit mcp_servers, and older installs may already contain a
# malicious entry. Preserve the stanza for auditability but mark it

View file

@ -77,6 +77,10 @@ def _cmd_status(args) -> int:
print(f" interval: every {_interval_label}")
print(f" stale after: {curator.get_stale_after_days()}d unused")
print(f" archive after: {curator.get_archive_after_days()}d unused")
print(
f" consolidate: {'on' if curator.get_consolidate() else 'off'}"
f"{'' if curator.get_consolidate() else ' (prune-only; LLM merge pass opt-in)'}"
)
rows = skill_usage.agent_created_report()
if not rows:
@ -174,10 +178,20 @@ def _cmd_run(args) -> int:
dry = bool(getattr(args, "dry_run", False))
background = bool(getattr(args, "background", False))
synchronous = bool(getattr(args, "synchronous", False)) or not background
# --consolidate forces the LLM umbrella-building pass on for this run,
# overriding the config default (off). When the flag is absent, pass None
# so run_curator_review reads curator.consolidate from config.
consolidate = True if bool(getattr(args, "consolidate", False)) else None
if dry:
print("curator: running DRY-RUN (report only, no mutations)...")
else:
print("curator: running review pass...")
if consolidate is None and not curator.get_consolidate():
print(
"curator: consolidation is off — running prune-only "
"(deterministic stale/archive). Pass --consolidate or set "
"`curator.consolidate: true` to enable the LLM merge pass."
)
def _on_summary(msg: str) -> None:
print(msg)
@ -186,6 +200,7 @@ def _cmd_run(args) -> int:
on_summary=_on_summary,
synchronous=synchronous,
dry_run=dry,
consolidate=consolidate,
)
auto = result.get("auto_transitions", {})
if auto:
@ -503,6 +518,12 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
help="Report only — no state changes, no archives, no consolidation "
"(use this to preview what curator would do)",
)
p_run.add_argument(
"--consolidate", dest="consolidate", action="store_true",
help="Force the LLM umbrella-building consolidation pass on for this "
"run, overriding the config default (off). Without this flag the "
"run is prune-only unless `curator.consolidate: true` is set.",
)
p_run.set_defaults(func=_cmd_run)
p_pause = subs.add_parser("pause", help="Pause the curator until resumed")