@spardutti/claude-skills 2.34.0 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/hooks/ship-gate-projects.sh +14 -0
- package/hooks/ship-gate-structure.sh +103 -0
- package/hooks/ship-gate.sh +7 -9
- package/lib/setup-hook.mjs +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
|
|
|
131
131
|
|
|
132
132
|
| Command | What it does |
|
|
133
133
|
|---------|--------------|
|
|
134
|
-
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
134
|
+
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths and that new files sit in their kind folder, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
135
135
|
| `/discover` | Find the right problem before deciding what to build — diverges first: generates competing framings through blind subagents under forced constraints, stress-tests the winner with a blind critic, and refuses to converge until every open question is answered or deferred, then drafts scope, non-goals, edge cases and success criteria for you to correct. Run before `/plan-feature` |
|
|
136
136
|
| `/plan-feature` | Integration-first feature planning, then building — 3 parallel subagents scan for reusable code, patterns and touch points, grounded clarifying questions follow, and on your go the same agent builds the plan |
|
|
137
137
|
| `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
|
|
@@ -191,7 +191,7 @@ commands/ Slash commands installed to .claude/commands/
|
|
|
191
191
|
agents/ Subagent definitions — commands declare which they need via requires-agents
|
|
192
192
|
scripts/ validate-skills.mjs — checks skill length caps and reference integrity
|
|
193
193
|
gauntlet.sh — the Stop-hook verification gates, embedded by the CLI
|
|
194
|
-
ship-gate.sh — /ship's file-length and mutation checks, behind an exit code
|
|
194
|
+
ship-gate.sh — /ship's file-length, folder-structure and mutation checks, behind an exit code
|
|
195
195
|
ship-gate-hook.sh — refuses gh pr create/merge without a ship-gate receipt
|
|
196
196
|
version-check.sh — SessionStart nudge when a newer catalog is published
|
|
197
197
|
gauntlet-selftest.sh — behavioural tests for the hooks (runs on pre-push)
|
|
@@ -105,3 +105,17 @@ project_key() { # project_key <owner> <tool>
|
|
|
105
105
|
case "$(family_of "$f")" in "$fam"|any) printf '%s\n' "$f"; [ -f "$f" ] && cat "$f" ;; esac
|
|
106
106
|
done; } | git hash-object --stdin
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
# A page test renders a whole screen, so it "kills" mutants in every util that screen calls.
|
|
110
|
+
# Backoffice ran 23 minutes that way, and hid 104 mutants no logic test checked.
|
|
111
|
+
stryker_page_tests() { # stryker_page_tests <base> <label>
|
|
112
|
+
grep -qs configFile "$1"stryker.conf* && return 0
|
|
113
|
+
pages=$(git ls-files -co --exclude-standard -- "${1:-.}" | grep -E '\.test\.[jt]sx$' \
|
|
114
|
+
| grep -vE '/(hooks|queries)/|(^|/)\.stryker-tmp/' | head -3)
|
|
115
|
+
[ -n "$pages" ] || return 0
|
|
116
|
+
echo " $2 stryker — UNPROVEN: page tests count as proof for logic, e.g."
|
|
117
|
+
printf '%s\n' "$pages" | sed 's/^/ /'
|
|
118
|
+
echo " Give Stryker a Vitest config that runs logic tests only — see"
|
|
119
|
+
echo " testing-best-practices/MUTATION-TESTING.md, \"Keep Page Tests Out Of The Run\"."
|
|
120
|
+
[ "$STATUS" = 0 ] && STATUS=2
|
|
121
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced by ship-gate.sh: a new file must sit in the folder named for its kind.
|
|
3
|
+
# Only ADDED files are judged, so a repo laid out the old way cannot get worse and need not move.
|
|
4
|
+
|
|
5
|
+
STRUCTURE_JS_KINDS="components|hooks|api|queries|schemas|types|utils|stores|test"
|
|
6
|
+
STRUCTURE_KIND_FILE='(^|[._])(router|routes|controller|service|schema|model)s?\.[cm]?[jt]s$|(Router|Routes|Controller|Service|Schema|Model)s?\.[cm]?[jt]s$'
|
|
7
|
+
|
|
8
|
+
# --no-renames: a move must show up as an add, or renaming a misplaced file would slip through.
|
|
9
|
+
added_files() {
|
|
10
|
+
{ git diff --no-renames --diff-filter=A --name-only "$BASE"...HEAD 2>/dev/null
|
|
11
|
+
git diff --no-renames --diff-filter=A --name-only HEAD 2>/dev/null
|
|
12
|
+
git ls-files --others --exclude-standard 2>/dev/null; } | sort -u
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
has_subject() { # has_subject <test path>: a file the test is named for, beside it or above its __tests__/
|
|
16
|
+
d=$(dirname "$1")
|
|
17
|
+
[ "${d##*/}" = __tests__ ] && d=$(dirname "$d")
|
|
18
|
+
stem=$(basename "$1" | sed -E 's/\.(test|spec)\.[^.]+$//')
|
|
19
|
+
while :; do
|
|
20
|
+
for e in ts tsx js jsx mjs cjs astro; do [ -f "$d/$stem.$e" ] && return 0; done
|
|
21
|
+
case "$stem" in *.*) stem=${stem%.*} ;; *) return 1 ;; esac
|
|
22
|
+
done
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
feature_misplaced() { # feature_misplaced <path below features/<name>/>
|
|
26
|
+
case "$1" in
|
|
27
|
+
index.*) ;;
|
|
28
|
+
*/*) printf '%s' "${1%%/*}" | grep -qxE "$STRUCTURE_JS_KINDS" \
|
|
29
|
+
|| echo "${1%%/*}/ is not a kind folder ($STRUCTURE_JS_KINDS)" ;;
|
|
30
|
+
*) echo "loose at the feature root, where only index.* belongs" ;;
|
|
31
|
+
esac
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
js_misplaced() { # js_misplaced <path> <owner>
|
|
35
|
+
f=$1; b=${f##*/}; parent=$(basename "$(dirname "$f")")
|
|
36
|
+
if printf '%s' "$b" | grep -qE '\.(test|spec)\.[^.]+$'; then
|
|
37
|
+
printf '%s' "/$f" | grep -qE '/(e2e|tests?)/' && return
|
|
38
|
+
has_subject "$f" || echo "a test sits beside the file it tests, and nothing here is named ${b%%.*}"
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
41
|
+
rest=$(printf '%s' "/$f" | sed -nE 's#.*/features/[^/]+/##p')
|
|
42
|
+
if [ -n "$rest" ]; then feature_misplaced "$rest"; return; fi
|
|
43
|
+
kinds="components|hooks|queries|api|schemas|types|utils"
|
|
44
|
+
grep -qs '"express"' "$2/package.json" && kinds="$kinds|routes|controllers|services|middleware|models"
|
|
45
|
+
if printf '%s' "/$f" | grep -qE "/src/($kinds)/"; then
|
|
46
|
+
echo "sorted by kind first; it belongs in <domain>/<kind>/ or shared/<kind>/"
|
|
47
|
+
elif printf '%s' "$b" | grep -qE '^use[A-Z].*\.[jt]sx?$' && ! printf '%s' "/$f" | grep -qE '/(hooks|queries)/'; then
|
|
48
|
+
echo "a hook belongs in hooks/ or queries/"
|
|
49
|
+
elif printf '%s' "$b" | grep -qE "$STRUCTURE_KIND_FILE" \
|
|
50
|
+
&& ! printf '%s' "$parent" | grep -qxE 'routes|controllers|services|schemas|models|api|queries|types'; then
|
|
51
|
+
echo "the kind is in the filename; it belongs in a kind folder"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
py_misplaced() { # py_misplaced <path>
|
|
56
|
+
f=$1; b=${f##*/}; d=$(dirname "$f"); parent=${d##*/}
|
|
57
|
+
case "$b" in
|
|
58
|
+
test_*.py|*_test.py|conftest.py)
|
|
59
|
+
printf '%s' "/$f" | grep -qE '/tests/' || echo "tests live under tests/, mirroring the app"
|
|
60
|
+
return ;;
|
|
61
|
+
esac
|
|
62
|
+
k=$(printf '%s' "$b" | sed -nE 's/^(.*_)?(router|service|schema|model)s?\.py$/\2/p')
|
|
63
|
+
if [ -n "$k" ] && [ "$parent" != "${k}s" ]; then
|
|
64
|
+
echo "the kind is in the filename; it belongs in ${k}s/"
|
|
65
|
+
elif printf '%s' "$parent" | grep -qxE 'routers|services|schemas|models' && [ -f "$(dirname "$d")/main.py" ]; then
|
|
66
|
+
echo "sorted by kind first; it belongs in <domain>/$parent/"
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
structure_check() {
|
|
71
|
+
bad=""; count=0
|
|
72
|
+
while IFS= read -r f; do
|
|
73
|
+
[ -f "$f" ] && ! is_ignored "$f" || continue
|
|
74
|
+
printf '%s' "/$f" | grep -qE '/(node_modules|\.stryker-tmp|mutants)/' && continue
|
|
75
|
+
o=$(owner_of "$f")
|
|
76
|
+
case "$f" in
|
|
77
|
+
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.astro) why=$(js_misplaced "$f" "$o") ;;
|
|
78
|
+
*.py) grep -qis fastapi "$o/pyproject.toml" || continue; why=$(py_misplaced "$f") ;;
|
|
79
|
+
*) continue ;;
|
|
80
|
+
esac
|
|
81
|
+
count=$((count+1))
|
|
82
|
+
[ -n "$why" ] && bad="$bad $f — $why
|
|
83
|
+
"
|
|
84
|
+
done <<< "$(added_files)"
|
|
85
|
+
echo
|
|
86
|
+
if [ -n "$bad" ]; then
|
|
87
|
+
echo "STRUCTURE — new files outside their kind folder:"
|
|
88
|
+
printf '%s' "$bad"
|
|
89
|
+
echo " Move them (Project Structure in the react, fastapi or express skill), or ship with --force."
|
|
90
|
+
echo
|
|
91
|
+
return 1
|
|
92
|
+
fi
|
|
93
|
+
echo "STRUCTURE — ok, $count new file(s) in their kind folder"
|
|
94
|
+
echo
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Called when nothing else in the diff is gated, so the gate would otherwise pass it.
|
|
98
|
+
structure_fail() {
|
|
99
|
+
echo "ship-gate: FAIL — deal with the findings above, then run this again."
|
|
100
|
+
echo " To ship anyway: bash .claude/hooks/ship-gate.sh --force"
|
|
101
|
+
rm -f "$RECEIPT"
|
|
102
|
+
exit 1
|
|
103
|
+
}
|
package/hooks/ship-gate.sh
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
# Exit codes:
|
|
26
26
|
# 0 clean — nothing over the line limit, nothing survived, nothing uncovered
|
|
27
27
|
# 1 findings that must be dealt with before shipping
|
|
28
|
-
# 2 ran, but could not prove the tests (a mutation tool is missing
|
|
29
|
-
# don't block; the output names
|
|
28
|
+
# 2 ran, but could not prove the tests (a mutation tool is missing, or page tests
|
|
29
|
+
# count as proof) — report, don't block; the output names the fix and where
|
|
30
30
|
#
|
|
31
31
|
# Config: the same .claude/gauntlet.conf as the Stop hook.
|
|
32
32
|
# GAUNTLET_MAX_LINES=200 the per-file limit; 0 turns the check off
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
set -uo pipefail
|
|
47
47
|
|
|
48
48
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
49
|
-
. "$HERE/ship-gate-projects.sh" || { echo "ship-gate: cannot read $HERE
|
|
49
|
+
. "$HERE/ship-gate-projects.sh" && . "$HERE/ship-gate-structure.sh" || { echo "ship-gate: cannot read its helpers in $HERE"; exit 1; }
|
|
50
50
|
|
|
51
51
|
MODE=""
|
|
52
52
|
case "${1:-}" in
|
|
@@ -76,7 +76,7 @@ GAUNTLET_MAX_LINES=200
|
|
|
76
76
|
GAUNTLET_MUTATE=""
|
|
77
77
|
GAUNTLET_SOURCE_EXT="ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|gd"
|
|
78
78
|
GAUNTLET_IGNORE_EXT="md|mdx|txt|rst|adoc|jsonc?|ya?ml|toml|lock|cfg|ini|env|csv|tsv|sql|html|css|scss|svg|png|jpg|jpeg|gif|webp|ico|pdf|woff2?|ttf|otf|mp3|mp4|wav|zip|gz|tres|tscn|import|godot"
|
|
79
|
-
GAUNTLET_IGNORE_FILES="*.gen.ts *.gen.tsx *.generated.* */migrations/*.py */alembic/versions/*.py */components/ui/*.tsx */*.config.*"
|
|
79
|
+
GAUNTLET_IGNORE_FILES="*.gen.ts *.gen.tsx *.generated.* */migrations/*.py */alembic/versions/*.py */components/ui/*.tsx */hooks/use-mobile.ts */*.config.*"
|
|
80
80
|
# Length-checked and skill-audited like everything else, but not mutated.
|
|
81
81
|
# Mutation pays on logic and burns time on presentation: a component's mutants
|
|
82
82
|
# are class names, copy and JSX shape, none of which is behaviour, and one
|
|
@@ -159,6 +159,8 @@ if [ -n "$IGNORED" ]; then
|
|
|
159
159
|
printf '%s\n' "$IGNORED" | sed 's/^/ /'
|
|
160
160
|
fi
|
|
161
161
|
|
|
162
|
+
[ -n "$FILES" ] && echo "ship-gate: $(printf '%s\n' "$FILES" | wc -l) changed code file(s), base $(git rev-parse --short "$BASE")"
|
|
163
|
+
STATUS=0; structure_check || { STATUS=1; [ -z "$FILES" ] && structure_fail; }
|
|
162
164
|
if [ -z "$FILES" ]; then
|
|
163
165
|
# "I recognised nothing" is not "there is nothing", and the gate used to report
|
|
164
166
|
# both as a PASS. A Godot repo changed only .gd files, which no extension in
|
|
@@ -183,10 +185,6 @@ if [ -z "$FILES" ]; then
|
|
|
183
185
|
exit 0
|
|
184
186
|
fi
|
|
185
187
|
|
|
186
|
-
echo "ship-gate: $(printf '%s\n' "$FILES" | wc -l) changed code file(s), base $(git rev-parse --short "$BASE")"
|
|
187
|
-
echo
|
|
188
|
-
STATUS=0
|
|
189
|
-
|
|
190
188
|
# ------------------------------------------------- check 1: file length (hard)
|
|
191
189
|
OVER=""
|
|
192
190
|
LARGEST=0
|
|
@@ -273,7 +271,7 @@ for owner in $OWNERS; do
|
|
|
273
271
|
# 8 of them on one commit, all long since killed. --mutate already scopes
|
|
274
272
|
# the run to the changed hunks, so incremental buys nothing here and costs
|
|
275
273
|
# a cache that goes stale exactly the way mutmut's did.
|
|
276
|
-
CMD="${RUN}npx --no-install stryker run --mutate '${FLAGS#,}'"
|
|
274
|
+
CMD="${RUN}npx --no-install stryker run --mutate '${FLAGS#,}'"; stryker_page_tests "$base" "$label"
|
|
277
275
|
elif [ -f "$base"package.json ]; then
|
|
278
276
|
MISSING="$MISSING $label needs Stryker:
|
|
279
277
|
npm --prefix ${owner} i -D @stryker-mutator/core @stryker-mutator/vitest-runner
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const APPLICATION_GATE_FILENAME = "skill-application-gate.sh";
|
|
|
27
27
|
const GAUNTLET_FILENAME = "gauntlet.sh";
|
|
28
28
|
const SHIP_GATE_FILENAME = "ship-gate.sh";
|
|
29
29
|
const SHIP_GATE_PROJECTS_FILENAME = "ship-gate-projects.sh";
|
|
30
|
+
const SHIP_GATE_STRUCTURE_FILENAME = "ship-gate-structure.sh";
|
|
30
31
|
const SHIP_GATE_HOOK_FILENAME = "ship-gate-hook.sh";
|
|
31
32
|
const VERSION_CHECK_FILENAME = "version-check.sh";
|
|
32
33
|
const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
|
|
@@ -106,6 +107,7 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
106
107
|
GAUNTLET_FILENAME,
|
|
107
108
|
SHIP_GATE_FILENAME,
|
|
108
109
|
SHIP_GATE_PROJECTS_FILENAME,
|
|
110
|
+
SHIP_GATE_STRUCTURE_FILENAME,
|
|
109
111
|
SHIP_GATE_HOOK_FILENAME,
|
|
110
112
|
VERSION_CHECK_FILENAME,
|
|
111
113
|
]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spardutti/claude-skills",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.0",
|
|
4
4
|
"description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"claude-skills": "bin/cli.mjs"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"prepack": "cp ../README.md README.md && mkdir -p hooks && cp ../scripts/skill-gate.sh ../scripts/skill-gate-automark.sh ../scripts/skill-application-gate.sh ../scripts/gauntlet.sh ../scripts/ship-gate.sh ../scripts/ship-gate-projects.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh hooks/",
|
|
14
|
+
"prepack": "cp ../README.md README.md && mkdir -p hooks && cp ../scripts/skill-gate.sh ../scripts/skill-gate-automark.sh ../scripts/skill-application-gate.sh ../scripts/gauntlet.sh ../scripts/ship-gate.sh ../scripts/ship-gate-projects.sh ../scripts/ship-gate-structure.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh hooks/",
|
|
15
15
|
"postpack": "git checkout -- README.md && rm -rf hooks"
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|