@spardutti/claude-skills 2.36.1 → 2.37.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/hooks/gauntlet.sh CHANGED
@@ -28,6 +28,7 @@
28
28
  # GAUNTLET_CODE_EXT="ts|tsx|py" override which extensions count as code
29
29
  # GAUNTLET_DEBUG=1 print the outcome to stderr on every run
30
30
  # GAUNTLET_REQUIRE_TESTS=1 block when a runner matched 0 test files
31
+ # GAUNTLET_SLOW=30 warn the user when a run takes this many seconds
31
32
  #
32
33
  # Setting either command switches the whole repo to explicit mode — auto-detection
33
34
  # is off, and only what you set runs.
@@ -64,8 +65,9 @@ WHY="$MARKER.why"
64
65
 
65
66
  # Records why this run ended, so "green" and "never ran" stop looking identical.
66
67
  quit() {
67
- printf '%s\n' "$1" > "$WHY" 2>/dev/null
68
+ printf '%s (%ss)\n' "$1" "$SECONDS" > "$WHY" 2>/dev/null
68
69
  if [ -n "${GAUNTLET_DEBUG:-}" ]; then printf 'gauntlet: %s\n' "$1" >&2; fi
70
+ [ "$SECONDS" -ge "${GAUNTLET_SLOW:-30}" ] && printf '{"systemMessage":"gauntlet took %ss, target %ss: %s"}\n' "$SECONDS" "${GAUNTLET_SLOW:-30}" "$1"
69
71
  exit 0
70
72
  }
71
73
 
@@ -119,13 +121,8 @@ SKIP_NOTE=""
119
121
 
120
122
  add_gate() { # kind (typecheck|tests), file regex, command
121
123
  LINE=$(printf '%s\t%s\t%s' "$1" "$2" "$3")
122
- if [ "$1" = "typecheck" ]; then
123
- TC_GATES="$TC_GATES$LINE
124
- "
125
- else
126
- TEST_GATES="$TEST_GATES$LINE
127
- "
128
- fi
124
+ if [ "$1" = "typecheck" ]; then TC_GATES="$TC_GATES$LINE"$'\n'
125
+ else TEST_GATES="$TEST_GATES$LINE"$'\n'; fi
129
126
  }
130
127
 
131
128
  JS_EXT='(ts|tsx|js|jsx|mjs|cjs)'
@@ -318,7 +315,7 @@ $FILES
318
315
  --- $FAILED output (last 60 lines) ---
319
316
  $TAIL"
320
317
 
321
- printf '%s\n' "red: the $FAILED gate failed" > "$WHY" 2>/dev/null
318
+ printf '%s\n' "red: the $FAILED gate failed (${SECONDS}s)" > "$WHY" 2>/dev/null
322
319
 
323
320
  # Exit 2 is the only path where a red is visible to the user: its stderr is shown
324
321
  # to them as the reason the turn is continuing. Exit 0 with {"decision":"continue"}
