From 52b77bfcadde49f1fcb6f2b6e1be026eede78b3b Mon Sep 17 00:00:00 2001 From: Krystie Date: Wed, 5 Aug 2026 15:09:59 -0700 Subject: [PATCH] [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 --- scripts/skill_index.py | 27 ++++++++++++++++++++++++--- tests/test_skill_index.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/scripts/skill_index.py b/scripts/skill_index.py index d0f2e24..d2f8a12 100644 --- a/scripts/skill_index.py +++ b/scripts/skill_index.py @@ -15,6 +15,20 @@ from typing import Optional 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//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: @@ -62,9 +76,16 @@ def build_skill_record(skill_md: Path, category: str) -> Optional[dict]: } -def scan_skills(skills_dir: str = SKILLS_DIR) -> list: - """Scan skills directory and return structured index.""" - base = Path(skills_dir) +def scan_skills(skills_dir: Optional[str] = None) -> list: + """Scan skills directory and return structured index. + + 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//skills/ -- without monkeypatching HOME. + """ + base = Path(skills_dir if skills_dir is not None else resolve_skills_dir()) if not base.exists(): return [] diff --git a/tests/test_skill_index.py b/tests/test_skill_index.py index 7ea8af5..818f26f 100644 --- a/tests/test_skill_index.py +++ b/tests/test_skill_index.py @@ -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): 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//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"}