mirror of
https://github.com/JuliusBrussee/caveman.git
synced 2026-08-11 13:21:09 +02:00
fix(hooks): SessionStart SKILL.md path off-by-one
Hook lives at <plugin_root>/src/hooks/ but read SKILL.md via a single '..' — resolving to nonexistent src/skills/, so every plugin install silently fell back to the stale hardcoded ruleset (missing language preservation, no-self-reference, intensity table). Resolve via candidates in order: $CLAUDE_PLUGIN_ROOT/skills/, __dirname/../../skills/ (plugin + repo layout), __dirname/../skills/ (standalone $CLAUDE_CONFIG_DIR layout). Sync the two missing rules into the fallback for installs with no SKILL.md at all. Fixes #587, fixes #589
This commit is contained in:
@@ -48,14 +48,32 @@ if (INDEPENDENT_MODES.has(mode)) {
|
||||
const modeLabel = mode === 'wenyan' ? 'wenyan-full' : mode;
|
||||
|
||||
// Read SKILL.md — the single source of truth for caveman behavior.
|
||||
// Plugin installs: __dirname = <plugin_root>/hooks/, SKILL.md at <plugin_root>/skills/caveman/SKILL.md
|
||||
// Standalone installs: __dirname = $CLAUDE_CONFIG_DIR/hooks/, SKILL.md won't exist — falls back to hardcoded rules.
|
||||
// Candidate locations, tried in order (#587/#589 — the old single '..' path
|
||||
// resolved to <plugin_root>/src/skills/, which doesn't exist, so plugin
|
||||
// installs silently used the stale fallback ruleset):
|
||||
// 1. $CLAUDE_PLUGIN_ROOT/skills/caveman/SKILL.md — Claude Code sets
|
||||
// CLAUDE_PLUGIN_ROOT when invoking plugin hooks; authoritative when present.
|
||||
// 2. ../../skills/caveman/SKILL.md — hook at <plugin_root>/src/hooks/
|
||||
// (plugin.json layout) or a repo checkout.
|
||||
// 3. ../skills/caveman/SKILL.md — standalone install with hooks at
|
||||
// $CLAUDE_CONFIG_DIR/hooks/ and the skill at $CLAUDE_CONFIG_DIR/skills/caveman/.
|
||||
// All misses fall through to the hardcoded fallback ruleset below.
|
||||
const skillCandidates = [];
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
skillCandidates.push(path.join(process.env.CLAUDE_PLUGIN_ROOT, 'skills', 'caveman', 'SKILL.md'));
|
||||
}
|
||||
skillCandidates.push(
|
||||
path.join(__dirname, '..', '..', 'skills', 'caveman', 'SKILL.md'),
|
||||
path.join(__dirname, '..', 'skills', 'caveman', 'SKILL.md')
|
||||
);
|
||||
|
||||
let skillContent = '';
|
||||
try {
|
||||
skillContent = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'skills', 'caveman', 'SKILL.md'), 'utf8'
|
||||
);
|
||||
} catch (e) { /* standalone install — will use fallback below */ }
|
||||
for (const candidate of skillCandidates) {
|
||||
try {
|
||||
skillContent = fs.readFileSync(candidate, 'utf8');
|
||||
break;
|
||||
} catch (e) { /* try next candidate */ }
|
||||
}
|
||||
|
||||
let output;
|
||||
|
||||
@@ -101,6 +119,8 @@ if (skillContent) {
|
||||
'## Rules\n\n' +
|
||||
'Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. ' +
|
||||
'Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.\n\n' +
|
||||
"Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. Compress the style, not the language. Technical terms, code, API names, commands, error strings stay verbatim.\n\n" +
|
||||
'No self-reference. Never name or announce the style. No "caveman mode on" tags. Output caveman-only.\n\n' +
|
||||
'Pattern: `[thing] [action] [reason]. [next step].`\n\n' +
|
||||
'Not: "Sure! I\'d be happy to help you with that. The issue you\'re experiencing is likely caused by..."\n' +
|
||||
'Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"\n\n' +
|
||||
|
||||
+59
-1
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -10,10 +11,13 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class HookScriptTests(unittest.TestCase):
|
||||
def run_cmd(self, cmd, home):
|
||||
def run_cmd(self, cmd, home, extra_env=None):
|
||||
env = os.environ.copy()
|
||||
env.pop("CLAUDE_PLUGIN_ROOT", None)
|
||||
env["HOME"] = str(home)
|
||||
env["USERPROFILE"] = str(home)
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
cwd=REPO_ROOT,
|
||||
@@ -156,6 +160,60 @@ class HookScriptTests(unittest.TestCase):
|
||||
self.assertNotIn("STATUSLINE SETUP NEEDED", result.stdout)
|
||||
self.assertEqual((claude_dir / ".caveman-active").read_text(), "full")
|
||||
|
||||
# Regression for #587/#589 — hook at <root>/src/hooks/ must resolve SKILL.md
|
||||
# at <root>/skills/caveman/, not the nonexistent <root>/src/skills/.
|
||||
def test_activate_emits_skill_md_not_fallback_from_repo_layout(self):
|
||||
with tempfile.TemporaryDirectory(prefix="caveman-hooks-skillpath-") as tmp:
|
||||
home = Path(tmp)
|
||||
(home / ".claude").mkdir(parents=True)
|
||||
|
||||
result = self.run_cmd(["node", "src/hooks/caveman-activate.js"], home)
|
||||
|
||||
# Intensity table exists only in SKILL.md, never in the fallback
|
||||
self.assertIn("## Intensity", result.stdout)
|
||||
# Default mode is full — table filtered to the active level's row
|
||||
self.assertIn("| **full** |", result.stdout)
|
||||
self.assertNotIn("| **lite** |", result.stdout)
|
||||
|
||||
def test_activate_finds_skill_beside_config_dir_hooks(self):
|
||||
# Standalone layout: hooks at $CLAUDE_CONFIG_DIR/hooks/, skill installed
|
||||
# at $CLAUDE_CONFIG_DIR/skills/caveman/SKILL.md
|
||||
with tempfile.TemporaryDirectory(prefix="caveman-hooks-standalone-") as tmp:
|
||||
home = Path(tmp)
|
||||
claude_dir = home / ".claude"
|
||||
hooks_dir = claude_dir / "hooks"
|
||||
hooks_dir.mkdir(parents=True)
|
||||
for name in ("caveman-activate.js", "caveman-config.js", "package.json"):
|
||||
shutil.copy(REPO_ROOT / "src" / "hooks" / name, hooks_dir / name)
|
||||
skill_dir = claude_dir / "skills" / "caveman"
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: caveman\n---\nSTANDALONE MARKER RULESET\n"
|
||||
)
|
||||
|
||||
result = self.run_cmd(["node", str(hooks_dir / "caveman-activate.js")], home)
|
||||
|
||||
self.assertIn("STANDALONE MARKER RULESET", result.stdout)
|
||||
|
||||
def test_activate_prefers_claude_plugin_root(self):
|
||||
with tempfile.TemporaryDirectory(prefix="caveman-hooks-pluginroot-") as tmp:
|
||||
home = Path(tmp)
|
||||
(home / ".claude").mkdir(parents=True)
|
||||
plugin_root = home / "plugin-cache"
|
||||
skill_dir = plugin_root / "skills" / "caveman"
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: caveman\n---\nPLUGIN ROOT MARKER RULESET\n"
|
||||
)
|
||||
|
||||
result = self.run_cmd(
|
||||
["node", "src/hooks/caveman-activate.js"],
|
||||
home,
|
||||
extra_env={"CLAUDE_PLUGIN_ROOT": str(plugin_root)},
|
||||
)
|
||||
|
||||
self.assertIn("PLUGIN ROOT MARKER RULESET", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user