Honor standalone cleaner skill roots

The standalone helper is advertised from inside the k-skill-cleaner directory, so --skills-root . now resolves that self-skill directory to its parent skills root before scanning siblings. A subprocess regression locks the installed-skill layout that previously returned skill_count 0.\n\nConstraint: Preserve the documented standalone command without requiring users to switch to --skills-root ..\nRejected: Documentation-only fix | would make the advertised command more brittle and leave existing users with empty reports\nConfidence: high\nScope-risk: narrow\nDirective: Keep standalone helper invocation from inside k-skill-cleaner covered when changing root detection\nTested: PYTHONPATH=scripts python3 -m unittest scripts.test_k_skill_cleaner\nTested: Standalone temp-layout smoke from inside k-skill-cleaner with --skills-root .\nTested: npm run lint && npm run typecheck && npm test && npm run ci
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-04-28 17:59:33 +09:00
commit 1935e641a6
2 changed files with 71 additions and 2 deletions

View file

@ -71,10 +71,33 @@ AGENT_USAGE_SOURCES = [
]
def resolve_skills_root(root: Path | str) -> Path:
"""Resolve the directory that contains installable skill directories.
Standalone installs tell users to run this helper from inside the
``k-skill-cleaner`` directory with ``--skills-root .``. In that layout, the
current directory is itself a skill, while sibling skill directories live in
the parent directory. Treat that self-skill root as shorthand for its parent
so the advertised standalone command scans the installed skill bundle.
"""
root_path = Path(root).expanduser().resolve()
if (root_path / "SKILL.md").is_file():
parent = root_path.parent
if any(
child.is_dir()
and child.name not in EXCLUDED_ROOT_DIRS
and (child / "SKILL.md").is_file()
for child in parent.iterdir()
):
return parent
return root_path
def find_skill_dirs(root: Path | str) -> list[str]:
"""Return root-level directories that look like installable skills."""
root_path = Path(root)
root_path = resolve_skills_root(root)
skills: list[str] = []
for child in root_path.iterdir():
if not child.is_dir() or child.name in EXCLUDED_ROOT_DIRS:
@ -316,7 +339,11 @@ def _resolve_since(days: int | None, since: str | None, now: datetime | None = N
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Suggest K-skill cleanup candidates from interviews and usage logs.")
parser.add_argument("--skills-root", default=".", help="Repository root containing root-level skill directories")
parser.add_argument(
"--skills-root",
default=".",
help="Directory containing root-level skills; a skill directory with SKILL.md auto-scans its parent",
)
parser.add_argument("--usage-json", help="Optional JSON object mapping skill names to trigger counts")
parser.add_argument("--log", action="append", default=[], help="Agent log file to scan; repeatable")
parser.add_argument("--scan-default-logs", action="store_true", help="Best-effort scan known local agent log locations")

View file

@ -1,4 +1,7 @@
import json
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
@ -106,6 +109,45 @@ class KSkillCleanerTest(unittest.TestCase):
self.assertTrue(source["paths"] or source["fallback"])
self.assertIn("confidence", source)
def test_skill_local_helper_autodetects_parent_skills_root(self):
with tempfile.TemporaryDirectory() as tmp:
skills_root = Path(tmp)
cleaner_dir = skills_root / "k-skill-cleaner"
cleaner_scripts = cleaner_dir / "scripts"
cleaner_scripts.mkdir(parents=True)
(cleaner_dir / "SKILL.md").write_text("---\nname: k-skill-cleaner\n", encoding="utf-8")
shutil.copyfile(
Path(__file__).resolve().parents[1] / "k-skill-cleaner" / "scripts" / "k_skill_cleaner.py",
cleaner_scripts / "k_skill_cleaner.py",
)
for skill in ["kbo-results", "k-skill-setup"]:
skill_dir = skills_root / skill
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(f"---\nname: {skill}\n", encoding="utf-8")
result = subprocess.run(
[
sys.executable,
"scripts/k_skill_cleaner.py",
"--skills-root",
".",
"--never-use",
"kbo-results",
"--keep",
"k-skill-setup",
],
cwd=cleaner_dir,
check=True,
text=True,
capture_output=True,
)
report = json.loads(result.stdout)
self.assertEqual(report["skill_count"], 3)
self.assertEqual(report["candidates"][0]["skill"], "kbo-results")
self.assertEqual(report["candidates"][0]["action"], "remove")
if __name__ == "__main__":
unittest.main()