From 751488f4a219994446f02a09355a3f6e6bdbb582 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 10 Jul 2026 14:59:17 +0200 Subject: [PATCH] feat(skills): rename shortcuts skill and auto-enable caveman/ponytail on install Rename the portable conversation-shortcut skill from agent-shortcuts to shortcuts and re-prefix every token to soc* (sobu, soco, socobu, socon, sona, sote) so each token mirrors its command and never places two consonants next to each other. make install and make project now also register the caveman and ponytail marketplaces and enable both plugins in the target's .claude/settings.json so their SessionStart hooks activate every session; the merge is non-destructive, idempotent, and leaves an unparseable settings.json untouched. - skills/shortcuts/SKILL.md: new name and soc* token table - tests/test_shortcuts.py: assert present, sorted, soc-prefixed, no cluster - remove skills/agent-shortcuts and tests/test_agent_shortcuts.py - scripts/enable-plugins.js: node JSON merge helper - scripts/install.sh: run the helper after copying skills - README.md: document the auto-enable behavior Co-Authored-By: Claude Opus 4.8 --- README.md | 2 ++ scripts/enable-plugins.js | 42 ++++++++++++++++++++++++++++ scripts/install.sh | 8 ++++++ skills/shortcuts/SKILL.md | 19 +++++++++++++ tests/test_shortcuts.py | 59 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 scripts/enable-plugins.js create mode 100644 skills/shortcuts/SKILL.md create mode 100644 tests/test_shortcuts.py diff --git a/README.md b/README.md index d305e57..c1fbae9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Copy the skills into a specific project (`/.agents/skills` and `/.cl make project TARGET=/path/to/repo ``` +Both commands also enable the caveman and ponytail plugins in the target's `.claude/settings.json` so their modes auto-activate on session start; existing settings are preserved. + Restart your agent afterwards so it loads the new skills. ## Maintenance 🔧 diff --git a/scripts/enable-plugins.js b/scripts/enable-plugins.js new file mode 100644 index 0000000..0ccf901 --- /dev/null +++ b/scripts/enable-plugins.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// Enable the caveman and ponytail plugins in a Claude Code settings.json so +// their SessionStart hooks auto-activate both modes. Merges non-destructively: +// existing marketplaces and enabled plugins are preserved, unparseable files +// are left untouched. +"use strict"; + +const fs = require("fs"); + +const settingsPath = process.argv[2]; +if (!settingsPath) { + console.error("usage: enable-plugins.js "); + process.exit(2); +} + +const MARKETPLACES = { + caveman: { source: { source: "github", repo: "JuliusBrussee/caveman" } }, + ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } }, +}; +const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true }; + +let data = {}; +if (fs.existsSync(settingsPath)) { + const raw = fs.readFileSync(settingsPath, "utf8").trim(); + if (raw) { + try { + data = JSON.parse(raw); + } catch (err) { + console.error(`skills: ${settingsPath} is not valid JSON; leaving it untouched (${err.message}).`); + process.exit(0); + } + } +} + +data.extraKnownMarketplaces = data.extraKnownMarketplaces || {}; +for (const [name, entry] of Object.entries(MARKETPLACES)) { + if (!data.extraKnownMarketplaces[name]) data.extraKnownMarketplaces[name] = entry; +} +data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); + +fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n"); +console.log(`skills: enabled caveman + ponytail plugins in ${settingsPath}`); diff --git a/scripts/install.sh b/scripts/install.sh index 535db01..ff99ef7 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -44,4 +44,12 @@ for dst in "${TARGET}/.agents/skills" "${TARGET}/.claude/skills"; do cp -a "${src}/." "${dst}/" done +settings="${TARGET}/.claude/settings.json" +if command -v node &>/dev/null; then + log "skills: auto-enabling caveman + ponytail plugins in ${settings}" + node "${REPO_ROOT}/scripts/enable-plugins.js" "${settings}" +else + warn "skills: node not found; skipped auto-enabling caveman/ponytail plugins." +fi + log "skills: done. Restart your agent to load the skills." diff --git a/skills/shortcuts/SKILL.md b/skills/shortcuts/SKILL.md new file mode 100644 index 0000000..e66e585 --- /dev/null +++ b/skills/shortcuts/SKILL.md @@ -0,0 +1,19 @@ +--- +name: shortcuts +description: > + Portable operator conversation shortcuts, reusable across every project. + Trigger when the user's message is exactly one of these tokens (optionally + followed by arguments): sobu, soco, socobu, socon, sona, sote. Expand the + shortcut to the listed meaning and act on the expanded instruction. +--- + +When an operator message is one of the shortcuts below, expand it to the meaning and act on it. + +| Shortcut | Meaning | +| --- | --- | +| `sobu` | Stage next bundle: stage the next logical bundle of verified changes, explain what it does, and list what still remains uncommitted. | +| `soco` | Commit: commit the currently staged changes (pre-commit hooks run). | +| `socobu` | Commit no-verify, then stage next bundle: run `socon` followed by `sobu`. | +| `socon` | Commit no-verify: commit the currently staged changes with `--no-verify`. | +| `sona` | New alias: add the shortcut the operator states to the appropriate table, keeping it sorted. | +| `sote` | Run `make test`. | diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py new file mode 100644 index 0000000..8864aa6 --- /dev/null +++ b/tests/test_shortcuts.py @@ -0,0 +1,59 @@ +"""Validate the shortcuts skill table: rows present, sorted, ``soc``-prefixed, +no two adjacent consonants.""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SKILL = REPO_ROOT / "skills" / "shortcuts" / "SKILL.md" + +ROW_RE = re.compile(r"^\|\s*`(?P[^`]+)`\s*\|") +VOWELS = frozenset("aeiou") + + +def _has_consonant_cluster(name: str) -> bool: + prev_consonant = False + for char in name: + if not char.isalpha(): + prev_consonant = False + continue + is_consonant = char.lower() not in VOWELS + if is_consonant and prev_consonant: + return True + prev_consonant = is_consonant + return False + + +class TestShortcuts(unittest.TestCase): + def setUp(self): + self.assertTrue(SKILL.is_file(), f"missing {SKILL}") + self.shortcuts = [ + match.group("shortcut") + for line in SKILL.read_text(encoding="utf-8").splitlines() + if (match := ROW_RE.match(line)) + ] + + def test_shortcuts_present(self): + self.assertTrue(self.shortcuts, "no shortcut rows found in shortcuts skill") + + def test_shortcuts_sorted_ascending(self): + self.assertEqual( + self.shortcuts, + sorted(self.shortcuts), + f"shortcuts table not sorted; expected {sorted(self.shortcuts)}", + ) + + def test_shortcuts_start_with_so(self): + bad = [s for s in self.shortcuts if not s.startswith("so")] + self.assertFalse(bad, f"shortcuts not starting with 'so': {bad}") + + def test_no_consecutive_consonants(self): + bad = [s for s in self.shortcuts if _has_consonant_cluster(s)] + self.assertFalse(bad, f"shortcuts with two adjacent consonants: {bad}") + + +if __name__ == "__main__": + unittest.main()