@spardutti/claude-skills 2.34.0 → 2.35.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 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)
@@ -0,0 +1,102 @@
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, in the same folder
16
+ d=$(dirname "$1")
17
+ stem=$(basename "$1" | sed -E 's/\.(test|spec)\.[^.]+$//')
18
+ while :; do
19
+ for e in ts tsx js jsx mjs cjs astro; do [ -f "$d/$stem.$e" ] && return 0; done
20
+ case "$stem" in *.*) stem=${stem%.*} ;; *) return 1 ;; esac
21
+ done
22
+ }
23
+
24
+ feature_misplaced() { # feature_misplaced <path below features/<name>/>
25
+ case "$1" in
26
+ index.*) ;;
27
+ */*) printf '%s' "${1%%/*}" | grep -qxE "$STRUCTURE_JS_KINDS" \
28
+ || echo "${1%%/*}/ is not a kind folder ($STRUCTURE_JS_KINDS)" ;;
29
+ *) echo "loose at the feature root, where only index.* belongs" ;;
30
+ esac
31
+ }
32
+
33
+ js_misplaced() { # js_misplaced <path> <owner>
34
+ f=$1; b=${f##*/}; parent=$(basename "$(dirname "$f")")
35
+ if printf '%s' "$b" | grep -qE '\.(test|spec)\.[^.]+$'; then
36
+ printf '%s' "/$f" | grep -qE '/(e2e|tests?)/' && return
37
+ has_subject "$f" || echo "a test sits beside the file it tests, and nothing here is named ${b%%.*}"
38
+ return
39
+ fi
40
+ rest=$(printf '%s' "/$f" | sed -nE 's#.*/features/[^/]+/##p')
41
+ if [ -n "$rest" ]; then feature_misplaced "$rest"; return; fi
42
+ kinds="components|hooks|queries|api|schemas|types|utils"
43
+ grep -qs '"express"' "$2/package.json" && kinds="$kinds|routes|controllers|services|middleware|models"
44
+ if printf '%s' "/$f" | grep -qE "/src/($kinds)/"; then
45
+ echo "sorted by kind first; it belongs in <domain>/<kind>/ or shared/<kind>/"
46
+ elif printf '%s' "$b" | grep -qE '^use[A-Z].*\.[jt]sx?$' && ! printf '%s' "/$f" | grep -qE '/(hooks|queries)/'; then
47
+ echo "a hook belongs in hooks/ or queries/"
48
+ elif printf '%s' "$b" | grep -qE "$STRUCTURE_KIND_FILE" \
49
+ && ! printf '%s' "$parent" | grep -qxE 'routes|controllers|services|schemas|models|api|queries|types'; then
50
+ echo "the kind is in the filename; it belongs in a kind folder"
51
+ fi
52
+ }
53
+
54
+ py_misplaced() { # py_misplaced <path>
55
+ f=$1; b=${f##*/}; d=$(dirname "$f"); parent=${d##*/}
56
+ case "$b" in
57
+ test_*.py|*_test.py|conftest.py)
58
+ printf '%s' "/$f" | grep -qE '/tests/' || echo "tests live under tests/, mirroring the app"
59
+ return ;;
60
+ esac
61
+ k=$(printf '%s' "$b" | sed -nE 's/^(.*_)?(router|service|schema|model)s?\.py$/\2/p')
62
+ if [ -n "$k" ] && [ "$parent" != "${k}s" ]; then
63
+ echo "the kind is in the filename; it belongs in ${k}s/"
64
+ elif printf '%s' "$parent" | grep -qxE 'routers|services|schemas|models' && [ -f "$(dirname "$d")/main.py" ]; then
65
+ echo "sorted by kind first; it belongs in <domain>/$parent/"
66
+ fi
67
+ }
68
+
69
+ structure_check() {
70
+ bad=""; count=0
71
+ while IFS= read -r f; do
72
+ [ -f "$f" ] && ! is_ignored "$f" || continue
73
+ printf '%s' "/$f" | grep -qE '/(node_modules|\.stryker-tmp|mutants)/' && continue
74
+ o=$(owner_of "$f")
75
+ case "$f" in
76
+ *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.astro) why=$(js_misplaced "$f" "$o") ;;
77
+ *.py) grep -qis fastapi "$o/pyproject.toml" || continue; why=$(py_misplaced "$f") ;;
78
+ *) continue ;;
79
+ esac
80
+ count=$((count+1))
81
+ [ -n "$why" ] && bad="$bad $f — $why
82
+ "
83
+ done <<< "$(added_files)"
84
+ echo
85
+ if [ -n "$bad" ]; then
86
+ echo "STRUCTURE — new files outside their kind folder:"
87
+ printf '%s' "$bad"
88
+ echo " Move them (Project Structure in the react, fastapi or express skill), or ship with --force."
89
+ echo
90
+ return 1
91
+ fi
92
+ echo "STRUCTURE — ok, $count new file(s) in their kind folder"
93
+ echo
94
+ }
95
+
96
+ # Called when nothing else in the diff is gated, so the gate would otherwise pass it.
97
+ structure_fail() {
98
+ echo "ship-gate: FAIL — deal with the findings above, then run this again."
99
+ echo " To ship anyway: bash .claude/hooks/ship-gate.sh --force"
100
+ rm -f "$RECEIPT"
101
+ exit 1
102
+ }
@@ -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/ship-gate-projects.sh"; exit 1; }
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
@@ -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.34.0",
3
+ "version": "2.35.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": {