blob: 82f3b3453f9fbe0c9f65b9902e07bc2e62f16c96 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
#!/usr/bin/env bash
# Validate ruleset structure. Runs from the rulesets repo root.
# Checks:
# - Every .md rule file starts with a top-level heading
# - Every rule file has an 'Applies to:' header
# - Every language CLAUDE.md has a top-level heading
# - Every hook script has a shebang and is executable
# - Every cross-reference to claude-rules/ from a SKILL.md or
# claude-rules/*.md resolves to a real file (catches the install-layout
# drift that the bridge symlink fixes)
set -u
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
errors=0
warn() {
printf ' WARN: %s\n' "$1"
errors=$((errors + 1))
}
check_md_heading() {
local f="$1"
[ -f "$f" ] || return 0
if ! head -1 "$f" | grep -q '^# '; then
warn "$f — missing top-level heading"
fi
}
check_md_applies_to() {
local f="$1"
[ -f "$f" ] || return 0
if ! grep -q '^Applies to:' "$f"; then
warn "$f — missing 'Applies to:' header"
fi
}
check_hook() {
local f="$1"
[ -f "$f" ] || return 0
if ! head -1 "$f" | grep -q '^#!'; then
warn "$f — missing shebang"
fi
if [ ! -x "$f" ]; then
warn "$f — not executable (chmod +x)"
fi
}
check_md_links() {
# Validate cross-references to claude-rules/ — the install-layout problem
# solved by the bridge symlink in `make install`. Doesn't validate
# example file names that skills cite illustratively (e.g. ADR templates,
# arc42 section files), which are intentionally not real source files.
local f="$1"
[ -f "$f" ] || return 0
local dir
dir="$(dirname "$f")"
while IFS= read -r link; do
local url="${link##*\(}"
url="${url%\)}"
case "$url" in
*claude-rules/*) ;;
*) continue ;;
esac
url="${url%%#*}"
url="${url%%\?*}"
local resolved
resolved="$(cd "$dir" 2>/dev/null && readlink -m "$url" 2>/dev/null)"
if [ -z "$resolved" ] || [ ! -e "$resolved" ]; then
warn "$f — broken claude-rules link: $url"
fi
done < <(grep -oE '\[[^]]*\]\([^)]+\)' "$f" 2>/dev/null || true)
}
echo "Linting rulesets in $REPO_ROOT"
# Generic rules
for f in claude-rules/*.md; do
[ -f "$f" ] || continue
check_md_heading "$f"
check_md_applies_to "$f"
done
# Per-language rule files
for rules_dir in languages/*/claude/rules; do
[ -d "$rules_dir" ] || continue
for f in "$rules_dir"/*.md; do
[ -f "$f" ] || continue
check_md_heading "$f"
check_md_applies_to "$f"
done
done
# Per-language CLAUDE.md templates
for claude_md in languages/*/CLAUDE.md; do
[ -f "$claude_md" ] || continue
check_md_heading "$claude_md"
done
# Language-neutral default CLAUDE.md (install-lang's fallback when a bundle ships none)
[ -f languages/default-CLAUDE.md ] && check_md_heading languages/default-CLAUDE.md
# Hook scripts
for h in languages/*/claude/hooks/*.sh languages/*/githooks/*; do
[ -f "$h" ] || continue
check_hook "$h"
done
# Shared install/diff/lint scripts (sanity check)
for s in scripts/*.sh; do
[ -f "$s" ] || continue
check_hook "$s"
done
# Scripts `make install` symlinks onto PATH. Extensionless by convention, so
# they need their own glob — scripts/*.sh above never matched them, which left
# the most exposed shell in the repo as the only shell with no gate over it.
for s in claude-templates/bin/*; do
[ -f "$s" ] || continue
check_hook "$s"
done
# Markdown link validation across rules and skills
for f in claude-rules/*.md */SKILL.md; do
[ -f "$f" ] || continue
check_md_links "$f"
done
echo "---"
if [ "$errors" -eq 0 ]; then
echo "All checks passed."
else
echo "$errors warning(s)."
exit 1
fi
|