[grade=A] add SKILL_EVOLUTION_SKILLS_DIR env var for per-Hermes-profile skills trees

Forked from Carlo1911/skill-evolution main. Adds env-var override so a single
installed pipeline can target per-profile skills trees (default ~/.hermes/skills,
krystie ~/.hermes/profiles/krystie/skills, sami-story ~/.hermes/profiles/sami-story/skills)
without monkeypatching HOME or the SKILLS_DIR module constant.

- scripts/skill_index.py: add resolve_skills_dir() + SKILLS_DIR_ENV_VAR, wire into
  scan_skills() with explicit-arg > env-var > default resolution order. Read at
  call time (matches fetch_sessions.get_state_db_path() convention).
- tests/test_skill_index.py: 2 new tests covering nonblank env var and blank
  fallback (the latter monkeypatches SKILLS_DIR + invokes scan_skills() with no
  args so the test actually exercises the fallback expression, not just the
  explicit arg path).
- 49/49 conformance tests pass.
- Codex grade: A (verified 2026-08-05).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Krystie
2026-08-05 15:09:59 -07:00
parent 651553f4f5
commit 52b77bfcad
2 changed files with 59 additions and 3 deletions
+24 -3
View File
@@ -15,6 +15,20 @@ from typing import Optional
SKILLS_DIR = os.path.expanduser("~/.hermes/skills") SKILLS_DIR = os.path.expanduser("~/.hermes/skills")
SKILLS_DIR_ENV_VAR = "SKILL_EVOLUTION_SKILLS_DIR"
def resolve_skills_dir() -> str:
"""Resolve the skills directory, honouring SKILL_EVOLUTION_SKILLS_DIR.
Mirrors fetch_sessions.get_state_db_path()'s read-at-call-time shape: the env var
is read every call (not frozen at import) so a single installed pipeline can target
different skill trees per run (e.g. per-Hermes-profile layouts like
~/.hermes/profiles/<name>/skills/) without monkeypatching HOME. Falls back to the
default ~/.hermes/skills when unset or blank, so existing callers see no behaviour
change.
"""
return os.environ.get(SKILLS_DIR_ENV_VAR, "").strip() or SKILLS_DIR
def parse_name_description_frontmatter(content: str) -> dict: def parse_name_description_frontmatter(content: str) -> dict:
@@ -62,9 +76,16 @@ def build_skill_record(skill_md: Path, category: str) -> Optional[dict]:
} }
def scan_skills(skills_dir: str = SKILLS_DIR) -> list: def scan_skills(skills_dir: Optional[str] = None) -> list:
"""Scan skills directory and return structured index.""" """Scan skills directory and return structured index.
base = Path(skills_dir)
Resolution order: explicit `skills_dir` arg > SKILL_EVOLUTION_SKILLS_DIR env var >
the default ~/.hermes/skills. The env var is read at call time (same as
fetch_sessions.get_state_db_path) so a single installed pipeline can target
different skill trees per run -- e.g. per-Hermes-profile layouts like
~/.hermes/profiles/<name>/skills/ -- without monkeypatching HOME.
"""
base = Path(skills_dir if skills_dir is not None else resolve_skills_dir())
if not base.exists(): if not base.exists():
return [] return []
+35
View File
@@ -59,3 +59,38 @@ def test_scan_skills_dot_filter_keeps_names_unambiguous(tmp_path):
def test_scan_skills_returns_empty_for_missing_dir(tmp_path): def test_scan_skills_returns_empty_for_missing_dir(tmp_path):
assert skill_index.scan_skills(str(tmp_path / "nope")) == [] assert skill_index.scan_skills(str(tmp_path / "nope")) == []
def test_scan_skills_honors_skills_dir_env_var(tmp_path, monkeypatch):
"""SKILL_EVOLUTION_SKILLS_DIR redirects the scan at call time.
Without this, a single installed pipeline can only target one skills tree
(the hardcoded ~/.hermes/skills default). Per-Hermes-profile layouts
(~/.hermes/profiles/<name>/skills/) need to override it per run without
monkeypatching HOME or the module-level SKILLS_DIR constant.
"""
live = tmp_path / "live"
other = tmp_path / "other"
_write_skill(live, "general-skills", "live-skill")
_write_skill(other, "general-skills", "other-skill")
monkeypatch.setenv(skill_index.SKILLS_DIR_ENV_VAR, str(other))
found = skill_index.scan_skills()
assert {s["name"] for s in found} == {"other-skill"}
def test_scan_skills_env_var_blank_falls_back_to_default(tmp_path, monkeypatch):
"""A blank/whitespace env var disables the override (matches fetch_sessions'
get_state_db_path()/get_state_file() convention -- the same read-at-call-time
fallback behaviour for missing/blank env vars)."""
_write_skill(tmp_path, "general-skills", "fallback-skill")
# Monkeypatch the default to a path that is ONLY reachable via the env-var
# fallback -- if the blank env var were ever silently winning the override,
# the test would still pass; we want to confirm the default wins.
monkeypatch.setattr(skill_index, "SKILLS_DIR", str(tmp_path))
monkeypatch.setenv(skill_index.SKILLS_DIR_ENV_VAR, " ")
found = skill_index.scan_skills()
assert {s["name"] for s in found} == {"fallback-skill"}