@spardutti/claude-skills 2.56.3 → 2.58.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
@@ -36,7 +36,7 @@ npx @spardutti/claude-skills
36
36
  ```
37
37
 
38
38
  ```text
39
- Claude Skills Installer v2.56.3
39
+ Claude Skills Installer v2.58.0
40
40
 
41
41
  ── Frontend ──────────────────────────────
42
42
  ◉ react ◯ tanstack-query
@@ -134,7 +134,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
134
134
  |---------|--------------|
135
135
  | `/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, mutation-tests the changed lines to prove the tests would catch a break, and runs one `/code-review` round for logic bugs — **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 |
136
136
  | `/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` |
137
- | `/plan-feature` | Integration-first feature planning, then building — 3 parallel subagents scan for reusable code, patterns and touch points, grounded clarifying questions follow, the plan is written as type-checked stubs at the real paths, and on your go the same agent fills them in |
137
+ | `/plan-feature` | Integration-first feature planning, then building — one agent scans the repo for reusable code, patterns and touch points, grounded clarifying questions follow, the plan is written as type-checked stubs at the real paths, and on your go it fills them in |
138
138
  | `/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 |
139
139
  | `/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 |
140
140
  | `/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 |
@@ -18,6 +18,25 @@ load_gauntlet_conf() { # load_gauntlet_conf <project conf> <fallback conf>
18
18
  if [ -f "$conf" ]; then . "$conf"; fi
19
19
  }
20
20
 
21
+ skill_field() { # skill_field <SKILL.md> <key pattern> — its first value, quotes stripped
22
+ sed -n "s/^$2:[[:space:]]*//p" "$1" | head -1 | sed "s/^[\"']//; s/[\"']\$//"
23
+ }
24
+
25
+ # Claude Code writes gate-paths gitignore-style. A case glob has no **, but * already crosses /
26
+ # here, so dropping the **/ prefix is equivalent. set -f keeps the globs from matching real files.
27
+ skill_hits() { # skill_hits <gate-paths> <files...> — the files those globs match
28
+ local g t globs
29
+ set -f
30
+ globs=$(printf '%s' "$1" | tr ',' ' ' | sed 's!\*\*/!!g')
31
+ for t in "${@:2}"; do
32
+ for g in $globs; do
33
+ case "$g" in \**|/*) ;; *) g="*$g" ;; esac
34
+ case "$t" in $g) printf '%s\n' "$t"; break ;; esac
35
+ done
36
+ done
37
+ set +f
38
+ }
39
+
21
40
  json_field() { # json_field <json> <key> — a flat string value; \" is unescaped, \n is kept as two characters
22
41
  printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"([^\"\\\\]|\\\\.)*\"" | head -1 \
23
42
  | sed -E 's/^[^:]*:[[:space:]]*"//; s/"$//; s/\\"/"/g'
@@ -89,6 +89,25 @@ changed_files() {
89
89
  git ls-files --others --exclude-standard 2>/dev/null; } | sort -u | grep -vE '(^|/)\.claude/'
90
90
  }
91
91
 
92
+ file_hunks() { # file_hunks <base> <file> — its new-side line ranges, one "start-end" a line
93
+ local h
94
+ h=$(git diff -U0 "$1" -- "$2" 2>/dev/null | grep -oE '^@@ -[0-9,]+ \+[0-9]+(,[0-9]+)?' | sed 's/.*+//' \
95
+ | while IFS=, read -r s l; do l=${l:-1}; [ "$l" -gt 0 ] && echo "$s-$((s+l-1))"; done)
96
+ if [ -n "$h" ]; then printf '%s\n' "$h"; else echo "1-$(wc -l < "$2" | tr -d ' ')"; fi
97
+ }
98
+
99
+ # The audit's scope: every changed text file, minus generated, vendored and lock files. Tab-separated: paths may hold spaces.
100
+ scope_report() {
101
+ local f r new
102
+ echo "base $BASE"
103
+ printf '%s\n' "$CHANGED" | grep -vE '(^|/)(package-lock\.json|pnpm-lock\.yaml|[^/]*\.lock)$' | while IFS= read -r f; do
104
+ [ -f "$f" ] && ! is_ignored "$f" && grep -qI . "$f" || continue
105
+ r=$(file_hunks "$BASE" "$f" | sed -E 's/^([0-9]+)-\1$/\1/' | paste -sd, -)
106
+ new=""; git cat-file -e "$BASE:$f" 2>/dev/null || new=" (new)"
107
+ printf '%s\t%s%s\n' "$f" "$r" "$new"
108
+ done
109
+ }
110
+
92
111
  # Ignored extensions are left out; see the comment where ship-gate.sh first calls this.
93
112
  receipt_key() {
94
113
  { printf '%s\n' "$PROJECT_DIR"
@@ -52,6 +52,7 @@ HERE=$(cd "$(dirname "$0")" && pwd)
52
52
  MODE=""
53
53
  case "${1:-}" in
54
54
  --key) MODE=key; shift ;;
55
+ --scope) MODE=scope; shift ;;
55
56
  --force) MODE=force; shift ;;
56
57
  --baseline) MODE=baseline; shift ;;
57
58
  esac
@@ -81,6 +82,7 @@ for B in ${GAUNTLET_BASE:-} develop dev main; do [ -n "$BASE" ] || BASE=$(git me
81
82
  [ -n "$BASE" ] || BASE=$(git rev-list --max-parents=0 HEAD | head -1)
82
83
 
83
84
  CHANGED=$(changed_files)
85
+ [ "$MODE" = scope ] && { scope_report; exit 0; }
84
86
 
85
87
  # Only the files this gate can actually read go into the key. It used to be every
86
88
  # changed file, so a version bump in package.json or a line in a changelog threw
@@ -205,10 +207,7 @@ for owner in $OWNERS; do
205
207
  FLAGS=""
206
208
  GLOBBED=","
207
209
  while IFS= read -r f; do
208
- hunks=$(git diff -U0 "$BASE" -- "$f" 2>/dev/null \
209
- | grep -oE '^@@ -[0-9,]+ \+[0-9]+(,[0-9]+)?' | sed 's/.*+//' \
210
- | while IFS=, read -r s l; do l=${l:-1}; [ "$l" -gt 0 ] && echo "$s-$((s+l-1))"; done)
211
- [ -z "$hunks" ] && hunks="1-$(wc -l < "$f")"
210
+ hunks=$(file_hunks "$BASE" "$f")
212
211
  rel=${f#$owner/}
213
212
  # A Next.js route folder — [id], [[...slug]] — is a glob expression to
214
213
  # Stryker, which refuses "Cannot combine a glob expression with a mutation
@@ -145,8 +145,8 @@ function run(mode, args) {
145
145
  if (todo.length) console.log(todo.map((t) => ` - ${t}`).join("\n"));
146
146
  return todo.length ? 1 : 0;
147
147
  }
148
- // A gate run by hand or on a release is not a ship; only /ship's start opens one.
149
- if (mode === "gate" && !existsSync(path)) return 0;
148
+ // A gate run or a review outside /ship is not a ship; only /ship's start opens one.
149
+ if ((mode === "gate" || mode === "step") && !existsSync(path)) return 0;
150
150
  if (mode === "gate") addGateRun(record, args);
151
151
  else if (mode === "finding") addFinding(record, args[0]);
152
152
  else if (mode === "step") addStep(record, args);
@@ -161,7 +161,9 @@ function run(mode, args) {
161
161
  }
162
162
 
163
163
  try {
164
- if (process.env.SHIP_LOG_ROOT) process.chdir(process.env.SHIP_LOG_ROOT);
164
+ // Like ship-mark, ship-gate and ship-prep, a worktree ship names its folder in CLAUDE_PROJECT_DIR.
165
+ const root = process.env.SHIP_LOG_ROOT || process.env.CLAUDE_PROJECT_DIR;
166
+ if (root) process.chdir(root);
165
167
  process.exit(run(process.argv[2], process.argv.slice(3)) ?? 0);
166
168
  } catch (err) {
167
169
  console.error(`ship-log: ${err.message}`);
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bash
2
+ HERE=$(cd "$(dirname "$0")" && pwd)
3
+ . "$HERE/gate-config.sh" || { echo "ship-prep: cannot read $HERE/gate-config.sh"; exit 1; }
4
+ cd "${CLAUDE_PROJECT_DIR:-$PWD}" 2>/dev/null || { echo "ship-prep: cannot read ${CLAUDE_PROJECT_DIR:-$PWD}"; exit 1; }
5
+
6
+ stop() { echo "ship-prep: STOP — $1"; exit 1; }
7
+
8
+ first_remote() { # first_remote <branch...> — the first one origin has
9
+ local b
10
+ for b in "$@"; do git rev-parse -q --verify "origin/$b" >/dev/null && { echo "$b"; return; }; done
11
+ }
12
+
13
+ base_branch() { local dev; dev=$(first_remote develop development dev); echo "${dev:-$(first_remote main master)}"; }
14
+
15
+ start() {
16
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || stop "not a git repo."
17
+ local branch base dirty behind ahead
18
+ branch=$(git branch --show-current)
19
+ [ -n "$branch" ] || stop "Detached HEAD — check out a branch first."
20
+ if git remote | grep -qx origin; then git fetch -q origin 2>/dev/null; else echo "remote none: pr, merge and release cannot run"; fi
21
+ base=$(base_branch)
22
+ echo "branch $branch"
23
+ echo "base ${base:-none} (main $(first_remote main master))"
24
+ if ! git rev-parse -q --verify HEAD >/dev/null; then echo "commits none"
25
+ elif [ -n "$base" ]; then
26
+ read -r behind ahead < <(git rev-list --left-right --count "origin/$base...HEAD")
27
+ echo "ahead $ahead, behind $behind of origin/$base"
28
+ fi
29
+ dirty=$(git status --porcelain)
30
+ echo "uncommitted $(printf '%s' "$dirty" | grep -c .)"
31
+ [ -n "$dirty" ] && printf '%s\n' "$dirty" | sed 's/^/ /'
32
+ if gh auth status >/dev/null 2>&1; then
33
+ echo "gh logged in"
34
+ echo "pr $(gh pr view --json state,number,url -q '.state+" #"+(.number|tostring)+" "+.url' 2>/dev/null || echo none)"
35
+ else
36
+ echo "gh NOT logged in: pr, merge and release cannot run"
37
+ fi
38
+ bash "$HERE/ship-mark.sh" begin || stop "ship-mark.sh begin failed."
39
+ }
40
+
41
+ skills_report() { # skills_report <files...>
42
+ local f name paths hits
43
+ for f in "$(diary_dir "$HERE")"/skills/*/SKILL.md; do
44
+ [ -f "$f" ] || continue
45
+ name=$(skill_field "$f" name); paths=$(skill_field "$f" '[[:space:]]*gate-paths')
46
+ if [ -z "$paths" ]; then echo "skill $name: decide from its description"; continue; fi
47
+ hits=$(skill_hits "$paths" "$@" | paste -sd' ' -)
48
+ if [ -n "$hits" ]; then echo "skill $name: audit $hits"; else echo "skill $name: skip, no changed file matches $paths"; fi
49
+ done
50
+ }
51
+
52
+ scope() {
53
+ local branch scope base files=() diff f
54
+ cd "$(git rev-parse --show-toplevel)" || stop "cannot find the repo root."
55
+ branch=$(git branch --show-current)
56
+ scope=$(bash "$HERE/ship-gate.sh" --scope) || stop "ship-gate.sh --scope failed: $scope"
57
+ printf '%s\n' "$scope"
58
+ base=$(printf '%s\n' "$scope" | sed -n '1s/^base //p')
59
+ while IFS=$'\t' read -r f _; do [ -n "$f" ] && files+=("$f"); done < <(printf '%s\n' "$scope" | sed '1d')
60
+ skills_report "${files[@]}"
61
+ diff="/tmp/claude-ship-$(printf '%s %s' "$PWD" "$branch" | cksum | cut -d' ' -f1).diff"
62
+ : > "$diff"
63
+ if [ ${#files[@]} -gt 0 ]; then
64
+ git diff "$base" -- "${files[@]}" >> "$diff"
65
+ for f in "${files[@]}"; do
66
+ git ls-files --error-unmatch "$f" >/dev/null 2>&1 || git diff --no-index /dev/null "$f" >> "$diff"
67
+ done
68
+ fi
69
+ echo "diff $diff"
70
+ echo "review /code-review medium origin/$(base_branch)...$branch"
71
+ node "$HERE/ship-log.mjs" step audit start >/dev/null 2>&1
72
+ node "$HERE/ship-log.mjs" step review start >/dev/null 2>&1
73
+ return 0
74
+ }
75
+
76
+ secrets() {
77
+ local branch names hits
78
+ branch=$(git branch --show-current)
79
+ case "$branch" in main|master|develop|development|dev) stop "on protected branch $branch — commit on a feature or hotfix/<desc> branch." ;; esac
80
+ git status --short
81
+ names=$({ git diff HEAD --name-only --diff-filter=d; git ls-files --others --exclude-standard; } 2>/dev/null \
82
+ | grep -iE '(^|/)(\.env(\..*)?|[^/]*\.pem|[^/]*credentials[^/]*|[^/]*\.key)$' | grep -viE '\.(example|sample|template)$')
83
+ hits=$({ git diff HEAD; git ls-files --others --exclude-standard -z | xargs -0 -r -I{} git diff --no-index /dev/null {}; } 2>/dev/null \
84
+ | grep -E '^\+' | grep -E 'AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----|xox[abprs]-[A-Za-z0-9-]{10,}|[Bb]earer [A-Za-z0-9._~+/-]{20,}' | cut -c1-120)
85
+ [ -z "$names$hits" ] && { echo "secrets none"; return 0; }
86
+ echo "secrets FOUND — STOP and tell the user; suggest gitleaks or git-secrets as a pre-commit guard:"
87
+ [ -n "$names" ] && printf '%s\n' "$names" | sed 's/^/ file /'
88
+ [ -n "$hits" ] && printf '%s\n' "$hits" | sed 's/^/ line /'
89
+ return 1
90
+ }
91
+
92
+ CHECKS_FAILED='[.statusCheckRollup[]? | select(((.conclusion // .state) // "") | test("FAILURE|ERROR|CANCELLED|TIMED_OUT|ACTION_REQUIRED")) | (.name // .context)] | join(",")'
93
+ CHECKS_RUNNING='[.statusCheckRollup[]? | select((.status // .state // "") | test("IN_PROGRESS|QUEUED|PENDING|WAITING")) | (.name // .context)] | join(",")'
94
+ PR_JQ="[.number, .isDraft, .mergeable, (.reviewDecision // \"\"), ($CHECKS_FAILED), ($CHECKS_RUNNING), .baseRefName, .headRefName] | map(tostring) | join(\"|\")"
95
+
96
+ merge_check() { # merge_check [pr]; sets PR_BASE and PR_HEAD
97
+ local line n draft mergeable review failing pending why=""
98
+ line=$(gh pr view ${1:+"$1"} --json number,isDraft,mergeable,reviewDecision,statusCheckRollup,baseRefName,headRefName -q "$PR_JQ") || stop "no open PR found."
99
+ IFS='|' read -r n draft mergeable review failing pending PR_BASE PR_HEAD <<< "$line"
100
+ [ "$draft" = true ] && why="$why draft;"
101
+ [ "$mergeable" = CONFLICTING ] && why="$why conflicts with the base;"
102
+ [ -n "$failing" ] && why="$why CI failing: $failing;"
103
+ case "$review" in REVIEW_REQUIRED|CHANGES_REQUESTED) why="$why review: $review;" ;; esac
104
+ [ -n "$pending" ] && why="$why CI still running: $pending;"
105
+ [ -z "$why" ] && { echo "merge ok #$n"; return 0; }
106
+ echo "merge STOP #$n:$why"; return 1
107
+ }
108
+
109
+ land() { # land <pr> [squash|merge]
110
+ local worktree=""
111
+ [ -n "${1:-}" ] || stop "usage: ship-prep.sh land <pr> [squash|merge]"
112
+ merge_check "$1" >/dev/null || { merge_check "$1"; exit 1; }
113
+ [ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ] && worktree=1
114
+ if [ "${2:-squash}" = merge ]; then gh pr merge "$1" --merge || stop "gh pr merge failed."
115
+ elif [ -n "$worktree" ]; then
116
+ # gh --delete-branch switches the local checkout, which a worktree cannot; delete the branch on origin instead.
117
+ gh pr merge "$1" --squash || stop "gh pr merge failed."
118
+ git push -q origin --delete "$PR_HEAD" 2>/dev/null
119
+ else gh pr merge "$1" --squash --delete-branch || stop "gh pr merge failed."; fi
120
+ if [ -n "$worktree" ]; then
121
+ git fetch -q origin 2>/dev/null
122
+ echo "landed #$1; this is a worktree, so it stays on its branch — close the session to remove it"
123
+ else
124
+ git checkout -q "$PR_BASE" && git pull -q --ff-only origin "$PR_BASE" 2>/dev/null
125
+ echo "landed #$1; now on $(git branch --show-current) at $(git rev-parse --short HEAD)"
126
+ fi
127
+ }
128
+
129
+ release() {
130
+ local tag v major minor patch commits breaking feat next
131
+ git fetch -q --tags origin 2>/dev/null
132
+ tag=$(git tag --sort=-v:refname | head -1)
133
+ [ -n "$tag" ] || { echo "tag none; suggest v0.1.0 (confirm with the user)"; return 0; }
134
+ commits=$(git log "$tag"..HEAD --format='%s')
135
+ [ -n "$commits" ] || stop "no commits since $tag — nothing to release."
136
+ v=${tag##*[!0-9.]}; IFS=. read -r major minor patch <<< "$v"
137
+ breaking=$(( $(printf '%s\n' "$commits" | grep -cE '^[a-z]+(\([^)]*\))?!:') + $(git log "$tag"..HEAD --format=%B | grep -c 'BREAKING CHANGE') ))
138
+ feat=$(printf '%s\n' "$commits" | grep -cE '^feat(\([^)]*\))?!?:')
139
+ if [ "$major" -ge 1 ]; then
140
+ if [ "$breaking" -gt 0 ]; then next="$((major+1)).0.0"; elif [ "$feat" -gt 0 ]; then next="$major.$((minor+1)).0"; else next="$major.$minor.$((patch+1))"; fi
141
+ elif [ "$breaking" -gt 0 ]; then next="0.$((minor+1)).0"
142
+ else next="0.$minor.$((patch+1))"; fi
143
+ echo "tag $tag"
144
+ echo "suggest ${tag%"$v"}$next (confirm with the user)"
145
+ printf '%s\n' "$commits" | sed 's/^/ /'
146
+ }
147
+
148
+ case "${1:-}" in
149
+ start) start ;;
150
+ scope) scope ;;
151
+ secrets) secrets ;;
152
+ merge) merge_check "${2:-}" ;;
153
+ land) land "${2:-}" "${3:-}" ;;
154
+ release) release ;;
155
+ *) echo "usage: ship-prep.sh start | scope | secrets | merge [pr] | land <pr> [squash|merge] | release" >&2; exit 2 ;;
156
+ esac
@@ -26,4 +26,7 @@ BRANCH=$(git -C "${CWD:-$PROJECT_DIR}" branch --show-current 2>/dev/null | json_
26
26
  printf '{"ts":"%s","branch":"%s","model":"%s","reply":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BRANCH" "$MODEL" "$REPLY" \
27
27
  >> "$(diary_dir "$SCRIPT_DIR")/$LOG"
28
28
 
29
+ case "$LOG" in skill-flags.jsonl) STEP=audit ;; *) STEP=review ;; esac
30
+ SHIP_LOG_ROOT="${CWD:-$PROJECT_DIR}" node "$SCRIPT_DIR/ship-log.mjs" step "$STEP" end >/dev/null 2>&1
31
+
29
32
  exit 0
