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.
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user