{"repo":"NousResearch/hermes-agent","public_id":"861","display_label":"Fix mislabeling of local skills as builtin","summary":"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.","subsystem":"hermes_cli","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":862,"merged_at":"2026-03-12T15:05:34+00:00"},"resolution":{"state":"resolved","by":"pr-862"},"freshness":{"state":"stale","last_verified_at":"2026-03-12T15:05:34+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":861,"pr_number":862,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-12T15:05:34+00:00"}],"members":[{"kind":"issue","number":861,"title":"skills list mislabels local skills as builtin","url":null,"state":"closed"},{"kind":"pr","number":862,"title":"fix(skills): avoid mislabeling local skills as builtin in skills list","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","hermes_cli/skills_hub.py","tests/hermes_cli/test_skills_hub.py"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -2246,7 +2246,7 @@ def cmd_pairing(args):\n     skills_inspect.add_argument(\"identifier\", help=\"Skill identifier\")\n \n     skills_list = skills_subparsers.add_parser(\"list\", help=\"List installed skills\")\n-    skills_list.add_argument(\"--source\", default=\"all\", choices=[\"all\", \"hub\", \"builtin\"])\n+    skills_list.add_argument(\"--source\", default=\"all\", choices=[\"all\", \"hub\", \"builtin\", \"local\"])\n \n     skills_audit = skills_subparsers.add_parser(\"audit\", help=\"Re-scan installed hub skills\")\n     skills_audit.add_argument(\"name\", nargs=\"?\", help=\"Specific skill to audit (default: all)\")"]},{"file":"hermes_cli/skills_hub.py","hunks":["@@ -407,14 +407,17 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None:\n \n \n def do_list(source_filter: str = \"all\", console: Optional[Console] = None) -> None:\n-    \"\"\"List installed skills, distinguishing builtins from hub-installed.\"\"\"\n+    \"\"\"List installed skills, distinguishing hub, builtin, and local skills.\"\"\"\n     from tools.skills_hub import HubLockFile, ensure_hub_dirs\n+    from tools.skills_sync import _read_manifest\n     from tools.skills_tool import _find_all_skills\n \n     c = console or _console\n     ensure_hub_dirs()\n     lock = HubLockFile()\n     hub_installed = {e[\"name\"]: e for e in lock.list_installed()}\n+    bundled_manifest = _read_manifest()\n+    builtin_names = set(bundled_manifest.keys()) if isinstance(bundled_manifest, dict) else set()\n \n     all_skills = _find_all_skills()\n ","@@ -424,30 +427,42 @@ def do_list(source_filter: str = \"all\", console: Optional[Console] = None) -> No\n     table.add_column(\"Source\", style=\"dim\")\n     table.add_column(\"Trust\", style=\"dim\")\n \n+    hub_count = 0\n+    builtin_count = 0\n+    local_count = 0\n+\n     for skill in sorted(all_skills, key=lambda s: (s.get(\"category\") or \"\", s[\"name\"])):\n         name = skill[\"name\"]\n         category = skill.get(\"category\", \"\")\n         hub_entry = hub_installed.get(name)\n \n         if hub_entry:\n+            source_type = \"hub\"\n             source_display = hub_entry.get(\"source\", \"hub\")\n             trust = hub_entry.get(\"trust_level\", \"community\")\n-        else:\n+            hub_count += 1\n+        elif name in builtin_names:\n+            source_type = \"builtin\"\n             source_display = \"builtin\"\n             trust = \"builtin\"\n+            builtin_count += 1\n+        else:\n+            source_type = \"local\"\n+            source_display = \"local\"\n+            trust = \"local\"\n+            local_count += 1\n \n-        if source_filter == \"hub\" and not hub_entry:\n-            continue\n-        if source_filter == \"builtin\" and hub_entry:\n+        if source_filter != \"all\" and source_filter != source_type:\n             continue\n \n-        trust_style = {\"builtin\": \"bright_cyan\", \"trusted\": \"green\", \"community\": \"yellow\"}.get(trust, \"dim\")\n+        trust_style = {\"builtin\": \"bright_cyan\", \"trusted\": \"green\", \"community\": \"yellow\", \"local\": \"dim\"}.get(trust, \"dim\")\n         trust_label = \"official\" if source_display == \"official\" else trust\n         table.add_row(name, category, source_display, f\"[{trust_style}]{trust_label}[/]\")\n \n     c.print(table)\n-    c.print(f\"[dim]{len(hub_installed)} hub-installed, \"\n-            f\"{len(all_skills) - len(hub_installed)} builtin[/]\\n\")\n+    c.print(\n+        f\"[dim]{hub_count} hub-installed, {builtin_count} builtin, {local_count} local[/]\\n\"\n+    )\n \n \n def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> None:","@@ -1014,7 +1029,7 @@ def _print_skills_help(console: Console) -> None:\n         \"  [cyan]search[/] <query>              Search registries for skills\\n\"\n         \"  [cyan]install[/] <identifier>        Install a skill (with security scan)\\n\"\n         \"  [cyan]inspect[/] <identifier>        Preview a skill without installing\\n\"\n-        \"  [cyan]list[/] [--source hub|builtin] List installed skills\\n\"\n+        \"  [cyan]list[/] [--source hub|builtin|local] List installed skills\\n\"\n         \"  [cyan]audit[/] [name]                Re-scan hub skills for security\\n\"\n         \"  [cyan]uninstall[/] <name>            Remove a hub-installed skill\\n\"\n         \"  [cyan]publish[/] <path> --repo <r>   Publish a skill to GitHub via PR\\n\""]},{"file":"tests/hermes_cli/test_skills_hub.py","hunks":["@@ -5,6 +5,14 @@\n from hermes_cli.skills_hub import do_list\n \n \n+class _DummyLockFile:\n+    def __init__(self, installed):\n+        self._installed = installed\n+\n+    def list_installed(self):\n+        return self._installed\n+\n+\n def test_do_list_initializes_hub_dir(monkeypatch, tmp_path):\n     import tools.skills_hub as hub\n     import tools.skills_tool as skills_tool","@@ -29,3 +37,82 @@ def test_do_list_initializes_hub_dir(monkeypatch, tmp_path):\n     assert (hub_dir / \"lock.json\").exists()\n     assert (hub_dir / \"quarantine\").is_dir()\n     assert (hub_dir / \"index-cache\").is_dir()\n+\n+\n+def test_do_list_distinguishes_hub_builtin_and_local(monkeypatch, tmp_path):\n+    import tools.skills_hub as hub\n+    import tools.skills_sync as skills_sync\n+    import tools.skills_tool as skills_tool\n+\n+    hub_dir = tmp_path / \"skills\" / \".hub\"\n+    monkeypatch.setattr(hub, \"SKILLS_DIR\", tmp_path / \"skills\")\n+    monkeypatch.setattr(hub, \"HUB_DIR\", hub_dir)\n+    monkeypatch.setattr(hub, \"LOCK_FILE\", hub_dir / \"lock.json\")\n+    monkeypatch.setattr(hub, \"QUARANTINE_DIR\", hub_dir / \"quarantine\")\n+    monkeypatch.setattr(hub, \"AUDIT_LOG\", hub_dir / \"audit.log\")\n+    monkeypatch.setattr(hub, \"TAPS_FILE\", hub_dir / \"taps.json\")\n+    monkeypatch.setattr(hub, \"INDEX_CACHE_DIR\", hub_dir / \"index-cache\")\n+\n+    monkeypatch.setattr(\n+        hub,\n+        \"HubLockFile\",\n+        lambda: _DummyLockFile([\n+            {\"name\": \"hub-skill\", \"source\": \"github\", \"trust_level\": \"community\"},\n+        ]),\n+    )\n+    monkeypatch.setattr(\n+        skills_tool,\n+        \"_find_all_skills\",\n+        lambda: [\n+            {\"name\": \"hub-skill\", \"category\": \"x\", \"description\": \"hub\"},\n+            {\"name\": \"builtin-skill\", \"category\": \"x\", \"description\": \"builtin\"},\n+            {\"name\": \"local-skill\", \"category\": \"x\", \"description\": \"local\"},\n+        ],\n+    )\n+    monkeypatch.setattr(skills_sync, \"_read_manifest\", lambda: {\"builtin-skill\": \"abc123\"})\n+\n+    sink = StringIO()\n+    console = Console(file=sink, force_terminal=False, color_system=None)\n+\n+    do_list(console=console)\n+\n+    output = sink.getvalue()\n+    assert \"hub-skill\" in output\n+    assert \"builtin-skill\" in output\n+    assert \"local-skill\" in output\n+    assert \"1 hub-installed, 1 builtin, 1 local\" in output\n+\n+\n+def test_do_list_local_filter(monkeypatch, tmp_path):\n+    import tools.skills_hub as hub\n+    import tools.skills_sync as skills_sync\n+    import tools.skills_tool as skills_tool\n+\n+    hub_dir = tmp_path / \"skills\" / \".hub\"\n+    monkeypatch.setattr(hub, \"SKILLS_DIR\", tmp_path / \"skills\")\n+    monkeypatch.setattr(hub, \"HUB_DIR\", hub_dir)\n+    monkeypatch.setattr(hub, \"LOCK_FILE\", hub_dir / \"lock.json\")\n+    monkeypatch.setattr(hub, \"QUARANTINE_DIR\", hub_dir / \"quarantine\")\n+    monkeypatch.setattr(hub, \"AUDIT_LOG\", hub_dir / \"audit.log\")\n+    monkeypatch.setattr(hub, \"TAPS_FILE\", hub_dir / \"taps.json\")\n+    monkeypatch.setattr(hub, \"INDEX_CACHE_DIR\", hub_dir / \"index-cache\")\n+\n+    monkeypatch.setattr(hub, \"HubLockFile\", lambda: _DummyLockFile([]))\n+    monkeypatch.setattr(\n+        skills_tool,\n+        \"_find_all_skills\",\n+        lambda: [\n+            {\"name\": \"builtin-skill\", \"category\": \"x\", \"description\": \"builtin\"},\n+            {\"name\": \"local-skill\", \"category\": \"x\", \"description\": \"local\"},\n+        ],\n+    )\n+    monkeypatch.setattr(skills_sync, \"_read_manifest\", lambda: {\"builtin-skill\": \"abc123\"})\n+\n+    sink = StringIO()\n+    console = Console(file=sink, force_terminal=False, color_system=None)\n+\n+    do_list(source_filter=\"local\", console=console)\n+\n+    output = sink.getvalue()\n+    assert \"local-skill\" in output\n+    assert \"builtin-skill\" not in output\n"]}]},{"kind":"pr","number":869,"title":"fix(skills): distinguish local skills from builtin in 'skills list'","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#958","number":958}],"category":"merged_via","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":1014,"title":"fix(skills): label local skills correctly in skills list","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"bb7cdc6d4","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true}],"trust":{"default_tier":"verified","includes_suggested":false}}