@@ -4,6 +4,7 @@
4
4
  INPUT=$(cat)
5
5
 
6
6
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ . "$SCRIPT_DIR/gate-config.sh"
7
8
  PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
8
9
 
9
10
  if ! find "$PROJECT_DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null | grep -q .; then
@@ -130,25 +131,14 @@ if [ -n "$TARGETS" ]; then
130
131
  #
131
132
  # The value must be quoted in YAML either way: a scalar opening with * is
132
133
  # an alias indicator.
133
- UNQUOTE='s/^["'"'"']//; s/["'"'"']$//'
134
- SPATHS=$(sed -n 's/^[[:space:]]*gate-paths:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
135
- STRACKS=$(sed -n 's/^tracks:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
136
- SNAME=$(sed -n 's/^name:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
134
+ SPATHS=$(skill_field "$SKILL_FILE" '[[:space:]]*gate-paths')
135
+ STRACKS=$(skill_field "$SKILL_FILE" tracks)
136
+ SNAME=$(skill_field "$SKILL_FILE" name)
137
137
  [ -z "$SPATHS" ] && continue
138
138
  [ -z "$SNAME" ] && continue
139
139
  case "$INSTALLED" in *"\"$SNAME\""*) ;; *) continue ;; esac # automark marks only installed and custom skills
140
-
141
- HIT=""
142
- for G in $(printf '%s' "$SPATHS" | tr ',' ' '); do
143
- # Claude Code writes these gitignore-style. A case glob has no **, but *
144
- # already crosses / here, so dropping the **/ prefix is equivalent.
145
- G=$(printf '%s' "$G" | sed 's!\*\*/!!g')
146
- case "$G" in \**|/*) ;; *) G="*$G" ;; esac
147
- for T in $TARGETS; do
148
- case "$T" in $G) HIT=1 ;; esac
149
- done
150
- done
151
- [ -z "$HIT" ] && continue
140
+ # shellcheck disable=SC2086
141
+ [ -n "$(skill_hits "$SPATHS" $TARGETS)" ] || continue
152
142
 
153
143
  # A .tsx file is not proof of React — it could be Astro, Solid or Preact.
154
144
  # The packages the skill tracks are the proof, so the manifests decide.
@@ -22,6 +22,7 @@ import { homedir } from "node:os";
22
22
  // ship-gate-projects.sh sourced by ship-gate.sh — project detection, reuse
23
23
  // ship-gate-hook.sh PreToolUse on Bash — no commit without a receipt
24
24
  // ship-mark.sh written by /ship — no PR on a branch /ship never ran on
25
+ // ship-prep.sh run by /ship — prechecks, scope and skills in one call
25
26
  // version-check.sh SessionStart — a line when a newer catalog exists
26
27
  // skill-flags-record.sh SubagentStop — logs each skills-audit verdict
27
28
  // skill-report.mjs read by /skill-report — tallies that log
@@ -55,6 +56,7 @@ const SHIP_LOG_FILENAME = "ship-log.mjs";
55
56
  const SHIP_LOG_PARSE_FILENAME = "ship-log-parse.mjs";
56
57
  const SHIP_LOG_REPORT_FILENAME = "ship-log-report.mjs";
57
58
  const SHIP_MARK_FILENAME = "ship-mark.sh";
59
+ const SHIP_PREP_FILENAME = "ship-prep.sh";
58
60
  const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
59
61
  const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
60
62
 
@@ -163,6 +165,7 @@ export async function setupHook(targetDir = process.cwd()) {
163
165
  SHIP_LOG_PARSE_FILENAME,
164
166
  SHIP_LOG_REPORT_FILENAME,
165
167
  SHIP_MARK_FILENAME,
168
+ SHIP_PREP_FILENAME,
166
169
  ]) {
167
170
  const dest = join(hooksDir, name);
168
171
  await writeFile(dest, await readHookSource(name), { mode: 0o755 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.56.3",
3
+ "version": "2.58.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/gate-config.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 ../scripts/edit-check.sh ../scripts/line-limit.sh ../scripts/lint-ratchet.mjs ../scripts/text-rules.mjs ../scripts/ratchet-baseline.mjs ../scripts/diary-check.mjs ../scripts/diary-lib.mjs ../scripts/ship-log.mjs ../scripts/ship-log-parse.mjs ../scripts/ship-log-report.mjs ../scripts/ship-mark.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/gate-config.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 ../scripts/edit-check.sh ../scripts/line-limit.sh ../scripts/lint-ratchet.mjs ../scripts/text-rules.mjs ../scripts/ratchet-baseline.mjs ../scripts/diary-check.mjs ../scripts/diary-lib.mjs ../scripts/ship-log.mjs ../scripts/ship-log-parse.mjs ../scripts/ship-log-report.mjs ../scripts/ship-mark.sh ../scripts/ship-prep.sh hooks/",
15
15
  "postpack": "git checkout -- README.md && rm -rf hooks"
16
16
  },
17
17
  "engines": {