refactor(agent/subdirectory_hints,skill_preprocessing): fold hint-file lookup, compact exception ladders and docs

This commit is contained in:
Teknium
2026-09-02 19:13:55 -07:00
parent 82aa392f1c
commit 042aea505d
2 changed files with 97 additions and 190 deletions
+25 -64
View File
@@ -1,4 +1,5 @@
"""Shared SKILL.md preprocessing helpers."""
"""Shared SKILL.md preprocessing helpers: ``${HERMES_*}`` template tokens and
inline ``!`cmd``` shell expansion."""
import logging
import re
@@ -9,15 +10,11 @@ from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags
logger = logging.getLogger(__name__)
# Matches ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID} tokens in SKILL.md.
# Tokens that don't resolve (e.g. ${HERMES_SESSION_ID} with no session) are
# left as-is so the user can debug them.
# ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID} tokens. Unresolvable ones (e.g. no
# session) are left as-is so the author can spot them.
_SKILL_TEMPLATE_RE = re.compile(r"\$\{(HERMES_SKILL_DIR|HERMES_SESSION_ID)\}")
# Matches inline shell snippets like: !`date +%Y-%m-%d`
# Non-greedy, single-line only -- no newlines inside the backticks.
# Inline shell snippets like !`date +%Y-%m-%d` — single-line only.
_INLINE_SHELL_RE = re.compile(r"!`([^`\n]+)`")
# Cap inline-shell output so a runaway command can't blow out the context.
_INLINE_SHELL_MAX_OUTPUT = 4000
@@ -27,8 +24,7 @@ def load_skills_config() -> dict:
try:
from hermes_cli.config import load_config_readonly
cfg = load_config_readonly() or {}
skills_cfg = cfg.get("skills")
skills_cfg = (load_config_readonly() or {}).get("skills")
if isinstance(skills_cfg, dict):
return skills_cfg
except Exception:
@@ -36,38 +32,21 @@ def load_skills_config() -> dict:
return {}
def substitute_template_vars(
content: str,
skill_dir: Path | None,
session_id: str | None,
) -> str:
"""Replace ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID} in skill content.
Only substitutes tokens for which a concrete value is available --
unresolved tokens are left in place so the author can spot them.
"""
def substitute_template_vars(content: str, skill_dir: Path | None, session_id: str | None) -> str:
"""Replace ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID}; tokens without a value stay in place."""
if not content:
return content
skill_dir_str = str(skill_dir) if skill_dir else None
def _replace(match: re.Match) -> str:
token = match.group(1)
if token == "HERMES_SKILL_DIR" and skill_dir_str:
return skill_dir_str
if token == "HERMES_SESSION_ID" and session_id:
return str(session_id)
return match.group(0)
return _SKILL_TEMPLATE_RE.sub(_replace, content)
values = {
"HERMES_SKILL_DIR": str(skill_dir) if skill_dir else None,
"HERMES_SESSION_ID": str(session_id) if session_id else None,
}
return _SKILL_TEMPLATE_RE.sub(lambda m: values[m.group(1)] or m.group(0), content)
def run_inline_shell(command: str, cwd: Path | None, timeout: int) -> str:
"""Execute a single inline-shell snippet and return its stdout (trimmed).
Failures return a short ``[inline-shell error: ...]`` marker instead of
raising, so one bad snippet can't wreck the whole skill message.
"""
"""Run one inline-shell snippet and return its stdout (trimmed; stderr when
stdout is empty). Failures return an ``[inline-shell ...]`` marker instead
of raising, so one bad snippet can't wreck the whole skill message."""
_popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {}
try:
completed = subprocess.run(
@@ -84,43 +63,27 @@ def run_inline_shell(command: str, cwd: Path | None, timeout: int) -> str:
return f"[inline-shell timeout after {timeout}s: {command}]"
except FileNotFoundError:
return "[inline-shell error: bash not found]"
except RuntimeError as exc:
# tests/conftest.py installs a live-system guard that blocks real
# os.kill on out-of-tree PIDs. subprocess.run(timeout=...) may trip
# that guard while trying to clean up the timed-out shell; treat that
# as the same timeout outcome instead of surfacing the guard error.
if "live-system guard: blocked os.kill" in str(exc):
except Exception as exc:
# tests/conftest.py's live-system guard may block the os.kill that
# subprocess.run uses to clean up a timed-out shell; report the timeout.
if isinstance(exc, RuntimeError) and "live-system guard: blocked os.kill" in str(exc):
return f"[inline-shell timeout after {timeout}s: {command}]"
return f"[inline-shell error: {exc}]"
except Exception as exc:
return f"[inline-shell error: {exc}]"
output = (completed.stdout or "").rstrip("\n")
if not output and completed.stderr:
output = completed.stderr.rstrip("\n")
output = (completed.stdout or "").rstrip("\n") or (completed.stderr or "").rstrip("\n")
if len(output) > _INLINE_SHELL_MAX_OUTPUT:
output = output[:_INLINE_SHELL_MAX_OUTPUT] + "...[truncated]"
return output
def expand_inline_shell(
content: str,
skill_dir: Path | None,
timeout: int,
) -> str:
"""Replace every !`cmd` snippet in ``content`` with its stdout.
Runs each snippet with the skill directory as CWD so relative paths in
the snippet work the way the author expects.
"""
def expand_inline_shell(content: str, skill_dir: Path | None, timeout: int) -> str:
"""Replace every !`cmd` snippet with its stdout, run with the skill dir as CWD."""
if "!`" not in content:
return content
def _replace(match: re.Match) -> str:
cmd = match.group(1).strip()
if not cmd:
return ""
return run_inline_shell(cmd, skill_dir, timeout)
return run_inline_shell(cmd, skill_dir, timeout) if cmd else ""
return _INLINE_SHELL_RE.sub(_replace, content)
@@ -134,11 +97,9 @@ def preprocess_skill_content(
"""Apply configured SKILL.md template and inline-shell preprocessing."""
if not content:
return content
cfg = skills_cfg if isinstance(skills_cfg, dict) else load_skills_config()
if cfg.get("template_vars", True):
content = substitute_template_vars(content, skill_dir, session_id)
if cfg.get("inline_shell", False):
timeout = int(cfg.get("inline_shell_timeout", 10) or 10)
content = expand_inline_shell(content, skill_dir, timeout)
content = expand_inline_shell(content, skill_dir, int(cfg.get("inline_shell_timeout", 10) or 10))
return content