@@ -83,12 +83,30 @@ receipt_key() {
83
83
 
84
84
  # A scoped --baseline replaces only the changed modules' names. Rebuilding the whole
85
85
  # file re-mutated an entire API to accept eleven of them.
86
- baseline_write() { # baseline_write <baseline file> <survivors file> <scope regex, or empty>
86
+ baseline_write() { # baseline_write <baseline file> <survivors file> <scope regex, or empty> [modules]
87
87
  if [ -z "$3" ]; then cp "$2" "$1"; return; fi
88
- grep -vE "^($3)\.x" "$1" | cat - "$2" | sort -u > "$1.new"
88
+ { grep -vE "^($3)\.x" "$1"; cat "$2"; baseline_marked "$1" && printf '# module %s\n' $4; } | sort -u > "$1.new"
89
89
  mv "$1.new" "$1"
90
90
  }
91
91
 
92
+ # A baseline names each module it has run as "# module <name>", so a first run tests only the diff.
93
+ # One with no such line predates them and came from a whole-repo run, so it covers every module.
94
+ baseline_marked() { [ ! -f "$1" ] || grep -q '^# module ' "$1"; }
95
+ baseline_uncovered() { # baseline_uncovered <baseline> <modules>: the ones it has never run
96
+ for m in $2; do baseline_marked "$1" && ! grep -qsx "# module $m" "$1" && echo "$m"; done
97
+ }
98
+
99
+ # A module's survivors on its first run predate this gate, so they are recorded, not charged.
100
+ baseline_first() { # baseline_first <baseline> <survivors file> <modules> <label>; false when none are new
101
+ new=$(baseline_uncovered "$1" "$3"); [ -n "$new" ] || return 1
102
+ re=$(printf '%s\n' $new | sed 's/\./\\./g' | paste -sd'|' -)
103
+ n=$(grep -cE "^($re)\.x" "$2")
104
+ { cat "$1" 2>/dev/null; grep -E "^($re)\.x" "$2"; printf '# module %s\n' $new; } | sort -u > "$1.new"
105
+ mv "$1.new" "$1"
106
+ echo " $4 mutmut — first run for $(echo $new): $n existing survivor(s) recorded in $1."
107
+ echo " Commit it. From here only NEW survivors in these modules fail."
108
+ }
109
+
92
110
  family_of() { # js, py, or any when no manifest says
93
111
  o=$(owner_of "$1")
94
112
  if [ -f "$o/package.json" ]; then echo js
@@ -96,7 +96,7 @@ structure_check() {
96
96
 
97
97
  # Called when nothing else in the diff is gated, so the gate would otherwise pass it.
98
98
  structure_fail() {
99
- echo "ship-gate: FAIL — deal with the findings above, then run this again."
99
+ echo "ship-gate: FAIL in ${SECONDS}s — deal with the findings above, then run this again."
100
100
  echo " To ship anyway: bash .claude/hooks/ship-gate.sh --force"
101
101
  rm -f "$RECEIPT"
102
102
  exit 1
@@ -207,7 +207,7 @@ elif [ -n "$OVER" ]; then
207
207
  else
208
208
  echo "FILE LENGTH — ok, largest is $LARGEST lines"
209
209
  fi
210
-
210
+ [ "$STATUS" = 1 ] && { echo; echo "MUTATION — skipped: the fix changes the files, so it would run again"; structure_fail; }
211
211
  # ---------------------------------------------------------- which project owns
212
212
  # A monorepo holds several projects, each with its own runner and its own
213
213
  # mutation tool. Walk up from each changed file to the nearest manifest.
@@ -256,9 +256,9 @@ for owner in $OWNERS; do
256
256
 
257
257
  base=""; RUN=""; SCOPE_RE=""; GLOBS=""; MODS=""; MLOG="${TMPDIR:-/tmp}/ship-gate-mutmut.$$"
258
258
  if [ "$owner" != "." ]; then base="$owner/"; RUN="cd '$owner' && "; fi
259
- if [ -n "$GAUNTLET_MUTATE" ]; then
260
- CMD="$GAUNTLET_MUTATE"; TOOL="config"
261
- elif [ -f "$base"package.json ] && grep -qs '@stryker-mutator/core' "$base"package.json; then
259
+ PY=$(printf '%s\n' "$OWNED" | grep '\.py$' || true) # a husky-only package.json must not claim a Python diff
260
+ if [ -n "$GAUNTLET_MUTATE" ]; then CMD="$GAUNTLET_MUTATE"; TOOL="config"
261
+ elif [ -z "$PY" ] && grep -qs '@stryker-mutator/core' "$base"package.json; then
262
262
  TOOL="stryker"
263
263
  # From inside the owning package: npx resolves against ITS node_modules, and
264
264
  # stryker.config.json lives there too. A monorepo root usually has neither.
@@ -272,7 +272,7 @@ for owner in $OWNERS; do
272
272
  # the run to the changed hunks, so incremental buys nothing here and costs
273
273
  # a cache that goes stale exactly the way mutmut's did.
274
274
  CMD="${RUN}npx --no-install stryker run --mutate '${FLAGS#,}'"; stryker_page_tests "$base" "$label"
275
- elif [ -f "$base"package.json ]; then
275
+ elif [ -z "$PY" ] && [ -f "$base"package.json ]; then
276
276
  MISSING="$MISSING $label needs Stryker:
277
277
  npm --prefix ${owner} i -D @stryker-mutator/core @stryker-mutator/vitest-runner
278
278
  then ${base}stryker.config.json:
@@ -281,15 +281,14 @@ for owner in $OWNERS; do
281
281
  continue
282
282
  elif [ -n "$(py_mutmut "$base")" ]; then
283
283
  TOOL="mutmut"
284
- # 3.x has no per-line scoping, only fnmatch globs over mutant NAMES, so once a
285
- # baseline exists every run, --baseline included, is scoped to the changed modules.
284
+ # 3.x scopes only by fnmatch globs over mutant NAMES, so every run is scoped to the changed modules.
286
285
  #
287
286
  # `mutmut run` prints 🙁 for a survivor and exits 0 either way — parsing it
288
287
  # reports clean with survivors sitting there, which is a false green and
289
288
  # worse than reporting nothing. `mutmut results` is the readable source:
290
289
  # it prints "<mutant name>: survived" per survivor.
291
290
  M="$(py_mutmut "$base")"
292
- [ -f "$base.mutmut-baseline" ] && MODS=$(printf '%s\n' "$OWNED" \
291
+ MODS=$(printf '%s\n' "$OWNED" \
293
292
  | sed -n "s#^$base\(.*\)\.py\$#\1#p" | tr / . | sed 's#^src\.##' | sort -u)
294
293
  [ -n "$MODS" ] && GLOBS=$(printf " '%s.x*'" $MODS) \
295
294
  && SCOPE_RE=$(printf '%s\n' "$MODS" | sed 's/\./\\./g' | paste -sd'|' -)
@@ -322,7 +321,7 @@ for owner in $OWNERS; do
322
321
  continue
323
322
  fi
324
323
  [ "$TOOL" = mutmut ] && [ "$MODE" = baseline ] && [ -f "$PKEY.out" ] && CMD="cp '$PKEY.log' '$MLOG' 2>/dev/null; cat '$PKEY.out'"
325
- OUT=$(eval "$CMD" 2>&1); RC=$?
324
+ T0=$SECONDS; OUT=$(export FILES="$(printf '%s' "$OWNED" | tr '\n' ' ')" MUTATE_FLAGS="--mutate ${FLAGS#,}"; eval "$CMD" 2>&1); RC=$?; T=$((SECONDS - T0))
326
325
  [ "$TOOL" = mutmut ] && [ $RC -eq 0 ] && { printf '%s\n' "$OUT" > "$PKEY.out"; cp "$MLOG" "$PKEY.log" 2>/dev/null; }
327
326
  [ -n "$SCOPE_RE" ] && [ $RC -eq 0 ] && OUT=$(printf '%s\n' "$OUT" | grep -E "^[[:space:]]*($SCOPE_RE)\.x")
328
327
  # Match a finding, never a summary row. Stryker prints a `# survived` COLUMN
@@ -397,8 +396,9 @@ for owner in $OWNERS; do
397
396
  continue
398
397
  fi
399
398
 
399
+ [ "$MODE" != baseline ] && baseline_first "$BL" "$NOWF" "$MODS" "$label" && [ "$STATUS" = 0 ] && STATUS=2
400
400
  if [ "$MODE" = baseline ]; then
401
- baseline_write "$BL" "$NOWF" "$SCOPE_RE"
401
+ baseline_write "$BL" "$NOWF" "$SCOPE_RE" "$MODS"
402
402
  NOTE=" $label mutmut — baseline set: $(wc -l < "$BL") survivor(s) accepted. Commit $BL."
403
403
  SURVIVED=""
404
404
  elif [ -f "$BL" ]; then
@@ -418,7 +418,7 @@ for owner in $OWNERS; do
418
418
  fi
419
419
 
420
420
  if [ -n "$SURVIVED" ]; then
421
- echo " $label $TOOL — these lines can break and no test notices:"
421
+ echo " $label $TOOL (${T}s) — these lines can break and no test notices:"
422
422
  survivor_list "$SURVIVED" "$TOOL"
423
423
  if [ "$FOUND" -gt 20 ]; then
424
424
  echo " ... $((FOUND - 20)) more not shown. Every file with findings:"
@@ -432,13 +432,13 @@ for owner in $OWNERS; do
432
432
  [ "$FIXED" -gt 0 ] && echo " ($FIXED baselined survivor(s) now killed — --baseline banks them)"
433
433
  STATUS=1
434
434
  elif [ $RC -ne 0 ]; then
435
- echo " $label $TOOL — the run failed:"
435
+ echo " $label $TOOL (${T}s) — the run failed:"
436
436
  printf '%s\n' "$OUT" | tail -12 | sed 's/^/ /'
437
437
  [ "$STATUS" = 0 ] && STATUS=2
438
438
  elif [ -n "$NOTE" ]; then
439
439
  printf '%s\n' "$NOTE"
440
440
  else
441
- echo " $label $TOOL — ok, nothing survived and nothing was uncovered"
441
+ echo " $label $TOOL (${T}s) — ok, nothing survived and nothing was uncovered"
442
442
  [ -n "$PKEY" ] && touch "$PKEY"
443
443
  [ "$FIXED" -gt 0 ] && echo " ($FIXED baselined survivor(s) now killed — --baseline banks them)"
444
444
  fi
@@ -459,12 +459,12 @@ fi
459
459
  # a choice that can be argued for, but it cannot be passed off as this one.
460
460
  echo
461
461
  case $STATUS in
462
- 0) echo "ship-gate: PASS"
462
+ 0) echo "ship-gate: PASS in ${SECONDS}s"
463
463
  printf 'PASS %s\n' "$(date -u +%FT%TZ)" > "$RECEIPT" ;;
464
- 1) echo "ship-gate: FAIL — deal with the findings above, then run this again."
464
+ 1) echo "ship-gate: FAIL in ${SECONDS}s — deal with the findings above, then run this again."
465
465
  echo " To ship anyway: bash .claude/hooks/ship-gate.sh --force"
466
466
  rm -f "$RECEIPT" ;;
467
- 2) echo "ship-gate: UNPROVEN — nothing is wrong, but nothing was proven either"
467
+ 2) echo "ship-gate: UNPROVEN in ${SECONDS}s — nothing is wrong, but nothing was proven either"
468
468
  printf 'UNPROVEN %s\n' "$(date -u +%FT%TZ)" > "$RECEIPT" ;;
469
469
  esac
470
470
  exit $STATUS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.36.1",
3
+ "version": "2.37.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {