fix(curator): scan nested archive subdirs in restore_skill

restore_skill() in tools/skill_usage.py used archive_root.iterdir(), which
only walked the top level of .archive/. Skills archived under nested layouts
(e.g. .archive/openclaw-imports/<skill>/ from older archive paths or
external imports) were invisible to both the exact-match and prefix-match
candidate scans, surfacing as a misleading "skill '<name>' not found in
archive" error even though the directory existed on disk.

Switch both candidate scans to archive_root.rglob('*') so the lookup
descends into category subdirectories.

Fixes #17942
This commit is contained in:
0xDevNinja
2026-04-30 17:55:27 +05:30
committed by Teknium
parent 7913d6a90f
commit 564a649e6a
2 changed files with 40 additions and 3 deletions

View File

@ -315,6 +315,41 @@ def test_restore_skill_moves_back(skills_home):
assert get_record("temp-skill")["state"] == "active"
def test_restore_skill_finds_nested_archive_subdir(skills_home):
"""Skills archived under nested category subdirs (e.g.
.archive/<category>/<skill>/) — left behind by older archive layouts or
external imports — must still be restorable by name."""
from tools.skill_usage import restore_skill, get_record
skills_dir = skills_home / "skills"
nested = skills_dir / ".archive" / "openclaw-imports" / "nested-skill"
nested.mkdir(parents=True)
(nested / "SKILL.md").write_text(
"---\nname: nested-skill\ndescription: x\n---\n", encoding="utf-8",
)
ok, msg = restore_skill("nested-skill")
assert ok, msg
assert (skills_dir / "nested-skill" / "SKILL.md").exists()
assert not nested.exists()
assert get_record("nested-skill")["state"] == "active"
def test_restore_skill_finds_nested_timestamped_prefix(skills_home):
"""Prefix-match path (timestamped dupes) must also descend into nested
archive subdirs, not just .archive/ top-level."""
from tools.skill_usage import restore_skill
skills_dir = skills_home / "skills"
nested = skills_dir / ".archive" / "imports" / "dup-skill-20260101000000"
nested.mkdir(parents=True)
(nested / "SKILL.md").write_text(
"---\nname: dup-skill\ndescription: x\n---\n", encoding="utf-8",
)
ok, msg = restore_skill("dup-skill")
assert ok, msg
assert (skills_dir / "dup-skill" / "SKILL.md").exists()
def test_archive_collision_gets_suffix(skills_home):
from tools.skill_usage import archive_skill
skills_dir = skills_home / "skills"

View File

@ -385,11 +385,13 @@ def restore_skill(skill_name: str) -> Tuple[bool, str]:
if not archive_root.exists():
return False, "no archive directory"
# Try exact name match first, then any prefix match (for timestamped dupes)
candidates = [p for p in archive_root.iterdir() if p.is_dir() and p.name == skill_name]
# Try exact name match first, then any prefix match (for timestamped dupes).
# Recursive walk handles nested archive layouts (e.g. .archive/<category>/<skill>/)
# left behind by older archive paths or external imports.
candidates = [p for p in archive_root.rglob("*") if p.is_dir() and p.name == skill_name]
if not candidates:
candidates = sorted(
[p for p in archive_root.iterdir()
[p for p in archive_root.rglob("*")
if p.is_dir() and p.name.startswith(f"{skill_name}-")],
reverse=True,
)