[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:
+24
-3
@@ -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 []
|
||||||
|
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user