fix(curator): seed defaults on update, create logs/curator dir, defer 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.
This commit is contained in:
Teknium
2026-04-30 04:52:28 -07:00
committed by GitHub
parent d1d0ef6dbd
commit e8e5985ce6
3 changed files with 109 additions and 4 deletions

View File

@@ -350,7 +350,7 @@ def ensure_hermes_home():
else:
home.mkdir(parents=True, exist_ok=True)
_secure_dir(home)
for subdir in ("cron", "sessions", "logs", "memories"):
for subdir in ("cron", "sessions", "logs", "logs/curator", "memories"):
d = home / subdir
d.mkdir(parents=True, exist_ok=True)
_secure_dir(d)
@@ -371,6 +371,10 @@ def _ensure_hermes_home_managed(home: Path):
f"{d} does not exist. "
"Run 'sudo nixos-rebuild switch' first."
)
# Curator reports dir is a sub-path of logs/; create it if missing.
# In managed mode the activation script may not know about this subdir,
# so we mkdir it ourselves (it's inside an already-secured logs/ dir).
(home / "logs" / "curator").mkdir(parents=True, exist_ok=True)
# Inside umask(0o007) scope — SOUL.md will be created as 0660
_ensure_default_soul_md(home)
@@ -1201,7 +1205,7 @@ DEFAULT_CONFIG = {
},
# Config schema version - bump this when adding new required fields
"_config_version": 22,
"_config_version": 23,
}
# =============================================================================
@@ -3274,6 +3278,90 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
"Use `hermes plugins enable <name>` to activate."
)
# ── Version 22 → 23: seed curator defaults + create logs/curator/ ──
# The curator (background skill maintenance) was added in PR #16049, but
# existing configs from before that PR (or before the April 2026
# unification under `auxiliary.curator`) never wrote the curator section
# to disk. The runtime deep-merge in `load_config()` fills defaults at
# read time, so the curator *functions*; but users can't see/edit the
# settings in their `config.yaml`, and `hermes curator status` has no
# stable logs dir to point at until the first run mkdir's it.
#
# This migration:
# 1. Writes the `curator` top-level section to config.yaml (enabled,
# interval_hours, min_idle_hours, stale_after_days, archive_after_days)
# — only keys the user hasn't already overridden.
# 2. Writes the `auxiliary.curator` aux-task slot (provider, model,
# base_url, api_key, timeout, extra_body) — canonical slot for
# routing the curator fork to a cheaper aux model.
# 3. Creates `~/.hermes/logs/curator/` if missing (belt-and-suspenders
# on top of ensure_hermes_home() — old profiles that predate this
# migration still benefit).
if current_ver < 23:
try:
curator_dir = get_hermes_home() / "logs" / "curator"
curator_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
results["warnings"].append(f"Could not create {curator_dir}: {e}")
config = read_raw_config()
touched = False
# (1) Top-level curator section — only add missing keys
_curator_defaults = DEFAULT_CONFIG.get("curator", {})
raw_curator = config.get("curator")
if not isinstance(raw_curator, dict):
raw_curator = {}
added_curator: List[str] = []
for k, v in _curator_defaults.items():
if k not in raw_curator:
raw_curator[k] = copy.deepcopy(v)
added_curator.append(k)
if added_curator:
config["curator"] = raw_curator
touched = True
# (2) auxiliary.curator task slot
_aux_curator_defaults = (
DEFAULT_CONFIG.get("auxiliary", {}).get("curator", {})
)
raw_aux = config.get("auxiliary")
if not isinstance(raw_aux, dict):
raw_aux = {}
raw_aux_curator = raw_aux.get("curator")
if not isinstance(raw_aux_curator, dict):
raw_aux_curator = {}
added_aux: List[str] = []
for k, v in _aux_curator_defaults.items():
if k not in raw_aux_curator:
raw_aux_curator[k] = copy.deepcopy(v)
added_aux.append(k)
if added_aux:
raw_aux["curator"] = raw_aux_curator
config["auxiliary"] = raw_aux
touched = True
if touched:
save_config(config)
if added_curator:
results["config_added"].append(
f"curator ({len(added_curator)} default key(s))"
)
if not quiet:
print(
" ✓ Seeded curator defaults in config.yaml: "
f"{', '.join(added_curator)}"
)
if added_aux:
results["config_added"].append(
f"auxiliary.curator ({len(added_aux)} default key(s))"
)
if not quiet:
print(
" ✓ Seeded auxiliary.curator defaults in config.yaml: "
f"{', '.join(added_aux)}"
)
if current_ver < latest_ver and not quiet:
print(f"Config version: {current_ver}{latest_ver}")