@spardutti/claude-skills 2.45.0 → 2.47.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
@@ -137,6 +137,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
137
137
  | `/optimize` | Make one measured job faster without changing what it proves — fixes a benchmark and a result that must not move, sends 4 read-only subagents to find suspects, then changes one thing per round with a prediction written first, keeping only what beats the noise. Profiles after every wrong prediction, reruns before judging a result that flipped, and ends by writing the cause as a skill rule for you |
138
138
  | `/handoff` | Write a handoff doc so a fresh session picks the work up — captures what auto-compaction loses: decisions made and rejected, dead ends already ruled out, why the tree is dirty, and the one next step |
139
139
  | `/lockdown` | Per-repo supply-chain hardening — detects the package managers, Dockerfiles, and CI in use, then guides you through and applies install-time, deploy, and pipeline hardening for npm/pnpm, pip/uv, Docker, and GitHub Actions |
140
+ | `/skill-report` | Which installed skills earn their place in this repo — every `/ship` skills audit is logged by a hook, and this tallies it: how often each skill applied, how often it caught a violation, and the rule it catches most. Names rules that break often enough to become a script check, and skills that never flag and may be dead weight |
140
141
 
141
142
  ## How It Works
142
143
 
@@ -193,6 +194,7 @@ scripts/ validate-skills.mjs — checks skill length caps and reference in
193
194
  ship-gate.sh — /ship's file-length, folder-structure and mutation checks, behind an exit code
194
195
  ship-gate-hook.sh — refuses gh pr create/merge without a ship-gate receipt
195
196
  version-check.sh — SessionStart nudge when a newer catalog is published
197
+ skill-flags-record.sh — logs each /ship skills-audit verdict; skill-report.mjs tallies it
196
198
  gauntlet-selftest.sh — behavioural tests for the hooks (runs on pre-push)
197
199
  gauntlet-survey.sh — what the hook would do in every repo under a directory
