NousResearch/hermes-agent
Fix mislabeling of local skills as builtin
Cluster 861 · hermes cli. Data as of
Nothing open on GitHub. Fix merged in #862 on 12 Mar 2026. Check it covers your case.
The short answer
0open issues
0open PRs
4items grouped
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #862. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #862. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and PRs address a bug where local skills are incorrectly labeled as builtin in the skills list. The work involves distinguishing local skills from builtin ones using path-based classification.
Open issues0
Open PRs0
Fixes on record
-
#862 fix(skills): avoid mislabeling local skills as builtin in skills list
files:
hermes_cli/main.py,hermes_cli/skills_hub.py,tests/hermes_cli/test_skills_hub.pyShow the diff
-
hermes_cli/main.py@@ -2246,7 +2246,7 @@ def cmd_pairing(args): skills_inspect.add_argument("identifier", help="Skill identifier") skills_list = skills_subparsers.add_parser("list", help="List installed skills") - skills_list.add_argument("--source", default="all", choices=["all", "hub", "builtin"]) + skills_list.add_argument("--source", default="all", choices=["all", "hub", "builtin", "local"]) skills_audit = skills_subparsers.add_parser("audit", help="Re-scan installed hub skills") skills_audit.add_argument("name", nargs="?", help="Specific skill to audit (default: all)") -
hermes_cli/skills_hub.py@@ -407,14 +407,17 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None: def do_list(source_filter: str = "all", console: Optional[Console] = None) -> None: - """List installed skills, distinguishing builtins from hub-installed.""" + """List installed skills, distinguishing hub, builtin, and local skills.""" from tools.skills_hub import HubLockFile, ensure_hub_dirs + from tools.skills_sync import _read_manifest from tools.skills_tool import _find_all_skills c = console or _console ensure_hub_dirs() lock = HubLockFile() hub_installed = {e["name"]: e for e in lock.list_installed()} + bundled_manifest = _read_manifest() + builtin_names = set(bundled_manifest.keys()) if isinstance(bundled_manifest, dict) else set() all_skills = _find_all_skills() @@ -424,30 +427,42 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No table.add_column("Source", style="dim") table.add_column("Trust", style="dim") + hub_count = 0 + builtin_count = 0 + local_count = 0 + for skill in sorted(all_skills, key=lambda s: (s.get("category") or "", s["name"])): name = skill["name"] category = skill.get("category", "") hub_entry = hub_installed.get(name) if hub_entry: + source_type = "hub" source_display = hub_entry.get("source", "hub") trust = hub_entry.get("trust_level", "community") - else: + hub_count += 1 + elif name in builtin_names: + source_type = "builtin" source_display = "builtin" trust = "builtin" + builtin_count += 1 + else: + source_type = "local" + source_display = "local" + trust = "local" + local_count += 1 - if source_filter == "hub" and not hub_entry: - continue - if source_filter == "builtin" and hub_entry: + if source_filter != "all" and source_filter != source_type: continue - trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(trust, "dim") + trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow", "local": "dim"}.get(trust, "dim") trust_label = "official" if source_display == "official" else trust table.add_row(name, category, source_display, f"[{trust_style}]{trust_label}[/]") c.print(table) - c.print(f"[dim]{len(hub_installed)} hub-installed, " - f"{len(all_skills) - len(hub_installed)} builtin[/]\n") + c.print( + f"[dim]{hub_count} hub-installed, {builtin_count} builtin, {local_count} local[/]\n" + ) def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> None: @@ -1014,7 +1029,7 @@ def _print_skills_help(console: Console) -> None: " [cyan]search[/] <query> Search registries for skills\n" " [cyan]install[/] <identifier> Install a skill (with security scan)\n" " [cyan]inspect[/] <identifier> Preview a skill without installing\n" - " [cyan]list[/] [--source hub|builtin] List installed skills\n" + " [cyan]list[/] [--source hub|builtin|local] List installed skills\n" " [cyan]audit[/] [name] Re-scan hub skills for security\n" " [cyan]uninstall[/] <name> Remove a hub-installed skill\n" " [cyan]publish[/] <path> --repo <r> Publish a skill to GitHub via PR\n" -
tests/hermes_cli/test_skills_hub.py@@ -5,6 +5,14 @@ from hermes_cli.skills_hub import do_list +class _DummyLockFile: + def __init__(self, installed): + self._installed = installed + + def list_installed(self): + return self._installed + + def test_do_list_initializes_hub_dir(monkeypatch, tmp_path): import tools.skills_hub as hub import tools.skills_tool as skills_tool @@ -29,3 +37,82 @@ def test_do_list_initializes_hub_dir(monkeypatch, tmp_path): assert (hub_dir / "lock.json").exists() assert (hub_dir / "quarantine").is_dir() assert (hub_dir / "index-cache").is_dir() + + +def test_do_list_distinguishes_hub_builtin_and_local(monkeypatch, tmp_path): + import tools.skills_hub as hub + import tools.skills_sync as skills_sync + import tools.skills_tool as skills_tool + + hub_dir = tmp_path / "skills" / ".hub" + monkeypatch.setattr(hub, "SKILLS_DIR", tmp_path / "skills") + monkeypatch.setattr(hub, "HUB_DIR", hub_dir) + monkeypatch.setattr(hub, "LOCK_FILE", hub_dir / "lock.json") + monkeypatch.setattr(hub, "QUARANTINE_DIR", hub_dir / "quarantine") + monkeypatch.setattr(hub, "AUDIT_LOG", hub_dir / "audit.log") + monkeypatch.setattr(hub, "TAPS_FILE", hub_dir / "taps.json") + monkeypatch.setattr(hub, "INDEX_CACHE_DIR", hub_dir / "index-cache") + + monkeypatch.setattr( + hub, + "HubLockFile", + lambda: _DummyLockFile([ + {"name": "hub-skill", "source": "github", "trust_level": "community"}, + ]), + ) + monkeypatch.setattr( + skills_tool, + "_find_all_skills", + lambda: [ + {"name": "hub-skill", "category": "x", "description": "hub"}, + {"name": "builtin-skill", "category": "x", "description": "builtin"}, + {"name": "local-skill", "category": "x", "description": "local"}, + ], + ) + monkeypatch.setattr(skills_sync, "_read_manifest", lambda: {"builtin-skill": "abc123"}) + + sink = StringIO() + console = Console(file=sink, force_terminal=False, color_system=None) + + do_list(console=console) + + output = sink.getvalue() + assert "hub-skill" in output + assert "builtin-skill" in output + assert "local-skill" in output + assert "1 hub-installed, 1 builtin, 1 local" in output + + +def test_do_list_local_filter(monkeypatch, tmp_path): + import tools.skills_hub as hub + import tools.skills_sync as skills_sync + import tools.skills_tool as skills_tool + + hub_dir = tmp_path / "skills" / ".hub" + monkeypatch.setattr(hub, "SKILLS_DIR", tmp_path / "skills") + monkeypatch.setattr(hub, "HUB_DIR", hub_dir) + monkeypatch.setattr(hub, "LOCK_FILE", hub_dir / "lock.json") + monkeypatch.setattr(hub, "QUARANTINE_DIR", hub_dir / "quarantine") + monkeypatch.setattr(hub, "AUDIT_LOG", hub_dir / "audit.log") + monkeypatch.setattr(hub, "TAPS_FILE", hub_dir / "taps.json") + monkeypatch.setattr(hub, "INDEX_CACHE_DIR", hub_dir / "index-cache") + + monkeypatch.setattr(hub, "HubLockFile", lambda: _DummyLockFile([])) + monkeypatch.setattr( + skills_tool, + "_find_all_skills", + lambda: [ + {"name": "builtin-skill", "category": "x", "description": "builtin"}, + {"name": "local-skill", "category": "x", "description": "local"}, + ], + ) + monkeypatch.setattr(skills_sync, "_read_manifest", lambda: {"builtin-skill": "abc123"}) + + sink = StringIO() + console = Console(file=sink, force_terminal=False, color_system=None) + + do_list(source_filter="local", console=console) + + output = sink.getvalue() + assert "local-skill" in output + assert "builtin-skill" not in output
-
2 PRs closed without merging
From the close comments: done elsewhere 1 · merged via another change 1
Closed issues · 1
- Closed#861skills list mislabels local skills as builtin
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #862
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 12 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.