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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #862 fix(skills): avoid mislabeling local skills as builtin in skills list

    merged to main

    files: hermes_cli/main.py, hermes_cli/skills_hub.py, tests/hermes_cli/test_skills_hub.py

    Show 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

  • #869 fix(skills): distinguish local skills from builtin in 'skills list' · merged via another change → #958
  • #1014 fix(skills): label local skills correctly in skills list · done elsewhere → bb7cdc6
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.00 means 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 Lab

At 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.

For your agent

The same facts, typed.