volt: spark/hermes_profile_health.py grades a named profile (Q81) -- --profile/NAME/HERMES_HOME replaces the module constant, is_dir() replaces exists() on the four state dirs, config.yaml is parsed by yaml.safe_load and graded non-empty/non-mapping, the .hermes_profile branch is deleted (0 of 27 profile homes carry it, 0 occurrences in the Hermes source tree), and rc 2 is reserved for a target that cannot be graded

This commit is contained in:
ledger committed 2026-10-26 22:20:06 +00:00
1 parent d99b7d36fb
commit bc8ccb1bd2
1 file changed
+138 -34
+138 -34
View File
@@ -1,49 +1,153 @@
#!/usr/bin/env python3
"""Hermes Profile Health Checker - verifies spark profile structure.
Exit 0 = healthy, 1 = issues found, 2 = critical missing
"""Hermes profile health checker - grades ONE profile home.
Exit codes (troupe contract): 0 = clean, 1 = ran and named faults in the profile,
2 = could not grade (no profile named, profile home absent or not a directory, or an
input could not be read). A 2 never prints a verdict.
Profile home resolution, first match wins:
1. --profile PATH (absolute, contains os.sep, or starts with '~' or '.')
2. --profile NAME resolved under --profiles-root
3. $HERMES_HOME Hermes' own variable for the active profile home
--profiles-root defaults to $HERMES_PROFILES_DIR, else ~/.hermes/profiles.
Graded: config.yaml must exist, be non-empty, and yaml.safe_load to a non-empty
mapping; skills/, plugins/, cron/, memories/ must be directories (a regular file of
that name is a fault, not a pass).
Not graded, and not claimed: permissions, ownership, symlinked profile homes, whether
individual skills/plugins load, and config.yaml's schema (any non-empty mapping
passes; this tool does not know Hermes' key set).
The `.hermes_profile` marker is neither required nor reported: no profile home on
this host carries one (0 of 27) and nothing in the Hermes source tree writes it.
"""
import argparse
import os
import sys
from pathlib import Path
PROFILE_DIR = Path("/root/.hermes/profiles/spark")
EXPECTED_DIRS = ["skills", "plugins", "cron", "memories"]
try:
import yaml
except ImportError: # graded as ungradable, never as a pass
yaml = None
def check_profile():
issues = []
# Check profile dir exists
if not PROFILE_DIR.exists():
print("CRITICAL: Profile directory missing")
EXPECTED_DIRS = ("skills", "plugins", "cron", "memories")
DEFAULT_PROFILES_ROOT = "~/.hermes/profiles"
class Ungradable(Exception):
"""Raised when no verdict can be earned; main() turns it into rc 2."""
def resolve_home(profile, profiles_root):
"""Map a --profile value (path or name) or $HERMES_HOME to a profile home path."""
if profile:
if (
os.sep in profile
or (os.altsep and os.altsep in profile)
or profile.startswith("~")
or profile.startswith(".")
or Path(profile).is_absolute()
):
return Path(profile).expanduser()
root = profiles_root or os.environ.get("HERMES_PROFILES_DIR") or DEFAULT_PROFILES_ROOT
return Path(root).expanduser() / profile
env_home = os.environ.get("HERMES_HOME", "").strip()
if env_home:
return Path(env_home).expanduser()
raise Ungradable("no profile named: pass --profile PATH|NAME or set HERMES_HOME")
def grade_config(home):
"""Return (ok_lines, issues) for <home>/config.yaml."""
cfg = home / "config.yaml"
if not cfg.exists():
return [], ["config.yaml missing"]
if not cfg.is_file():
return [], ["config.yaml exists but is not a regular file"]
try:
raw = cfg.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
raise Ungradable(f"config.yaml unreadable: {exc}") from exc
if not raw.strip():
return [], ["config.yaml is empty"]
if yaml is None:
raise Ungradable("PyYAML not importable, cannot grade config.yaml")
try:
data = yaml.safe_load(raw)
except yaml.YAMLError as exc:
text = str(exc).strip()
first = text.splitlines()[0] if text else exc.__class__.__name__
return [], [f"config.yaml is not valid YAML: {first}"]
if data is None:
return [], ["config.yaml parses to no document"]
if not isinstance(data, dict):
return [], [f"config.yaml is a {type(data).__name__}, not a mapping"]
if not data:
return [], ["config.yaml is an empty mapping"]
return [f"{cfg} ({len(data)} key(s), parsed)"], []
def check_profile(home):
"""Grade one profile home. Returns the process exit code."""
home = Path(home)
if not home.exists():
print(f"ungradable: profile home does not exist: {home}", file=sys.stderr)
return 2
# Check config.yaml
config = PROFILE_DIR / "config.yaml"
if not config.exists():
issues.append("config.yaml missing")
else:
print(f"OK: {config}")
# Check expected directories
if not home.is_dir():
print(f"ungradable: profile home is not a directory: {home}", file=sys.stderr)
return 2
oks, issues = grade_config(home)
for dirname in EXPECTED_DIRS:
dirpath = PROFILE_DIR / dirname
if not dirpath.exists():
issues.append(f"{dirname}/ missing")
dirpath = home / dirname
if dirpath.is_dir():
oks.append(f"{dirpath}/")
elif dirpath.exists():
issues.append(f"{dirname} exists but is not a directory")
else:
print(f"OK: {dirpath}/")
# Check magic file
magic = PROFILE_DIR / ".hermes_profile"
if magic.exists():
print(f"OK: {magic}")
issues.append(f"{dirname}/ missing")
for line in oks:
print(f"OK: {line}")
if issues:
print("\nIssues:")
for i in issues:
print(f" - {i}")
for issue in issues:
print(f" - {issue}")
print(f"\nProfile is unhealthy: {len(issues)} issue(s)")
return 1
print("\nProfile is healthy")
print(f"\nProfile is healthy: {home}")
return 0
def main(argv=None):
parser = argparse.ArgumentParser(
description="Grade one Hermes profile home (0 healthy / 1 faults / 2 ungradable).",
)
parser.add_argument(
"--profile",
help="profile home PATH (absolute, or containing '/', '~', '.') or a profile NAME "
"resolved under --profiles-root; defaults to $HERMES_HOME",
)
parser.add_argument(
"--profiles-root",
default=None,
help="root for NAME resolution (default: $HERMES_PROFILES_DIR or "
+ DEFAULT_PROFILES_ROOT + ")",
)
args = parser.parse_args(argv)
try:
home = resolve_home(args.profile, args.profiles_root)
except Ungradable as exc:
print(f"ungradable: {exc}", file=sys.stderr)
return 2
try:
return check_profile(home)
except Ungradable as exc:
print(f"ungradable: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(check_profile())
sys.exit(main())