198
200
  preflight.sh — everything that must pass before a release
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced by ship-gate.sh: which changed files a Stryker run can skip, and what its output says.
3
+
4
+ TEST_RE='(\.|_)(test|spec)\.[^.]+$|(^|/)tests?/'
5
+
6
+ stryker_owner() { # stryker_owner <owner> <owned files>
7
+ b=""; [ "$1" != "." ] && b="$1/"
8
+ [ -z "$GAUNTLET_MUTATE" ] && ! grep -q '\.py$' <<< "$2" \
9
+ && grep -qs '@stryker-mutator/core' "${b}package.json"
10
+ }
11
+
12
+ changed_tests() { # changed_tests <family>
13
+ printf '%s\n' "$CHANGED" | grep -E "$TEST_RE" | while IFS= read -r t; do
14
+ [ -n "$t" ] && [ "$(family_of "$t")" = "$1" ] && printf '%s\n' "$t"
15
+ done
16
+ }
17
+
18
+ # Everything a file's verdict rests on besides the file itself and the tests.
19
+ # Tests are left out: adding to them can only kill more, so only a lost line counts.
20
+ files_context() { # files_context <owner> <owned files>
21
+ fam=$(family_of "$1/.")
22
+ { printf '%s\n' "$PROJECT_DIR" "$1" "$(git rev-parse "$BASE" 2>/dev/null)"
23
+ cat "$HERE/ship-gate.sh" "$HERE/ship-gate-projects.sh" "$HERE/ship-gate-files.sh"
24
+ printf '%s\n' "$CHANGED" | grep -vE '\.(md|mdx|txt|rst|adoc)$' | grep -vE "$TEST_RE" \
25
+ | grep -vxF -f <(printf '%s\n' "$2") | while IFS= read -r f; do
26
+ [ -n "$f" ] || continue
27
+ case "$(family_of "$f")" in "$fam"|any) printf '%s\n' "$f"; [ -f "$f" ] && cat "$f" ;; esac
28
+ done; } | git hash-object --stdin
29
+ }
30
+
31
+ file_receipt() { # file_receipt <context> <file>
32
+ printf '/tmp/claude-shipgate-file-%s\n' "$({ printf '%s\n' "$1" "$2"; cat "$2"; } | git hash-object --stdin)"
33
+ }
34
+
35
+ test_then() { # test_then <snapshot> <test>: its content when the snapshot was taken
36
+ if grep -qxF "$2" "$1/manifest"; then cat "$1/files/$2" 2>/dev/null
37
+ else git show "$BASE:$2" 2>/dev/null; fi
38
+ }
39
+
40
+ # Never `| grep -q` under pipefail: diff exits 1 on any difference, and an early
41
+ # grep exit fails the writer, so a match reads as none.
42
+ tests_lost_lines() { # tests_lost_lines <snapshot> <family>
43
+ [ -f "$1/manifest" ] || return 0
44
+ while IFS= read -r t; do
45
+ [ -n "$t" ] || continue
46
+ case "$(diff <(test_then "$1" "$t") <(cat "$t" 2>/dev/null))" in *$'\n<'*) return 0 ;; esac
47
+ done <<< "$({ cat "$1/manifest"; changed_tests "$2"; } | sort -u)"
48
+ return 1
49
+ }
50
+
51
+ # Sets OWNED to the files that still need mutating.
52
+ files_split() { # files_split <context> <owner> <label>
53
+ fam=$(family_of "$2/."); kept=""; rest=""
54
+ while IFS= read -r f; do
55
+ [ -n "$f" ] || continue
56
+ r=$(file_receipt "$1" "$f")
57
+ if [ -f "$r" ] && ! tests_lost_lines "$(cat "$r")" "$fam"; then kept="$kept ${f#"$2"/}"
58
+ else rest="$rest$f"$'\n'; fi
59
+ done <<< "$OWNED"
60
+ OWNED=${rest%$'\n'}
61
+ [ -n "$kept" ] || return 0
62
+ echo " $3 stryker — ok, not re-run: passed and unchanged since:$kept"
63
+ echo " A change in a file they call is not re-checked."
64
+ b=""; [ "$2" != "." ] && b="$2/"
65
+ [ -z "$OWNED" ] && stryker_page_tests "$b" "$3"
66
+ return 0
67
+ }
68
+
69
+ # A finding with no location cannot be pinned on one file, so it keeps none.
70
+ files_record() { # files_record <context> <owner> <findings>
71
+ [ -n "$(printf '%s\n' "$3" | grep . | grep -vE ':[0-9]+:[0-9]+$')" ] && return 0
72
+ fam=$(family_of "$2/.")
73
+ snap="/tmp/claude-shipgate-tests-$({ printf '%s\n' "$1"; changed_tests "$fam" \
74
+ | while IFS= read -r t; do printf '%s\n' "$t"; cat "$t" 2>/dev/null; done; } | git hash-object --stdin)"
75
+ mkdir -p "$snap/files"; changed_tests "$fam" > "$snap/manifest"
76
+ while IFS= read -r t; do
77
+ [ -f "$t" ] && mkdir -p "$snap/files/$(dirname "$t")" && cp "$t" "$snap/files/$t"
78
+ done < "$snap/manifest"
79
+ while IFS= read -r f; do
80
+ [ -n "$f" ] || continue
81
+ grep -qF " ${f#"$2"/}:" <<< "$3" && continue
82
+ printf '%s\n' "$snap" > "$(file_receipt "$1" "$f")"
83
+ done <<< "$OWNED"
84
+ }
85
+
86
+ # Match a finding, never a summary row. Stryker prints a `# survived` COLUMN
87
+ # HEADER every run and repeats the word in its table, so a bare grep reports
88
+ # survivors on a clean run — worse than not running at all. Stryker marks each
89
+ # real finding `[Survived]`; table and border lines are excluded outright.
90
+ # NoCoverage is printed exactly like Survived and means something worse: not
91
+ # "a test missed this mutation" but "no test executes this line at all". The
92
+ # gate matched only [Survived], so a diff whose files have no tests came back
93
+ # PASS — 306 uncovered mutants reported as proven on one commit. It is judged
94
+ # only for files that went in WITH a range: a glob-shaped path is mutated
95
+ # whole, so its untouched lines would report uncovered forever, and a finding
96
+ # nobody can close is what teaches people to --force.
97
+ #
98
+ # The status line carries no file name; the line under it does, as
99
+ # file:line:column. They are paired here rather than grepped separately.
100
+ stryker_findings() { # stryker_findings <output> <globbed paths, comma-wrapped>
101
+ printf '%s\n' "$1" | awk -v globbed="$2" '
102
+ /^\[Survived\]/ { pend = $0; kind = "s"; next }
103
+ /^\[NoCoverage\]/ { pend = $0; kind = "n"; next }
104
+ pend != "" {
105
+ file = $0; sub(/:[0-9]+:[0-9]+$/, "", file)
106
+ if (kind == "s" || index(globbed, "," file ",") == 0) print pend " " $0
107
+ pend = ""
108
+ }
109
+ # A survivor with no location under it is still a survivor, never dropped.
110
+ END { if (pend != "" && kind == "s") print pend }
111
+ '
112
+ }
@@ -25,6 +25,18 @@ py_mutmut() {
25
25
  command -v mutmut >/dev/null 2>&1 && printf 'mutmut\n'
26
26
  }
