feat(cron): cron.provider config + plugins/cron discovery + resolver

Phase 2 of the pluggable cron-scheduler refactor. Still no call-site changes;
this wires up provider SELECTION with a hard safety net.

Task 2.1: cron.provider config key (hermes_cli/config.py), empty = built-in.
  Additive key — deep-merge picks it up into existing configs with no version
  bump (verified: load_config() yields the key on a pre-existing config.yaml).
Task 2.2: plugins/cron/__init__.py — discovery machinery cloned near-verbatim
  from plugins/memory/__init__.py, retargeted at CronScheduler /
  register_cron_scheduler. Bundled (plugins/cron/<name>/) + user
  (/plugins/<name>/) dirs, bundled wins collisions. The built-in is
  NOT discovered here — it's core, so the fallback can't be removed.
Task 2.3: resolve_cron_scheduler() in cron/scheduler_provider.py — reads
  cron.provider and ALWAYS degrades to built-in (missing / unavailable / load
  error / typo all fall back with a warning). cron can never be left without a
  trigger.

Deviation from plan: the plan's resolver snippet used cfg_get("cron.provider")
(dotted-string form). The real cfg_get signature is cfg_get(cfg, *keys,
default=) — corrected to cfg_get(load_config(), "cron", "provider", default=""),
matching plugins/memory/__init__.py:349. Tests monkeypatch load_config (not
cfg_get) so the real traversal runs.

Tests: default key empty, discovery returns list, unknown load returns None,
and the four resolver paths (empty→builtin, no-section→builtin,
unknown→builtin, unavailable→builtin, available→used). Full tests/cron/: 453
passed; config suite green (additive key, no migration break).
This commit is contained in:
Ben 2026-06-18 14:09:36 +10:00
parent e6ff41ca95
commit ae8fa11097
4 changed files with 495 additions and 0 deletions

View file

@ -2124,6 +2124,14 @@ DEFAULT_CONFIG = {
},
"cron": {
# Active cron SCHEDULER provider (Axis B — the trigger that decides
# WHEN a due job fires). Empty string = the built-in in-process 60s
# ticker (default). Name an installed provider (plugins/cron/<name>/ or
# $HERMES_HOME/plugins/<name>/) to relocate the trigger — e.g. "chronos",
# the NAS-mediated managed-cron provider for scale-to-zero deployments.
# An unknown or unavailable provider falls back to the built-in, so cron
# never loses its trigger.
"provider": "",
# Wrap delivered cron responses with a header (task name) and footer
# ("The agent cannot see this message"). Set to false for clean output.
"wrap_response": True,