refactor(skills)!: rename every skill to kvb-<category>-<name>

All 38 first-party skills move to kvb-<category>-<name> with the categories think, create, code, ci, git, check, org and skills; kvb-cleanup and kvb-help are the only uncategorized names. tests/test_naming.py pins the schema, the Agent Skills name rules and the README organigram. enable-plugins.js repoints hook commands of renamed skills in settings.local.json so a stale script path cannot block tools.

BREAKING CHANGE: the old skill names and slash commands (/commit-next, /dream, /autotune, ...) are gone; use their kvb- successors listed in the README organigram.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-01 00:47:23 +01:00
1 parent 64403cefca
commit 624baccfc4
59 files changed
+504 -312

No files matched your search

+9 -9
View File
@@ -1,4 +1,4 @@
"""Validate the autotune reminder hook and its settings registration."""
"""Validate the kvb-skills-tune reminder hook and its settings registration."""
from __future__ import annotations
@@ -10,8 +10,8 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "autotune" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "autotune" / "hooks" / "reminder.sh"
SKILL = REPO_ROOT / "skills" / "kvb-skills-tune" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "kvb-skills-tune" / "hooks" / "reminder.sh"
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
@@ -33,7 +33,7 @@ def _run_hook(stamp: Path, interval: str = "3600") -> subprocess.CompletedProces
class TestAutotuneSkill(unittest.TestCase):
def test_skill_is_trigger_only(self):
text = SKILL.read_text(encoding="utf-8")
self.assertIn("name: autotune", text)
self.assertIn("name: kvb-skills-tune", text)
self.assertIn("multiSelect: false", text)
@@ -51,7 +51,7 @@ class TestSkillPersistence(unittest.TestCase):
self.assertIn(target, text)
def test_autoskill_routes_to_the_same_persistence(self):
text = (REPO_ROOT / "skills" / "autoskill" / "SKILL.md").read_text(
text = (REPO_ROOT / "skills" / "kvb-skills-auto" / "SKILL.md").read_text(
encoding="utf-8"
)
for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"):
@@ -66,7 +66,7 @@ class TestAutotuneHook(unittest.TestCase):
def test_first_run_hints_and_stamps(self):
result = _run_hook(self.stamp)
self.assertIn("/autotune", result.stdout)
self.assertIn("/kvb-skills-tune", result.stdout)
self.assertTrue(self.stamp.is_file())
def test_second_run_is_silent_within_interval(self):
@@ -75,12 +75,12 @@ class TestAutotuneHook(unittest.TestCase):
def test_hint_returns_after_the_interval(self):
_run_hook(self.stamp)
self.assertIn("/autotune", _run_hook(self.stamp, interval="0").stdout)
self.assertIn("/kvb-skills-tune", _run_hook(self.stamp, interval="0").stdout)
def test_corrupt_stamp_does_not_crash(self):
self.stamp.parent.mkdir(parents=True, exist_ok=True)
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8")
self.assertIn("/autotune", _run_hook(self.stamp).stdout)
self.assertIn("/kvb-skills-tune", _run_hook(self.stamp).stdout)
@unittest.skipUnless(shutil.which("node"), "node not installed")
@@ -100,7 +100,7 @@ class TestSettingsRegistration(unittest.TestCase):
self._patch()
data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-skills-tune/hooks/reminder.sh"), 1)
def test_existing_hooks_preserved(self):
self.settings.write_text(
+3 -3
View File
@@ -11,8 +11,8 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CI_FAILURES = REPO_ROOT / "skills" / "ci-artifacts" / "scripts" / "ci_failures.py"
FIND_FIX = REPO_ROOT / "skills" / "merge-fix" / "scripts" / "find_fix.py"
CI_FAILURES = REPO_ROOT / "skills" / "kvb-ci-artifacts" / "scripts" / "ci_failures.py"
FIND_FIX = REPO_ROOT / "skills" / "kvb-git-merge-fix" / "scripts" / "find_fix.py"
def _load(path: Path):
@@ -55,7 +55,7 @@ class TestCiFailures(unittest.TestCase):
"Ran 41 tests in 1.0s\n"
"OK\n"
"UP017 [*] Use `datetime.UTC` alias\n"
" --> skills/democracy/scripts/council.py:92:40\n"
" --> skills/kvb-org-organize/scripts/council.py:92:40\n"
"Found 1 error.\n"
"make: *** [Makefile:15: test] Error 1\n"
"##[error]Process completed with exit code 2.\n"
+2 -2
View File
@@ -1,4 +1,4 @@
"""Exercise the democracy council script: election, staleness, release, gate."""
"""Exercise the kvb-org-organize council script: election, staleness, release, gate."""
from __future__ import annotations
@@ -10,7 +10,7 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
COUNCIL = REPO_ROOT / "skills" / "democracy" / "scripts" / "council.py"
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
class TestCouncil(unittest.TestCase):
+59 -16
View File
@@ -10,8 +10,8 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "dream" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "dream" / "hooks" / "reminder.sh"
SKILL = REPO_ROOT / "skills" / "kvb-skills-dream" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "kvb-skills-dream" / "hooks" / "reminder.sh"
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
@@ -38,7 +38,7 @@ def _run_hook(
class TestDreamSkill(unittest.TestCase):
def test_skill_is_trigger_only(self):
text = SKILL.read_text(encoding="utf-8")
self.assertIn("name: dream", text)
self.assertIn("name: kvb-skills-dream", text)
self.assertIn("multiSelect: false", text)
@@ -51,7 +51,7 @@ class TestDreamHook(unittest.TestCase):
def test_first_run_hints_and_stamps(self):
result = _run_hook(self.cwd, self.stamp)
self.assertIn("/dream", result.stdout)
self.assertIn("/kvb-skills-dream", result.stdout)
self.assertTrue(self.stamp.is_file())
def test_second_run_is_silent_within_interval(self):
@@ -60,12 +60,12 @@ class TestDreamHook(unittest.TestCase):
def test_hint_returns_after_the_interval(self):
_run_hook(self.cwd, self.stamp)
self.assertIn("/dream", _run_hook(self.cwd, self.stamp, interval="0").stdout)
self.assertIn("/kvb-skills-dream", _run_hook(self.cwd, self.stamp, interval="0").stdout)
def test_corrupt_stamp_does_not_crash(self):
self.stamp.parent.mkdir(parents=True, exist_ok=True)
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8")
self.assertIn("/dream", _run_hook(self.cwd, self.stamp).stdout)
self.assertIn("/kvb-skills-dream", _run_hook(self.cwd, self.stamp).stdout)
def test_outside_a_repository_it_stays_silent(self):
self.assertEqual(_run_hook(self.cwd).stdout, "")
@@ -78,7 +78,7 @@ class TestDreamHook(unittest.TestCase):
subprocess.run(
["git", "init", "-q", str(repo)], check=True, capture_output=True
)
self.assertIn("/dream", _run_hook(repo).stdout)
self.assertIn("/kvb-skills-dream", _run_hook(repo).stdout)
self.assertTrue((repo / ".git" / ".dream-stamp").is_file())
self.assertEqual(_run_hook(repo).stdout, "")
@@ -103,7 +103,7 @@ class TestDreamHook(unittest.TestCase):
subprocess.run(
["git", *args], check=True, capture_output=True, env=env, cwd=str(repo)
)
self.assertIn("/dream", _run_hook(repo).stdout)
self.assertIn("/kvb-skills-dream", _run_hook(repo).stdout)
self.assertEqual(_run_hook(tree).stdout, "")
@@ -124,16 +124,59 @@ class TestSettingsRegistration(unittest.TestCase):
self._patch()
data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-skills-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-skills-tune/hooks/reminder.sh"), 1)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("democracy/scripts/council.py"), 1)
self.assertEqual(gate.count("democracy/scripts/watch.py"), 1)
self.assertEqual(entries.count("democracy/scripts/watch.py"), 1)
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
post = json.dumps(data["hooks"]["PostToolUse"])
self.assertEqual(post.count("democracy/scripts/watch.py"), 1)
self.assertEqual(gate.count("subagent-model/scripts/log.py"), 1)
self.assertEqual(post.count("subagent-model/scripts/log.py"), 1)
self.assertEqual(post.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1)
def test_hooks_of_renamed_skills_are_repointed(self):
base = Path(self.tmp.name) / "skills"
def hook(command):
return {"hooks": [{"type": "command", "command": command}]}
self.settings.write_text(
json.dumps(
{
"hooks": {
"UserPromptSubmit": [
hook(f'bash "{base}/autotune/hooks/reminder.sh"'),
hook(f'bash "{base}/dream/hooks/reminder.sh"'),
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
hook("echo keep"),
],
"PreToolUse": [
hook(f'python3 "{base}/democracy/scripts/council.py" gate'),
hook(f'python3 "{base}/democracy/scripts/watch.py" block'),
hook(f'python3 "{base}/subagent-model/scripts/log.py" spawn'),
],
"PostToolUse": [
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
hook(f'python3 "{base}/subagent-model/scripts/log.py" persist'),
],
}
}
),
encoding="utf-8",
)
data = self._patch()
text = json.dumps(data["hooks"])
for old in ("/autotune/", "/dream/", "/democracy/", "/subagent-model/"):
self.assertNotIn(f"skills{old}", text)
self.assertIn("echo keep", text)
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-skills-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-skills-tune/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
if __name__ == "__main__":
+67
View File
@@ -0,0 +1,67 @@
"""Pin every first-party skill name to kvb-<category>-<name> and the Agent Skills name rules."""
from __future__ import annotations
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills"
README = REPO_ROOT / "README.md"
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skills")
UNCATEGORIZED = frozenset({"kvb-cleanup", "kvb-help"})
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
NAME_LINE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
ORGANIGRAM_EDGE = re.compile(r"^\s*(\w+) --> \w+\[(kvb-[a-z0-9-]+)\]\s*$", re.MULTILINE)
class TestSkillNames(unittest.TestCase):
def setUp(self):
self.names = sorted(p.name for p in SKILLS.iterdir() if p.is_dir())
def test_name_field_matches_the_directory(self):
for name in self.names:
with self.subTest(skill=name):
text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8")
match = NAME_LINE.search(text)
self.assertIsNotNone(match, "SKILL.md has no name: line")
self.assertEqual(match.group(1), name)
def test_names_follow_the_agent_skills_rules(self):
for name in self.names:
with self.subTest(skill=name):
self.assertRegex(name, SPEC)
self.assertLessEqual(len(name), 64)
self.assertNotRegex(name, "anthropic|claude")
def test_names_use_a_listed_category(self):
for name in self.names:
if name in UNCATEGORIZED:
continue
with self.subTest(skill=name):
self.assertRegex(name, SCHEMA)
def test_every_category_and_exception_is_in_use(self):
for category in CATEGORIES:
with self.subTest(category=category):
self.assertTrue(
any(n.startswith(f"kvb-{category}-") for n in self.names),
f"category {category} has no skill",
)
self.assertLessEqual(UNCATEGORIZED, set(self.names))
def test_readme_organigram_places_every_skill_under_its_category(self):
text = README.read_text(encoding="utf-8")
section = text.split("## Organigram", 1)[1].split("\n## ", 1)[0]
edges = ORGANIGRAM_EDGE.findall(section)
self.assertEqual(sorted(name for _, name in edges), self.names)
for parent, name in edges:
with self.subTest(skill=name):
expected = "none" if name in UNCATEGORIZED else name.split("-")[1]
self.assertEqual(parent, expected)
if __name__ == "__main__":
unittest.main()
+5 -5
View File
@@ -1,4 +1,4 @@
"""The self-improving skills must share the objective held by autotune."""
"""The self-improving skills must share the objective held by kvb-skills-tune."""
from __future__ import annotations
@@ -7,12 +7,12 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills"
ROUTERS = ("autoskill", "confidence")
ROUTERS = ("kvb-skills-auto", "kvb-check-confidence")
class TestOptimizeObjective(unittest.TestCase):
def test_autotune_names_the_three_axes(self):
text = (SKILLS / "autotune" / "SKILL.md").read_text(encoding="utf-8")
text = (SKILLS / "kvb-skills-tune" / "SKILL.md").read_text(encoding="utf-8")
self.assertIn("## Objective", text)
for axis in ("**Tokens**", "**Time**", "**Quality**"):
self.assertIn(axis, text)
@@ -21,9 +21,9 @@ class TestOptimizeObjective(unittest.TestCase):
for skill in ROUTERS:
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
self.assertIn(
"`autotune` skill's **Objective**",
"`kvb-skills-tune` skill's **Objective**",
text,
f"{skill} does not route to the autotune objective",
f"{skill} does not route to the kvb-skills-tune objective",
)
+3 -3
View File
@@ -1,4 +1,4 @@
"""Validate the shortcuts skill table: rows present, sorted, ``soc``-prefixed,
"""Validate the kvb-skills-shortcuts skill table: rows present, sorted, ``soc``-prefixed,
no two adjacent consonants."""
from __future__ import annotations
@@ -8,7 +8,7 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "shortcuts" / "SKILL.md"
SKILL = REPO_ROOT / "skills" / "kvb-skills-shortcuts" / "SKILL.md"
ROW_RE = re.compile(r"^\|\s*`(?P<shortcut>[^`]+)`\s*\|")
VOWELS = frozenset("aeiou")
@@ -37,7 +37,7 @@ class TestShortcuts(unittest.TestCase):
]
def test_shortcuts_present(self):
self.assertTrue(self.shortcuts, "no shortcut rows found in shortcuts skill")
self.assertTrue(self.shortcuts, "no shortcut rows found in kvb-skills-shortcuts skill")
def test_shortcuts_sorted_ascending(self):
self.assertEqual(
+5 -5
View File
@@ -1,4 +1,4 @@
"""Exercise the subagent-model log: spawn hook, feedback, sandbox persist, report."""
"""Exercise the kvb-org-perform log: spawn hook, feedback, sandbox persist, report."""
from __future__ import annotations
@@ -10,7 +10,7 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
LOG_PY = REPO_ROOT / "skills" / "subagent-model" / "scripts" / "log.py"
LOG_PY = REPO_ROOT / "skills" / "kvb-org-perform" / "scripts" / "log.py"
class TestSubagentModelLog(unittest.TestCase):
@@ -66,13 +66,13 @@ class TestSubagentModelLog(unittest.TestCase):
def test_sandboxed_feedback_is_printed_and_stored_by_the_persist_hook(self):
stdout = self._pending_feedback()
self._persist("python3 /x/skills/subagent-model/scripts/log.py feedback --description 'd'", stdout)
self._persist("python3 /x/skills/kvb-org-perform/scripts/log.py feedback --description 'd'", stdout)
self.assertEqual(self._lines()[0]["kind"], "feedback")
def test_persist_rejects_pending_lines_it_did_not_get_from_a_bare_log_call(self):
stdout = self._pending_feedback()
line = json.loads(stdout[len("pending: "):])
call = "/x/skills/subagent-model/scripts/log.py feedback --description d"
call = "/x/skills/kvb-org-perform/scripts/log.py feedback --description d"
for command, output, session in (
("cat ci.log", stdout, "s1"),
(f"{call}; cat ci.log", stdout, "s1"),
@@ -82,7 +82,7 @@ class TestSubagentModelLog(unittest.TestCase):
(call, stdout + stdout, "s1"),
(call, "pending: " + json.dumps({**line, "extra": 1}), "s1"),
(call, "pending: " + json.dumps({**line, "completed": "yes"}), "s1"),
("/x/skills/subagent-model/scripts/log.py followup --description d", stdout, "s1"),
("/x/skills/kvb-org-perform/scripts/log.py followup --description d", stdout, "s1"),
):
self._persist(command, output, session)
self.assertFalse(self.log.exists())
+2 -2
View File
@@ -1,4 +1,4 @@
"""Exercise the democracy resource watch: stages, projection, block, admit, alerts, caches."""
"""Exercise the kvb-org-organize resource watch: stages, projection, block, admit, alerts, caches."""
from __future__ import annotations
@@ -13,7 +13,7 @@ import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
WATCH = REPO_ROOT / "skills" / "democracy" / "scripts" / "watch.py"
WATCH = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "watch.py"
LIMITS = {"warn": 40, "stop": 15, "critical": 12}