feat: project-local skill discovery with per-repo trust gate

Sessions started inside a git checkout now source skills from
<root>/.hermes/skills/ and <root>/.agents/skills/ (the cross-tool
convention shared with other agent harnesses) as the highest-precedence
skill tier: project > local > external_dirs.

Loading is trust-gated per repo (skills.trusted_project_dirs, managed by
'hermes skills trust'/'untrust') because skills are executable procedure
documents — auto-sourcing them from any cloned repo is a prompt-injection
vector. Untrusted repos with skills get a one-line banner notice instead.

- agent/skill_utils.py: find_project_root, get_project_skills_dirs,
  get_untrusted_project_skills_root, get_scan_ordered_skills_dirs;
  project dirs join the curator read-only ownership boundary
- agent/prompt_builder.py: project tier scanned first, entries tagged
  [project], same-named local entries shadowed; cache key extended
- tools/skills_tool.py: skills_list scans project dirs first (first-wins);
  skill_view resolves cross-tier collisions in favor of the project tier
  (same-tier ambiguity still refuses); security warning recognizes the tier
- agent/skill_commands.py + hermes_cli/commands.py: /skill-name slash
  commands and gateway slash menus include project skills
- tools/credential_files.py: project dirs mounted into remote backends
- cli.py: banner notice (loaded count / trust hint)
- hermes_cli/main.py + subcommands/skills.py: hermes skills trust/untrust
- config: skills.project_discovery (default on), skills.trusted_project_dirs
- docs: Project-Local Skills section in skills.md
- tests: tests/agent/test_project_skills.py (18 cases)

Session cwd is fixed at agent build time, so the resolved tier is stable
for the conversation and the system prompt stays byte-stable (cache-safe).
This commit is contained in:
Teknium
2026-08-17 11:27:36 -07:00
parent 187e5b2b95
commit f891d702df
12 changed files with 637 additions and 18 deletions
+29
View File
@@ -8328,6 +8328,35 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
"[dim] Switch with: /model sonnet or /model gpt5[/]"
)
# Project-local skills: one-line status. Trusted → show count;
# untrusted-with-skills → point at `hermes skills trust`. Never raises.
try:
from agent.skill_utils import (
get_project_skills_dirs,
get_untrusted_project_skills_root,
iter_skill_index_files,
)
_proj_dirs = get_project_skills_dirs()
if _proj_dirs:
_n = sum(
sum(1 for _ in iter_skill_index_files(d, "SKILL.md"))
for d in _proj_dirs
)
if _n:
self._console_print(
f"[dim]◆ {_n} project skill(s) loaded from this repo[/]"
)
else:
_untrusted = get_untrusted_project_skills_root()
if _untrusted is not None:
_root, _n = _untrusted
self._console_print(
f"[yellow]◆ {_n} project skill(s) found in {_root} but not "
f"loaded — run `hermes skills trust` to enable them.[/]"
)
except Exception:
logger.debug("project skills banner notice failed", exc_info=True)
self._console_print()
def _restore_session_cwd(self, session_meta: dict, *, quiet: bool = False) -> None: