@spardutti/claude-skills 2.36.0 → 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 +6 -9
- package/hooks/ship-gate-projects.sh +26 -4
- package/hooks/ship-gate-structure.sh +1 -1
- package/hooks/ship-gate.sh +16 -16
- package/package.json +1 -1
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
|
-
|
|
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"}
|
|
@@ -36,12 +36,16 @@ owner_of() {
|
|
|
36
36
|
# Blaming an unreachable database for that sent one agent chasing Docker for 20 minutes.
|
|
37
37
|
mutmut_unchecked() { # mutmut_unchecked <label> <base> <count> <run log>
|
|
38
38
|
echo " $1 mutmut — UNPROVEN: $3 mutant(s) were never run."
|
|
39
|
-
|
|
39
|
+
# "Failed to run pytest with args" is one line carrying every selected test id — 20KB of
|
|
40
|
+
# them on one repo — so the line is cut. Unlisted, the gate blamed a database instead.
|
|
41
|
+
stop='Failed to run clean test|failed to collect stats|Failed to collect list of tests|Stopping early|Failed to run pytest with args|BadTestExecutionCommandsException'
|
|
40
42
|
if grep -aqsE "$stop" "$4"; then
|
|
41
43
|
echo " mutmut stopped before testing a single mutant:"
|
|
42
|
-
grep -aE "^(FAILED|ERROR) |^[A-Za-z]*Error: |$stop" "$4" | tail -8 | sed 's/^/ /'
|
|
44
|
+
grep -aE "^(FAILED|ERROR) |^[A-Za-z]*Error: |$stop" "$4" | tail -8 | cut -c1-200 | sed 's/^/ /'
|
|
43
45
|
echo " It runs the whole suite, then these tests again in one process, so a"
|
|
44
46
|
echo " test that passes alone can fail here on state an earlier test left."
|
|
47
|
+
echo " mutmut hides pytest's own error: set debug = true under [tool.mutmut]"
|
|
48
|
+
echo " and run it again to read it."
|
|
45
49
|
return
|
|
46
50
|
fi
|
|
47
51
|
echo " They are recorded \"not checked\", so the suite proved nothing"
|
|
@@ -79,12 +83,30 @@ receipt_key() {
|
|
|
79
83
|
|
|
80
84
|
# A scoped --baseline replaces only the changed modules' names. Rebuilding the whole
|
|
81
85
|
# file re-mutated an entire API to accept eleven of them.
|
|
82
|
-
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]
|
|
83
87
|
if [ -z "$3" ]; then cp "$2" "$1"; return; fi
|
|
84
|
-
grep -vE "^($3)\.x" "$1"
|
|
88
|
+
{ grep -vE "^($3)\.x" "$1"; cat "$2"; baseline_marked "$1" && printf '# module %s\n' $4; } | sort -u > "$1.new"
|
|
85
89
|
mv "$1.new" "$1"
|
|
86
90
|
}
|
|
87
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
|
+
|
|
88
110
|
family_of() { # js, py, or any when no manifest says
|
|
89
111
|
o=$(owner_of "$1")
|
|
90
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
|
package/hooks/ship-gate.sh
CHANGED
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
elif [ -
|
|
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
|
|
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
|
-
|
|
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
|