27
27
 
28
+ # Node that lives only in Docker leaves no Stryker on the host. An executable
29
+ # .stryker-run runs it there instead, taking the arguments stryker would.
30
+ stryker_bin() { if [ -x "$1.stryker-run" ]; then echo ./.stryker-run; else echo npx --no-install stryker; fi; }
31
+ stryker_elsewhere() { # stryker_elsewhere <tool> <base>: after a failed run, say where Stryker could run
32
+ [ "$1" = stryker ] && [ ! -x "$2.stryker-run" ] || return 0
33
+ d=$(cd "${2:-.}" && pwd)
34
+ while [ "$d" != / ]; do [ -d "$d/node_modules/@stryker-mutator/core" ] && return 0; d=$(dirname "$d"); done
35
+ echo " Stryker is not installed on this machine. If Node runs only in Docker, add an"
36
+ echo " executable $2.stryker-run that runs it there and passes on its arguments:"
37
+ echo " docker compose run --rm -T <service> nice -n 19 npx stryker \"\$@\""
38
+ }
39
+
28
40
  owner_of() {
29
41
  d=$(dirname "$1")
30
42
  while :; do
@@ -127,7 +139,7 @@ project_key() { # project_key <owner> <tool>
127
139
  fam=$(family_of "$1/.")
128
140
  { printf '%s\n' "$PROJECT_DIR" "$1" "$2" "$(git rev-parse "$BASE" 2>/dev/null)" \
129
141
  "$GAUNTLET_NO_MUTATE" "$GAUNTLET_IGNORE_FILES"
130
- cat "$HERE/ship-gate.sh" "$HERE/ship-gate-projects.sh"
142
+ cat "$HERE/ship-gate.sh" "$HERE/ship-gate-projects.sh" "$HERE/ship-gate-files.sh"
131
143
  printf '%s\n' "$CHANGED" | grep -vE '\.(md|mdx|txt|rst|adoc)$' | while IFS= read -r f; do
132
144
  [ -n "$f" ] || continue
133
145
  case "$(family_of "$f")" in "$fam"|any) printf '%s\n' "$f"; [ -f "$f" ] && cat "$f" ;; esac
@@ -46,7 +46,7 @@
46
46
  set -uo pipefail
47
47
 
48
48
  HERE=$(cd "$(dirname "$0")" && pwd)
49
- . "$HERE/ship-gate-projects.sh" && . "$HERE/ship-gate-structure.sh" || { echo "ship-gate: cannot read its helpers in $HERE"; exit 1; }
49
+ . "$HERE/ship-gate-projects.sh" && . "$HERE/ship-gate-structure.sh" && . "$HERE/ship-gate-files.sh" || { echo "ship-gate: cannot read its helpers in $HERE"; exit 1; }
50
50
 
51
51
  MODE=""
52
52
  case "${1:-}" in
@@ -122,7 +122,7 @@ if [ "$MODE" = force ]; then
122
122
  fi
123
123
  gate_report "$RECEIPT_KEY"
124
124
  SOURCED=$(printf '%s\n' "$CHANGED" | grep -E "\.($GAUNTLET_SOURCE_EXT)$" \
125
- | grep -vE '(\.|_)(test|spec)\.[^.]+$|(^|/)tests?/|(^|/)\.stryker-tmp/' | while read -r f; do
125
+ | grep -vE "$TEST_RE|(^|/)\.stryker-tmp/" | while read -r f; do
126
126
  [ -f "$f" ] && printf '%s\n' "$f"
127
127
  done)
128
128
 
@@ -223,6 +223,7 @@ for owner in $OWNERS; do
223
223
  OWNED=$(while IFS= read -r f; do
224
224
  [ "$(owner_of "$f")" = "$owner" ] && printf '%s\n' "$f"
225
225
  done <<< "$FILES" | grep -vE "$GAUNTLET_NO_MUTATE" || true)
226
+ FCTX=""; if stryker_owner "$owner" "$OWNED"; then FCTX=$(files_context "$owner" "$OWNED"); files_split "$FCTX" "$owner" "$label"; fi
226
227
  [ -z "$OWNED" ] && continue
227
228
 
228
229
  # ONE comma-joined --mutate value, never a flag per range. Stryker's CLI
@@ -271,7 +272,7 @@ for owner in $OWNERS; do
271
272
  # 8 of them on one commit, all long since killed. --mutate already scopes
272
273
  # the run to the changed hunks, so incremental buys nothing here and costs
273
274
  # a cache that goes stale exactly the way mutmut's did.
274
- CMD="${RUN}npx --no-install stryker run --concurrency $(gate_workers) --mutate '${FLAGS#,}'"; stryker_page_tests "$base" "$label"
275
+ CMD="${RUN}$(stryker_bin "$base") run --concurrency $(gate_workers) --mutate '${FLAGS#,}'"; stryker_page_tests "$base" "$label"
275
276
  elif [ -z "$PY" ] && [ -f "$base"package.json ]; then
276
277
  MISSING="$MISSING $label needs Stryker:
277
278
  npm --prefix ${owner} i -D @stryker-mutator/core @stryker-mutator/vitest-runner
@@ -324,32 +325,9 @@ for owner in $OWNERS; do
324
325
  T0=$SECONDS; OUT=$(export FILES="$(printf '%s' "$OWNED" | tr '\n' ' ')" MUTATE_FLAGS="--mutate ${FLAGS#,}"; eval "$CMD" 2>&1); RC=$?; T=$((SECONDS - T0))
325
326
  [ "$TOOL" = mutmut ] && [ $RC -eq 0 ] && { printf '%s\n' "$OUT" > "$PKEY.out"; cp "$MLOG" "$PKEY.log" 2>/dev/null; }
326
327
  [ -n "$SCOPE_RE" ] && [ $RC -eq 0 ] && OUT=$(printf '%s\n' "$OUT" | grep -E "^[[:space:]]*($SCOPE_RE)\.x")
327
- # Match a finding, never a summary row. Stryker prints a `# survived` COLUMN
328
- # HEADER every run and repeats the word in its table, so a bare grep reports
329
- # survivors on a clean run — worse than not running at all. Stryker marks each
330
- # real finding `[Survived]`; table and border lines are excluded outright.
331
- # NoCoverage is printed exactly like Survived and means something worse: not
332
- # "a test missed this mutation" but "no test executes this line at all". The
333
- # gate matched only [Survived], so a diff whose files have no tests came back
334
- # PASS — 306 uncovered mutants reported as proven on one commit. It is judged
335
- # only for files that went in WITH a range: a glob-shaped path is mutated
336
- # whole, so its untouched lines would report uncovered forever, and a finding
337
- # nobody can close is what teaches people to --force.
338
- #
339
- # The status line carries no file name; the line under it does, as
340
- # file:line:column. They are paired here rather than grepped separately.
341
328
  if [ "$TOOL" = stryker ]; then
342
- ALL=$(printf '%s\n' "$OUT" | awk -v globbed="$GLOBBED" '
343
- /^\[Survived\]/ { pend = $0; kind = "s"; next }
344
- /^\[NoCoverage\]/ { pend = $0; kind = "n"; next }
345
- pend != "" {
346
- file = $0; sub(/:[0-9]+:[0-9]+$/, "", file)
347
- if (kind == "s" || index(globbed, "," file ",") == 0) print pend " " $0
348
- pend = ""
349
- }
350
- # A survivor with no location under it is still a survivor, never dropped.
351
- END { if (pend != "" && kind == "s") print pend }
352
- ')
329
+ ALL=$(stryker_findings "$OUT" "$GLOBBED")
330
+ [ -n "$FCTX" ] && [ $RC -eq 0 ] && files_record "$FCTX" "$owner" "$ALL"
353
331
  else
354
332
  ALL=$(printf '%s\n' "$OUT" | grep -E '[Ss]urvived' \
355
333
  | grep -vE '^[[:space:]]*[#│|+-]|# *surviv')
@@ -433,7 +411,7 @@ for owner in $OWNERS; do
433
411
  STATUS=1
434
412
  elif [ $RC -ne 0 ]; then
435
413
  echo " $label $TOOL (${T}s) — the run failed:"
436
- printf '%s\n' "$OUT" | tail -12 | sed 's/^/ /'
414
+ printf '%s\n' "$OUT" | tail -12 | sed 's/^/ /'; stryker_elsewhere "$TOOL" "$base"
437
415
  [ "$STATUS" = 0 ] && STATUS=2
438
416
  elif [ -n "$NOTE" ]; then
439
417
  printf '%s\n' "$NOTE"
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ # SubagentStop: appends each gauntlet-skills verdict to .claude/skill-flags.jsonl.
3
+
4
+ INPUT=$(cat | tr -d '\n')
5
+
6
+ case "$INPUT" in
7
+ *'"agent_type":"gauntlet-skills"'*) ;;
8
+ *) exit 0 ;;
9
+ esac
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
13
+
14
+ # Kept JSON-escaped as it arrived, so the line stays valid JSON without a parser.
15
+ REPLY=$(printf '%s' "$INPUT" | sed -nE 's/.*"last_assistant_message":"(([^"\\]|\\.)*)".*/\1/p')
16
+ [ -z "$REPLY" ] && exit 0
17
+
18
+ TRANSCRIPT=$(printf '%s' "$INPUT" | grep -o '"agent_transcript_path":"[^"]*"' | head -1 | sed 's/.*:"//; s/"$//')
19
+ MODEL=""
20
+ [ -f "$TRANSCRIPT" ] && MODEL=$(grep -o '"model":"[^"]*"' "$TRANSCRIPT" | tail -1 | sed 's/.*:"//; s/"$//')
21
+
22
+ printf '{"ts":"%s","model":"%s","reply":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$MODEL" "$REPLY" \
23
+ >> "$PROJECT_DIR/.claude/skill-flags.jsonl"
24
+
25
+ exit 0
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // Prints, per skill, how often the /ship skills audit applied it and flagged a violation.
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const DEFAULT_LOG = fileURLToPath(new URL("../skill-flags.jsonl", import.meta.url));
7
+
8
+ function parseVerdict(line) {
9
+ try {
10
+ const { model, reply } = JSON.parse(line);
11
+ return { model, verdict: JSON.parse(reply.slice(reply.indexOf("{"), reply.lastIndexOf("}") + 1)) };
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function tally(lines) {
18
+ const skills = new Map();
19
+ const models = new Map();
20
+ let unreadable = 0;
21
+ for (const line of lines) {
22
+ const parsed = parseVerdict(line);
23
+ if (!parsed?.verdict?.skill) { unreadable++; continue; }
24
+ const { model, verdict } = parsed;
25
+ if (!(verdict.checked > 0)) continue;
26
+ const s = skills.get(verdict.skill) ?? { audits: 0, flagged: 0, rules: new Map() };
27
+ s.audits++;
28
+ const violations = Array.isArray(verdict.violations) ? verdict.violations : [];
29
+ if (violations.length) s.flagged++;
30
+ for (const v of violations) s.rules.set(v.rule, (s.rules.get(v.rule) ?? 0) + 1);
31
+ skills.set(verdict.skill, s);
32
+ if (model) models.set(model, (models.get(model) ?? 0) + 1);
33
+ }
34
+ return { skills, models, unreadable };
35
+ }
36
+
37
+ function topRule(rules) {
38
+ const [rule] = [...rules].sort((a, b) => b[1] - a[1])[0] ?? ["-"];
39
+ return rule.length > 60 ? rule.slice(0, 57) + "..." : rule;
40
+ }
41
+
42
+ function render({ skills, models, unreadable }) {
43
+ if (!skills.size) return "No skill audits recorded yet. Run /ship to record some.";
44
+ const rows = [...skills].sort((a, b) => b[1].flagged - a[1].flagged || a[0].localeCompare(b[0]));
45
+ const width = Math.max(5, ...rows.map(([name]) => name.length));
46
+ const out = [`${"skill".padEnd(width)} audits flagged top rule broken`];
47
+ for (const [name, s] of rows) {
48
+ out.push(`${name.padEnd(width)} ${String(s.audits).padStart(6)} ${String(s.flagged).padStart(7)} ${topRule(s.rules)}`);
49
+ }
50
+ if (models.size) out.push("", "models: " + [...models].map(([m, n]) => `${m} (${n})`).join(", "));
51
+ if (unreadable) out.push(`unreadable lines: ${unreadable}`);
52
+ return out.join("\n");
53
+ }
54
+
55
+ let text = "";
56
+ try {
57
+ text = readFileSync(process.argv[2] ?? DEFAULT_LOG, "utf-8");
58
+ } catch {
59
+ // no log yet
60
+ }
61
+ console.log(render(tally(text.split("\n").filter(Boolean))));
@@ -11,6 +11,7 @@ const GAUNTLET_FILENAME = "gauntlet.sh";
11
11
  const SHIP_GATE_FILENAME = "ship-gate.sh";
12
12
  const SHIP_GATE_HOOK_FILENAME = "ship-gate-hook.sh";
13
13
  const VERSION_CHECK_FILENAME = "version-check.sh";
14
+ const SKILL_FLAGS_FILENAME = "skill-flags-record.sh";
14
15
  const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
15
16
  const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
16
17
 
@@ -116,6 +117,16 @@ export async function registerHooks(settingsPath) {
116
117
  settings.hooks.Stop = [gauntletEntry];
117
118
  }
118
119
 
120
+ // No matcher: the script filters on agent_type itself.
121
+ const skillFlagsCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${SKILL_FLAGS_FILENAME}`;
122
+ const skillFlagsEntry = { hooks: [{ type: "command", command: skillFlagsCommand }] };
123
+
124
+ if (Array.isArray(settings.hooks.SubagentStop)) {
125
+ upsert(settings.hooks.SubagentStop, SKILL_FLAGS_FILENAME, skillFlagsEntry);
126
+ } else {
127
+ settings.hooks.SubagentStop = [skillFlagsEntry];
128
+ }
129
+
119
130
  // Both gates clear by touching a marker under /tmp. The hook scripts let those
120
131
  // commands through themselves, but auto mode's permission classifier does not:
121
132
  // a touch whose only purpose is to unlock a gate is exactly what it refuses.
@@ -20,6 +20,8 @@ import { homedir } from "node:os";
20
20
  // ship-gate-projects.sh sourced by ship-gate.sh — project detection, reuse
21
21
  // ship-gate-hook.sh PreToolUse on Bash — no commit without a receipt
22
22
  // version-check.sh SessionStart — a line when a newer catalog exists
23
+ // skill-flags-record.sh SubagentStop — logs each skills-audit verdict
24
+ // skill-report.mjs read by /skill-report — tallies that log
23
25
 
24
26
  const GATE_FILENAME = "skill-gate.sh";
25
27
  const AUTO_MARK_FILENAME = "skill-gate-automark.sh";
@@ -28,8 +30,11 @@ const GAUNTLET_FILENAME = "gauntlet.sh";
28
30
  const SHIP_GATE_FILENAME = "ship-gate.sh";
29
31
  const SHIP_GATE_PROJECTS_FILENAME = "ship-gate-projects.sh";
30
32
  const SHIP_GATE_STRUCTURE_FILENAME = "ship-gate-structure.sh";
33
+ const SHIP_GATE_FILES_FILENAME = "ship-gate-files.sh";
31
34
  const SHIP_GATE_HOOK_FILENAME = "ship-gate-hook.sh";
32
35
  const VERSION_CHECK_FILENAME = "version-check.sh";
36
+ const SKILL_FLAGS_FILENAME = "skill-flags-record.sh";
37
+ const SKILL_REPORT_FILENAME = "skill-report.mjs";
33
38
  const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
34
39
  const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
35
40
 
@@ -108,8 +113,11 @@ export async function setupHook(targetDir = process.cwd()) {
108
113
  SHIP_GATE_FILENAME,
109
114
  SHIP_GATE_PROJECTS_FILENAME,
110
115
  SHIP_GATE_STRUCTURE_FILENAME,
116
+ SHIP_GATE_FILES_FILENAME,
111
117
  SHIP_GATE_HOOK_FILENAME,
112
118
  VERSION_CHECK_FILENAME,
119
+ SKILL_FLAGS_FILENAME,
120
+ SKILL_REPORT_FILENAME,
113
121
  ]) {
114
122
  const dest = join(hooksDir, name);
115
123
  await writeFile(dest, await readHookSource(name), { mode: 0o755 });
@@ -131,5 +139,6 @@ export async function setupHook(targetDir = process.cwd()) {
131
139
  console.log(` Hook installed: .claude/hooks/${GAUNTLET_FILENAME}`);
132
140
  console.log(` Installed: .claude/hooks/${SHIP_GATE_FILENAME} (used by /ship)`);
133
141
  console.log(` Hook installed: .claude/hooks/${SHIP_GATE_HOOK_FILENAME}`);
142
+ console.log(` Hook installed: .claude/hooks/${SKILL_FLAGS_FILENAME}`);
134
143
  console.log(` Settings updated: .claude/settings.json`);
135
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.45.0",
3
+ "version": "2.47.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-structure.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-files.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh ../scripts/skill-flags-record.sh ../scripts/skill-report.mjs hooks/",
15
15
  "postpack": "git checkout -- README.md && rm -rf hooks"
16
16
  },
17
17
  "engines": {