@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 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
+ }
@@ -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) — report,
29
- # don't block; the output names exactly what to install and where
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/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
@@ -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
@@ -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.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": {