{"repo":"NousResearch/hermes-agent","public_id":"4759","display_label":"Symlink support in skill discovery and management","summary":"These issues and pull requests address a recurring problem where skill discovery and management tools fail to follow symbolic links, causing symlinked skills to be invisible or unmodifiable. Multiple approaches and fixes target rglob behavior, external directories, and related lookup paths.","subsystem":"skills","counts":{"members":20,"open_issues":2,"open_issues_distinct":2,"open_prs":6,"closed_issues":3,"merged_prs":1,"attempt_prs":14},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":17512},"resolution":{"state":"partially_resolved","by":"pr-17512"},"freshness":{"state":"verified","last_verified_at":"2026-04-29T15:16:52+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.09936842105263158},"fix_edges":[{"issue_number":4381,"pr_number":17512,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":4759,"pr_number":17512,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":4759,"title":"skill_manage patch/edit/delete can't find skills from external_dirs","url":null,"state":"closed"},{"kind":"issue","number":8293,"title":"Bug: symlinked skills under ~/.hermes/skills are omitted from skills_list and bare-name skill_view lookup","url":null,"state":"closed"},{"kind":"issue","number":35184,"title":"skill_manager_tool._find_skill uses Path.rglob() which does not follow symlinks, making symlinked skills invisible to skill_manage","url":null,"state":"open"},{"kind":"issue","number":44609,"title":"skill_manage _find_skill uses rglob which silently skips symlinked skills (Python 3.12)","url":null,"state":"closed"},{"kind":"issue","number":54195,"title":"skill_manage cannot find symlinked skills in skills.external_dirs","url":null,"state":"open"},{"kind":"pr","number":4868,"title":"fix: skill_manage now finds skills from external_dirs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"ad4feeaf0","number":0},{"kind":"commit","value":"ad4feeaf0d617010bb18d7efa1dbfcfce6a812b6","number":0},{"kind":"pr","value":"#4759","number":4759}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":5284,"title":"fix(skills): respect local skill dir in skill manager","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"ad4feeaf0","number":0},{"kind":"commit","value":"ad4feeaf0d617010bb18d7efa1dbfcfce6a812b6","number":0},{"kind":"commit","value":"ad4feeaf0","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":8357,"title":"fix(skills): follow symlinked local skill dirs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#14230","number":14230},{"kind":"pr","value":"#8769","number":8769},{"kind":"pr","value":"#7634","number":7634},{"kind":"pr","value":"#12624","number":12624}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":8769,"title":"fix(skills): discover symlinked skill directories","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#14230","number":14230},{"kind":"pr","value":"#7634","number":7634},{"kind":"pr","value":"#12624","number":12624}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":12624,"title":"fix(skills): follow symlinks when discovering SKILL.md","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":17512,"title":"fix(skills): let skill_manage modify skills in external_dirs in place","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_skill_manager_tool.py","tools/skill_manager_tool.py"],"fix_hunks":[{"file":"tests/tools/test_skill_manager_tool.py","hunks":["@@ -566,3 +566,151 @@ def test_guard_flag_handles_config_error(self):\n \n         with patch(\"hermes_cli.config.load_config\", side_effect=RuntimeError(\"boom\")):\n             assert _guard_agent_created_enabled() is False\n+\n+\n+# ---------------------------------------------------------------------------\n+# External skills directories (skills.external_dirs) — mutations in place\n+# ---------------------------------------------------------------------------\n+\n+\n+@contextmanager\n+def _two_roots(local_dir: Path, external_dir: Path):\n+    \"\"\"Patch the skill manager so local SKILLS_DIR = local_dir and\n+    get_all_skills_dirs() returns [local_dir, external_dir] in order.\"\"\"\n+    with patch(\"tools.skill_manager_tool.SKILLS_DIR\", local_dir), \\\n+         patch(\"agent.skill_utils.get_all_skills_dirs\",\n+               return_value=[local_dir, external_dir]):\n+        yield\n+\n+\n+def _write_external_skill(external_dir: Path, name: str = \"ext-skill\") -> Path:\n+    skill_dir = external_dir / name\n+    skill_dir.mkdir(parents=True)\n+    (skill_dir / \"SKILL.md\").write_text(\n+        f\"---\\nname: {name}\\ndescription: An external skill.\\n---\\n\\n\"\n+        \"# External\\n\\nBody with OLD_MARKER here.\\n\"\n+    )\n+    return skill_dir\n+\n+\n+class TestExternalSkillMutations:\n+    \"\"\"Verify skill_manage can patch/edit/write/remove/delete skills that live\n+    under skills.external_dirs — in place, without duplicating to local.\n+\n+    Regression for issues #4759 and #4381: the read-only gate used to refuse\n+    with 'Skill X is in an external directory and cannot be modified', which\n+    caused agents to create duplicate copies in ~/.hermes/skills/ as a\n+    workaround.\n+    \"\"\"\n+\n+    def test_patch_external_skill_writes_in_place(self, tmp_path):\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+        skill_dir = _write_external_skill(external)\n+\n+        with _two_roots(local, external):\n+            result = _patch_skill(\"ext-skill\", \"OLD_MARKER\", \"NEW_MARKER\")\n+\n+        assert result[\"success\"] is True, result\n+        assert \"NEW_MARKER\" in (skill_dir / \"SKILL.md\").read_text()\n+        # No duplicate in local\n+        assert not (local / \"ext-skill\").exists()\n+\n+    def test_edit_external_skill_writes_in_place(self, tmp_path):\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+        skill_dir = _write_external_skill(external)\n+\n+        new_content = (\n+            \"---\\nname: ext-skill\\ndescription: Rewritten.\\n---\\n\\n\"\n+            \"# Rewritten\\n\\nBrand new body.\\n\"\n+        )\n+        with _two_roots(local, external):\n+            result = _edit_skill(\"ext-skill\", new_content)\n+\n+        assert result[\"success\"] is True, result\n+        assert \"Brand new body\" in (skill_dir / \"SKILL.md\").read_text()\n+        assert not (local / \"ext-skill\").exists()\n+\n+    def test_write_file_on_external_skill(self, tmp_path):\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+        skill_dir = _write_external_skill(external)\n+\n+        with _two_roots(local, external):\n+            result = _write_file(\"ext-skill\", \"references/notes.md\", \"# Notes\\n\")\n+\n+        assert result[\"success\"] is True, result\n+        assert (skill_dir / \"references\" / \"notes.md\").read_text() == \"# Notes\\n\"\n+        assert not (local / \"ext-skill\").exists()\n+\n+    def test_remove_file_on_external_skill(self, tmp_path):\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+        skill_dir = _write_external_skill(external)\n+        (skill_dir / \"references\").mkdir()\n+        (skill_dir / \"references\" / \"notes.md\").write_text(\"# Notes\\n\")\n+\n+        with _two_roots(local, external):\n+            result = _remove_file(\"ext-skill\", \"references/notes.md\")\n+\n+        assert result[\"success\"] is True, result\n+        assert not (skill_dir / \"references\" / \"notes.md\").exists()\n+\n+    def test_delete_external_skill_removes_skill_not_root(self, tmp_path):\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+        skill_dir = _write_external_skill(external)\n+\n+        with _two_roots(local, external):\n+            result = _delete_skill(\"ext-skill\")\n+\n+        assert result[\"success\"] is True, result\n+        assert not skill_dir.exists()\n+        # The external root must NOT be rmdir'd, even when empty after deletion\n+        assert external.exists() and external.is_dir()\n+\n+    def test_delete_external_skill_cleans_empty_category(self, tmp_path):\n+        \"\"\"When a skill lives under external/<category>/<name>, deleting the\n+        last skill in the category should rmdir the empty category dir but\n+        stop at the external root.\"\"\"\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+        cat_dir = external / \"team\"\n+        cat_dir.mkdir()\n+        skill_dir = cat_dir / \"ext-skill\"\n+        skill_dir.mkdir()\n+        (skill_dir / \"SKILL.md\").write_text(\n+            \"---\\nname: ext-skill\\ndescription: An external skill.\\n---\\n\\n\"\n+            \"# External\\n\\nBody.\\n\"\n+        )\n+\n+        with _two_roots(local, external):\n+            result = _delete_skill(\"ext-skill\")\n+\n+        assert result[\"success\"] is True, result\n+        assert not skill_dir.exists()\n+        assert not cat_dir.exists()  # empty category cleaned up\n+        assert external.exists()     # but never the external root\n+\n+    def test_create_still_writes_to_local_root(self, tmp_path):\n+        \"\"\"Creating a new skill always lands in local SKILLS_DIR, never\n+        external_dirs — create is unchanged by this PR.\"\"\"\n+        local = tmp_path / \"local\"\n+        external = tmp_path / \"vault\"\n+        local.mkdir(); external.mkdir()\n+\n+        with _two_roots(local, external):\n+            result = _create_skill(\"fresh-skill\", VALID_SKILL_CONTENT.replace(\n+                \"name: test-skill\", \"name: fresh-skill\"))\n+\n+        assert result[\"success\"] is True, result\n+        assert (local / \"fresh-skill\" / \"SKILL.md\").exists()\n+        assert not (external / \"fresh-skill\").exists()\n+"]},{"file":"tools/skill_manager_tool.py","hunks":["@@ -109,16 +109,28 @@ def _security_scan_skill(skill_dir: Path) -> Optional[str]:\n MAX_DESCRIPTION_LENGTH = 1024\n \n \n-def _is_local_skill(skill_path: Path) -> bool:\n-    \"\"\"Check if a skill path is within the local SKILLS_DIR.\n-\n-    Skills found in external_dirs are read-only from the agent's perspective.\n+def _containing_skills_root(skill_path: Path) -> Path:\n+    \"\"\"Return the skills root directory (local or external_dirs entry) that\n+    contains ``skill_path``.  Falls back to the local ``SKILLS_DIR`` if no\n+    match is found (defensive — callers should have located the skill via\n+    ``_find_skill`` first).\n     \"\"\"\n+    from agent.skill_utils import get_all_skills_dirs\n+\n     try:\n-        skill_path.resolve().relative_to(SKILLS_DIR.resolve())\n-        return True\n-    except ValueError:\n-        return False\n+        resolved = skill_path.resolve()\n+    except OSError:\n+        resolved = skill_path\n+\n+    for root in get_all_skills_dirs():\n+        try:\n+            resolved.relative_to(root.resolve())\n+            return root\n+        except (ValueError, OSError):\n+            continue\n+    return SKILLS_DIR\n+\n+\n MAX_SKILL_CONTENT_CHARS = 100_000   # ~36k tokens at 2.75 chars/token\n MAX_SKILL_FILE_BYTES = 1_048_576    # 1 MiB per supporting file\n ","@@ -397,9 +409,6 @@ def _edit_skill(name: str, content: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found. Use skills_list() to see available skills.\"}\n \n-    if not _is_local_skill(existing[\"path\"]):\n-        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first.\"}\n-\n     skill_md = existing[\"path\"] / \"SKILL.md\"\n     # Back up original content for rollback\n     original_content = skill_md.read_text(encoding=\"utf-8\") if skill_md.exists() else None","@@ -440,9 +449,6 @@ def _patch_skill(\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found.\"}\n \n-    if not _is_local_skill(existing[\"path\"]):\n-        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first.\"}\n-\n     skill_dir = existing[\"path\"]\n \n     if file_path:","@@ -522,15 +528,13 @@ def _delete_skill(name: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found.\"}\n \n-    if not _is_local_skill(existing[\"path\"]):\n-        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be deleted.\"}\n-\n     skill_dir = existing[\"path\"]\n+    skills_root = _containing_skills_root(skill_dir)\n     shutil.rmtree(skill_dir)\n \n-    # Clean up empty category directories (don't remove SKILLS_DIR itself)\n+    # Clean up empty category directories (don't remove the skills root itself)\n     parent = skill_dir.parent\n-    if parent != SKILLS_DIR and parent.exists() and not any(parent.iterdir()):\n+    if parent != skills_root and parent.exists() and not any(parent.iterdir()):\n         parent.rmdir()\n \n     return {","@@ -567,9 +571,6 @@ def _write_file(name: str, file_path: str, file_content: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found. Create it first with action='create'.\"}\n \n-    if not _is_local_skill(existing[\"path\"]):\n-        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified. Copy it to your local skills directory first.\"}\n-\n     target, err = _resolve_skill_target(existing[\"path\"], file_path)\n     if err:\n         return {\"success\": False, \"error\": err}","@@ -604,9 +605,6 @@ def _remove_file(name: str, file_path: str) -> Dict[str, Any]:\n     if not existing:\n         return {\"success\": False, \"error\": f\"Skill '{name}' not found.\"}\n \n-    if not _is_local_skill(existing[\"path\"]):\n-        return {\"success\": False, \"error\": f\"Skill '{name}' is in an external directory and cannot be modified.\"}\n-\n     skill_dir = existing[\"path\"]\n \n     target, err = _resolve_skill_target(skill_dir, file_path)\n"]}]},{"kind":"pr","number":23484,"title":"fix(skill-tools): follow symlinks in skill discovery via rglob_follow","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12624","number":12624},{"kind":"pr","value":"#12624","number":12624}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":35213,"title":"fix(skills): follow symlinks during skill discovery (#35184)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#35184","number":35184},{"kind":"pr","value":"#35213","number":35213},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35299","number":35299}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":35244,"title":"fix(skill_manager): follow symlinks when discovering skills (#35184)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#35184","number":35184},{"kind":"pr","value":"#35213","number":35213},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35299","number":35299}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":54200,"title":"fix(skills): follow symlinks when discovering skills in external dirs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35244","number":35244}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":60960,"title":"Fix _find_skill: use iter_skill_index_files to follow symlinks","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35184","number":35184},{"kind":"pr","value":"#56423","number":56423},{"kind":"pr","value":"#35299","number":35299}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":67748,"title":"fix(skills): follow symlinks in skill management","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"ad4feeaf0","number":0},{"kind":"commit","value":"ad4feeaf0","number":0},{"kind":"commit","value":"e683c9db9","number":0},{"kind":"commit","value":"02aba4a72","number":0},{"kind":"commit","value":"a884f6d5d","number":0},{"kind":"commit","value":"02aba4a72","number":0},{"kind":"commit","value":"a884f6d5d","number":0},{"kind":"commit","value":"a7e516e97ff6","number":0},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#4759","number":4759},{"kind":"pr","value":"#35184","number":35184},{"kind":"pr","value":"#44609","number":44609},{"kind":"pr","value":"#54195","number":54195},{"kind":"pr","value":"#4868","number":4868},{"kind":"pr","value":"#5255","number":5255},{"kind":"pr","value":"#5284","number":5284},{"kind":"pr","value":"#5407","number":5407},{"kind":"pr","value":"#7634","number":7634},{"kind":"pr","value":"#8177","number":8177},{"kind":"pr","value":"#4381","number":4381},{"kind":"pr","value":"#13963","number":13963},{"kind":"pr","value":"#21810","number":21810},{"kind":"pr","value":"#8357","number":8357},{"kind":"pr","value":"#8769","number":8769},{"kind":"pr","value":"#9253","number":9253},{"kind":"pr","value":"#11847","number":11847},{"kind":"pr","value":"#14230","number":14230},{"kind":"pr","value":"#12624","number":12624},{"kind":"pr","value":"#13996","number":13996},{"kind":"pr","value":"#13963","number":13963},{"kind":"pr","value":"#8177","number":8177},{"kind":"pr","value":"#14230","number":14230},{"kind":"pr","value":"#11847","number":11847},{"kind":"pr","value":"#14476","number":14476},{"kind":"pr","value":"#14668","number":14668},{"kind":"pr","value":"#14740","number":14740},{"kind":"pr","value":"#14740","number":14740},{"kind":"pr","value":"#8293","number":8293},{"kind":"pr","value":"#17512","number":17512},{"kind":"pr","value":"#4759","number":4759},{"kind":"pr","value":"#21812","number":21812},{"kind":"pr","value":"#21810","number":21810},{"kind":"pr","value":"#22236","number":22236},{"kind":"pr","value":"#21810","number":21810},{"kind":"pr","value":"#23484","number":23484},{"kind":"pr","value":"#12624","number":12624},{"kind":"pr","value":"#23562","number":23562},{"kind":"pr","value":"#29411","number":29411},{"kind":"pr","value":"#29444","number":29444},{"kind":"pr","value":"#29444","number":29444},{"kind":"pr","value":"#17512","number":17512},{"kind":"pr","value":"#35213","number":35213},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#35299","number":35299},{"kind":"pr","value":"#44129","number":44129},{"kind":"pr","value":"#44210","number":44210},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#52433","number":52433},{"kind":"pr","value":"#54200","number":54200},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#54195","number":54195},{"kind":"pr","value":"#56420","number":56420},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#56423","number":56423},{"kind":"pr","value":"#56420","number":56420},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#60960","number":60960},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#4759","number":4759},{"kind":"pr","value":"#35184","number":35184},{"kind":"pr","value":"#44609","number":44609},{"kind":"pr","value":"#54195","number":54195},{"kind":"pr","value":"#75225","number":75225},{"kind":"pr","value":"#75130","number":75130},{"kind":"pr","value":"#35213","number":35213},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35299","number":35299},{"kind":"pr","value":"#44210","number":44210},{"kind":"pr","value":"#54200","number":54200},{"kind":"pr","value":"#56420","number":56420},{"kind":"pr","value":"#56423","number":56423},{"kind":"pr","value":"#60960","number":60960},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#75225","number":75225},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#8357","number":8357},{"kind":"pr","value":"#8769","number":8769},{"kind":"pr","value":"#9253","number":9253},{"kind":"pr","value":"#11847","number":11847},{"kind":"pr","value":"#12624","number":12624},{"kind":"pr","value":"#14230","number":14230},{"kind":"pr","value":"#14476","number":14476},{"kind":"pr","value":"#14668","number":14668},{"kind":"pr","value":"#14740","number":14740},{"kind":"pr","value":"#8177","number":8177},{"kind":"pr","value":"#13996","number":13996},{"kind":"pr","value":"#21812","number":21812},{"kind":"pr","value":"#22236","number":22236},{"kind":"pr","value":"#23562","number":23562},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#35213","number":35213},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#35299","number":35299},{"kind":"pr","value":"#44210","number":44210},{"kind":"pr","value":"#54200","number":54200},{"kind":"pr","value":"#56423","number":56423},{"kind":"pr","value":"#60960","number":60960},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#8177","number":8177},{"kind":"pr","value":"#75225","number":75225},{"kind":"pr","value":"#75130","number":75130},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#374151","number":374151},{"kind":"pr","value":"#4759","number":4759},{"kind":"pr","value":"#35184","number":35184},{"kind":"pr","value":"#44609","number":44609},{"kind":"pr","value":"#54195","number":54195},{"kind":"pr","value":"#35213","number":35213},{"kind":"pr","value":"#35244","number":35244},{"kind":"pr","value":"#44210","number":44210},{"kind":"pr","value":"#54200","number":54200},{"kind":"pr","value":"#56420","number":56420},{"kind":"pr","value":"#56423","number":56423},{"kind":"pr","value":"#60960","number":60960},{"kind":"pr","value":"#67748","number":67748},{"kind":"pr","value":"#75225","number":75225}],"category":"partial_fix","implication":"needs_llm"},"merged":false},{"kind":"pr","number":108040,"title":"fix(skills): classify external_dirs as external and default-deny in-place writes","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8c8fc6c1ec","number":0},{"kind":"pr","value":"#128387","number":128387},{"kind":"pr","value":"#17512","number":17512}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":110349,"title":"fix(skills): follow symlinks in skill_manage discovery and resolution","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"6580056cbc","number":0},{"kind":"pr","value":"#118550","number":118550}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":112321,"title":"fix(skills): skill_manage follows directory symlinks; rollback and ownership stay on the link","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#118691","number":118691},{"kind":"pr","value":"#118692","number":118692}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}