From 340be3750d77ab5bb5dd2196ea6f7ae0a5e849aa Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:27:33 +0530 Subject: [PATCH] fix(skills): slash commands register only names that resolve uniquely scan_skill_commands kept the first copy of a duplicated name (project > local > external, no create_dir), so /dup-demo silently ran a/one while skill_view refused dup-demo. Build the map from the shared skill catalog instead: only `unique` entries get a /command; same-tier duplicates stay reachable by exact path (`/skill a/one`), shadowed copies never register. /reload-skills also clears the catalog cache so a reload never serves a TTL-stale view. --- agent/skill_commands.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/agent/skill_commands.py b/agent/skill_commands.py index 909305e788..5944116179 100644 --- a/agent/skill_commands.py +++ b/agent/skill_commands.py @@ -458,28 +458,20 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: # (#74574). commands: Dict[str, Dict[str, Any]] = {} try: - from tools.skills_tool import _skills_dir, _get_disabled_skill_names - from agent.skill_utils import ( - get_external_skills_dirs, get_project_skills_dirs, iter_project_skill_files, iter_skill_index_files, - ) + from tools.skills_tool import _get_disabled_skill_names, _skill_catalog disabled = _get_disabled_skill_names() seen_names: set = set() - # Precedence: project (through the quarantine chokepoint) > local > external. - # Resolve the local dir at call time: import-time SKILLS_DIR is frozen to - # the launch home, but a multiplexed profile scope may have changed it. - # See #67277. - skills_dir = _skills_dir() - iters = [iter_project_skill_files(d) for d in get_project_skills_dirs()] - local = [skills_dir] if skills_dir.exists() else [] - iters += [iter_skill_index_files(d, "SKILL.md") for d in local + get_external_skills_dirs()] - for _iter in iters: - for skill_md in _iter: + # Only names skill_view resolves to exactly this file get a /command: the shared catalog applies + # project > local > create_dir > external precedence (live profile dir, #67277) and leaves + # same-tier duplicates to their exact paths (`/skill a/one`), so a bare slug never guesses. + for entry in _skill_catalog(): + if entry["status"] == "unique": try: - _scan_skill_md(skill_md, disabled, seen_names, commands) + _scan_skill_md(Path(entry["path"]), disabled, seen_names, commands) except Exception: continue except Exception: - pass + logger.debug("Skill command scan failed", exc_info=True) # Publish the scanned map atomically: a reader must see a consistent # (key, map) pair. Only the publish/lookup pair is locked; the scan above # (file I/O, deferred imports) stays outside it (#14536, #74574). @@ -536,6 +528,8 @@ def reload_skills() -> Dict[str, Any]: # Clear the entire multi-slot cache: a skill edit could affect any # platform/profile combination, so every cached identity must rescan. _skill_commands_by_key.clear() + from tools import skills_tool + skills_tool._SKILLS_CACHE.clear() # the scan reads the shared catalog; an explicit reload must not hit its TTL before = command_snapshot(before_commands) new_commands = scan_skill_commands() result = diff_command_snapshots(before, command_snapshot(new_commands))