@spardutti/claude-skills 2.53.1 → 2.54.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 +1 -1
- package/hooks/gate-config.sh +3 -0
- package/hooks/ship-gate-files.sh +61 -7
- package/hooks/ship-gate-hook.sh +28 -12
- package/hooks/ship-gate-projects.sh +1 -1
- package/hooks/ship-gate-structure.sh +33 -3
- package/hooks/ship-gate.sh +4 -10
- package/hooks/ship-log-parse.mjs +102 -0
- package/hooks/ship-log-report.mjs +48 -0
- package/hooks/ship-log.mjs +111 -0
- package/hooks/ship-mark.sh +14 -0
- package/lib/setup-claude-md.mjs +1 -1
- package/lib/setup-hook.mjs +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/hooks/gate-config.sh
CHANGED
|
@@ -8,6 +8,9 @@ GAUNTLET_IGNORE_FILES="*.gen.ts *.gen.tsx *.generated.* */migrations/*.py */alem
|
|
|
8
8
|
# Components are length-checked but not mutated: their mutants are class names and JSX, not
|
|
9
9
|
# behaviour. Override in .claude/gauntlet.conf to mutate them too.
|
|
10
10
|
GAUNTLET_NO_MUTATE='\.(tsx|jsx)$'
|
|
11
|
+
# The /ship log is off unless a conf sets GAUNTLET_SHIP_LOG=1; every project writes to one folder.
|
|
12
|
+
GAUNTLET_SHIP_LOG=""
|
|
13
|
+
GAUNTLET_SHIP_LOG_DIR="${HOME:-}/.claude/ship-logs"
|
|
11
14
|
|
|
12
15
|
load_gauntlet_conf() { # load_gauntlet_conf <project conf> <fallback conf>
|
|
13
16
|
[ -n "${HOME:-}" ] && [ -f "$HOME/.claude/gauntlet.conf" ] && . "$HOME/.claude/gauntlet.conf"
|
package/hooks/ship-gate-files.sh
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
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
2
|
|
|
4
3
|
TEST_RE='(\.|_)(test|spec)\.[^.]+$|(^|/)tests?/'
|
|
5
4
|
|
|
@@ -90,20 +89,25 @@ files_record() { # files_record <context> <owner> <findings>
|
|
|
90
89
|
# NoCoverage is printed exactly like Survived and means something worse: not
|
|
91
90
|
# "a test missed this mutation" but "no test executes this line at all". The
|
|
92
91
|
# 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.
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# nobody can close is what teaches people to --force.
|
|
92
|
+
# PASS — 306 uncovered mutants reported as proven on one commit. A glob-shaped
|
|
93
|
+
# path is mutated whole, so only its changed lines are judged: the rest would
|
|
94
|
+
# report forever, and a finding nobody can close is what teaches people to --force.
|
|
97
95
|
#
|
|
98
96
|
# The status line carries no file name; the line under it does, as
|
|
99
97
|
# file:line:column. They are paired here rather than grepped separately.
|
|
100
|
-
stryker_findings() { # stryker_findings <output> <globbed
|
|
98
|
+
stryker_findings() { # stryker_findings <output> <",path:ranges," per globbed file>
|
|
101
99
|
printf '%s\n' "$1" | awk -v globbed="$2" '
|
|
100
|
+
function changed(rs, l, parts, ab, i) {
|
|
101
|
+
for (i = split(rs, parts, " "); i > 0; i--) { split(parts[i], ab, "-"); if (l >= ab[1] + 0 && l <= ab[2] + 0) return 1 }
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
BEGIN { for (i = split(globbed, g, ","); i > 0; i--) if (g[i] != "") { k = g[i]; sub(/:[^:]*$/, "", k); r = g[i]; sub(/^.*:/, "", r); ranges[k] = r } }
|
|
102
105
|
/^\[Survived\]/ { pend = $0; kind = "s"; next }
|
|
103
106
|
/^\[NoCoverage\]/ { pend = $0; kind = "n"; next }
|
|
104
107
|
pend != "" {
|
|
105
108
|
file = $0; sub(/:[0-9]+:[0-9]+$/, "", file)
|
|
106
|
-
|
|
109
|
+
line = $0; sub(/:[0-9]+$/, "", line); sub(/^.*:/, "", line)
|
|
110
|
+
if (!(file in ranges) || changed(ranges[file], line + 0)) print pend " " $0
|
|
107
111
|
pend = ""
|
|
108
112
|
}
|
|
109
113
|
# A survivor with no location under it is still a survivor, never dropped.
|
|
@@ -120,8 +124,23 @@ stryker_zero_tests() { # stryker_zero_tests <output> <label> <seconds>
|
|
|
120
124
|
echo " On Vitest 5 this is stryker-js#6210: pin vitest@4 until a fixed runner ships."
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
# Read by header name, so a reordered column cannot shift the numbers.
|
|
128
|
+
stryker_counts() { # stryker_counts <output> <label>
|
|
129
|
+
printf '%s\n' "$1" | awk -F'|' -v label="$2" '
|
|
130
|
+
/# killed/ { for (i = 1; i <= NF; i++) { h = $i; gsub(/^ +| +$/, "", h); col[h] = i } next }
|
|
131
|
+
/^ *All files/ && col["# killed"] {
|
|
132
|
+
for (k in col) { v = $col[k]; gsub(/ /, "", v); n[k] = v + 0 }
|
|
133
|
+
t = n["# killed"] + n["# survived"] + n["# timeout"] + n["# no cov"] + n["# errors"]
|
|
134
|
+
printf " %s stryker — %d mutants: %d killed, %d survived, %d uncovered, %d timed out, %d errors\n",
|
|
135
|
+
label, t, n["# killed"], n["# survived"], n["# no cov"], n["# timeout"], n["# errors"]
|
|
136
|
+
exit
|
|
137
|
+
}'
|
|
138
|
+
}
|
|
139
|
+
|
|
123
140
|
# Sets ALL from a Stryker run. Fails when a broken run leaves nothing provable.
|
|
124
141
|
stryker_judge() { # uses OUT GLOBBED FCTX RC owner label T from ship-gate.sh
|
|
142
|
+
echo " $label stryker — mutated: $(while IFS= read -r f; do printf '%s ' "${f#"$owner"/}"; done <<< "$OWNED")"
|
|
143
|
+
stryker_counts "$OUT" "$label"
|
|
125
144
|
ALL=$(stryker_findings "$OUT" "$GLOBBED")
|
|
126
145
|
if stryker_zero_tests "$OUT" "$label" "$T"; then
|
|
127
146
|
ALL=$(grep -v '^\[Survived\]' <<< "$ALL"); [ -n "$ALL" ]; return
|
|
@@ -129,3 +148,38 @@ stryker_judge() { # uses OUT GLOBBED FCTX RC owner label T from ship-gate.sh
|
|
|
129
148
|
[ -n "$FCTX" ] && [ "$RC" -eq 0 ] && files_record "$FCTX" "$owner" "$ALL"
|
|
130
149
|
return 0
|
|
131
150
|
}
|
|
151
|
+
|
|
152
|
+
# Files that decide whether Stryker can run the suite at all; none of them is code it mutates.
|
|
153
|
+
SETUP_RE='(^|/)(stryker\.conf[^/]*|stryker\.config\.[^/]*|vitest\.(config|workspace)\.[^/]*|vite\.config\.[^/]*|jest\.config\.[^/]*|tsconfig[^/]*\.json|package(-lock)?\.json|pnpm-(lock|workspace)\.yaml|yarn\.lock|bun\.lockb?|\.npmrc)$|(^|/)patches/'
|
|
154
|
+
|
|
155
|
+
# A version bump touches package.json on every release and proves nothing about Stryker.
|
|
156
|
+
setup_files() {
|
|
157
|
+
printf '%s\n' "$CHANGED" | grep -E "$SETUP_RE" | while IFS= read -r f; do
|
|
158
|
+
[ -n "$({ git diff "$BASE" -- "$f"; git ls-files --error-unmatch -- "$f" >/dev/null 2>&1 || sed 's/^/+/' "$f"; } 2>/dev/null \
|
|
159
|
+
| grep -E '^[-+]' | grep -vE '^(\+\+\+|---) ' | grep -v '"version"')" ] && printf '%s\n' "$f"
|
|
160
|
+
done
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# A PR can break Stryker (a dropped patch, a test config) with no code to mutate, so a trial
|
|
164
|
+
# runs the suite under it. A project with code to mutate skips it: its real run starts the same way.
|
|
165
|
+
setup_check() {
|
|
166
|
+
[ -z "$GAUNTLET_MUTATE" ] || return 0
|
|
167
|
+
local o b bin label key out rc t0 mutated
|
|
168
|
+
mutated=$(printf '%s\n' "$FILES" | grep -vE "$GAUNTLET_NO_MUTATE" | while IFS= read -r f; do [ -n "$f" ] && owner_of "$f"; done)
|
|
169
|
+
for o in $(setup_files | while IFS= read -r f; do owner_of "$f"; done | sort -u); do
|
|
170
|
+
grep -qxF "$o" <<< "$mutated" && continue
|
|
171
|
+
b=""; [ "$o" != . ] && b="$o/"; [ "$o" = . ] && label="<repo root>" || label="$o/"
|
|
172
|
+
grep -qs '@stryker-mutator/core' "${b}package.json" || continue
|
|
173
|
+
key="/tmp/claude-shipgate-project-$(project_key "$o" dryrun)"
|
|
174
|
+
[ -f "$key" ] && { echo " $label stryker setup — ok, not re-run: its trial run passed"; continue; }
|
|
175
|
+
bin=$(stryker_bin "$b"); t0=$SECONDS; out=$(cd "${b:-.}" && $bin run --dryRunOnly --concurrency "$(gate_workers)" 2>&1); rc=$?
|
|
176
|
+
if [ $rc -eq 0 ]; then echo " $label stryker setup ($((SECONDS - t0))s) — ok, its trial run passed"; touch "$key"
|
|
177
|
+
elif grep -qE 'initial test run|DryRunExecutor' <<< "$out"; then
|
|
178
|
+
echo " $label stryker setup ($((SECONDS - t0))s) — this change breaks Stryker's own test run:"
|
|
179
|
+
printf '%s\n' "$out" | tail -12 | sed 's/^/ /'; STATUS=1
|
|
180
|
+
else
|
|
181
|
+
echo " $label stryker setup ($((SECONDS - t0))s) — UNPROVEN, the trial run could not start:"
|
|
182
|
+
printf '%s\n' "$out" | tail -6 | sed 's/^/ /'; stryker_elsewhere stryker "$b"; [ "$STATUS" = 0 ] && STATUS=2
|
|
183
|
+
fi
|
|
184
|
+
done
|
|
185
|
+
}
|
package/hooks/ship-gate-hook.sh
CHANGED
|
@@ -42,6 +42,21 @@ case "$CMD" in
|
|
|
42
42
|
*) exit 0 ;;
|
|
43
43
|
esac
|
|
44
44
|
|
|
45
|
+
json_escape() {
|
|
46
|
+
local s="$1"
|
|
47
|
+
s="${s//\\/\\\\}"
|
|
48
|
+
s="${s//\"/\\\"}"
|
|
49
|
+
s="${s//$'\n'/\\n}"
|
|
50
|
+
s="${s//$'\r'/\\r}"
|
|
51
|
+
s="${s//$'\t'/\\t}"
|
|
52
|
+
printf '"%s"' "$s"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
deny() {
|
|
56
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$(json_escape "$1")"
|
|
57
|
+
exit 0
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
GATE="$PROJECT_DIR/.claude/hooks/ship-gate.sh"
|
|
46
61
|
[ -x "$GATE" ] || exit 0
|
|
47
62
|
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
@@ -84,6 +99,18 @@ BRANCH=$(pr_branch)
|
|
|
84
99
|
KEY=$(CLAUDE_PROJECT_DIR="$TARGET" bash "$GATE" --key 2>/dev/null)
|
|
85
100
|
[ -z "$KEY" ] && exit 0
|
|
86
101
|
|
|
102
|
+
# A gate run by hand skips /ship's audit and review, so the PR waits for /ship itself.
|
|
103
|
+
# A merge whose branch gh cannot name is left to the receipt alone.
|
|
104
|
+
MARKER="$PROJECT_DIR/.claude/hooks/ship-mark.sh"
|
|
105
|
+
case "$CMD" in *"gh pr create"*) MARK_BRANCH=${BRANCH:-$(git branch --show-current)} ;; *) MARK_BRANCH=$BRANCH ;; esac
|
|
106
|
+
if [ -f "$MARKER" ] && [ -n "$MARK_BRANCH" ] && ! CLAUDE_PROJECT_DIR="$TARGET" bash "$MARKER" check "$MARK_BRANCH"; then
|
|
107
|
+
deny "BLOCKED: /ship has not run on branch $MARK_BRANCH.
|
|
108
|
+
|
|
109
|
+
Load the /ship command with the Skill tool and follow it from the start. A gate run
|
|
110
|
+
by hand is not enough: /ship also runs the skills audit and the code review.
|
|
111
|
+
The gate's earlier pass is reused, so only those two steps cost time."
|
|
112
|
+
fi
|
|
113
|
+
|
|
87
114
|
RECEIPT="/tmp/claude-shipgate-$KEY"
|
|
88
115
|
if [ -f "$RECEIPT" ]; then
|
|
89
116
|
exit 0
|
|
@@ -111,15 +138,4 @@ To publish without the gate, say so out loud and run:
|
|
|
111
138
|
|
|
112
139
|
bash .claude/hooks/ship-gate.sh --force"
|
|
113
140
|
|
|
114
|
-
|
|
115
|
-
local s="$1"
|
|
116
|
-
s="${s//\\/\\\\}"
|
|
117
|
-
s="${s//\"/\\\"}"
|
|
118
|
-
s="${s//$'\n'/\\n}"
|
|
119
|
-
s="${s//$'\r'/\\r}"
|
|
120
|
-
s="${s//$'\t'/\\t}"
|
|
121
|
-
printf '"%s"' "$s"
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$(json_escape "$MSG")"
|
|
125
|
-
exit 0
|
|
141
|
+
deny "$MSG"
|
|
@@ -92,7 +92,7 @@ changed_files() {
|
|
|
92
92
|
# Ignored extensions are left out; see the comment where ship-gate.sh first calls this.
|
|
93
93
|
receipt_key() {
|
|
94
94
|
{ printf '%s\n' "$PROJECT_DIR"
|
|
95
|
-
changed_files | grep -viE "\.($GAUNTLET_IGNORE_EXT)$" | while IFS= read -r f; do
|
|
95
|
+
{ changed_files | grep -viE "\.($GAUNTLET_IGNORE_EXT)$"; setup_files; } | LC_ALL=C sort -u | while IFS= read -r f; do
|
|
96
96
|
[ -n "$f" ] || continue
|
|
97
97
|
printf '%s\n' "$f"
|
|
98
98
|
[ -f "$f" ] && cat "$f"
|
|
@@ -1,6 +1,4 @@
|
|
|
1
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
2
|
|
|
5
3
|
STRUCTURE_JS_KINDS="components|hooks|api|queries|schemas|types|utils|stores|test"
|
|
6
4
|
STRUCTURE_KIND_FILE='(^|[._])(router|routes|controller|service|schema|model)s?\.[cm]?[jt]s$|(Router|Routes|Controller|Service|Schema|Model)s?\.[cm]?[jt]s$'
|
|
@@ -69,6 +67,7 @@ py_misplaced() { # py_misplaced <path>
|
|
|
69
67
|
fi
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
# Only ADDED files are judged, so a repo laid out the old way cannot get worse and need not move.
|
|
72
71
|
structure_check() {
|
|
73
72
|
bad=""; count=0
|
|
74
73
|
while IFS= read -r f; do
|
|
@@ -115,13 +114,44 @@ gate_report() {
|
|
|
115
114
|
# A FAIL goes to the diary /add-rule reads, so a test gap the gate keeps finding can become a rule.
|
|
116
115
|
gate_exit() {
|
|
117
116
|
echo "ship-gate: full output in $REPORT"
|
|
118
|
-
[ "$1" = 1 ] || return 0
|
|
117
|
+
[ "$1" = 1 ] || [ "$GAUNTLET_SHIP_LOG" = 1 ] || return 0
|
|
119
118
|
exec 1>&3 2>&4
|
|
120
119
|
# A child left running (a dev server) can hold tee open forever; give it 2s, not a hang.
|
|
121
120
|
for _ in 1 2 3 4 5 6 7 8 9 10; do [ -n "$TEE_PID" ] && kill -0 "$TEE_PID" 2>/dev/null || break; sleep 0.2; done
|
|
121
|
+
[ "$1" = 1 ] && gate_diary
|
|
122
|
+
ship_log "$REPORT" "$1"
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
gate_diary() {
|
|
122
127
|
local diary; diary=$(diary_dir "$HERE")
|
|
123
128
|
mkdir -p "$diary"
|
|
124
129
|
printf '{"ts":"%s","branch":"%s","status":"FAIL","report":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
125
130
|
"$(git branch --show-current 2>/dev/null | json_string)" "$(json_string < "$REPORT")" \
|
|
126
131
|
>> "$diary/gate-findings.jsonl"
|
|
127
132
|
}
|
|
133
|
+
|
|
134
|
+
# Every run goes to the /ship log when it is on; a broken log never changes the verdict.
|
|
135
|
+
ship_log() { # ship_log <report> <exit code>
|
|
136
|
+
[ "$GAUNTLET_SHIP_LOG" = 1 ] && command -v node >/dev/null || return 0
|
|
137
|
+
SHIP_LOG_TEST_RE="$TEST_RE" SHIP_LOG_SOURCE_EXT="$GAUNTLET_SOURCE_EXT" node "$HERE/ship-log.mjs" gate "$1" "$2" "$SECONDS" "$BASE" >/dev/null 2>&1 || true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
gate_force() {
|
|
141
|
+
printf 'FORCED %s\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
142
|
+
echo "ship-gate: FORCED — receipt written without running any check."
|
|
143
|
+
local r; r=$(mktemp); echo "ship-gate: FORCED" > "$r"
|
|
144
|
+
ship_log "$r" 0; rm -f "$r"
|
|
145
|
+
exit 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Nothing to mutate, but a Stryker trial run may still have been unable to start.
|
|
149
|
+
no_code_exit() {
|
|
150
|
+
echo "ship-gate: no changed code files — nothing to check"
|
|
151
|
+
if [ "$STATUS" = 2 ]; then
|
|
152
|
+
printf 'ship-gate: UNPROVEN in %ss — nothing is wrong, but nothing was proven either\n' "$SECONDS"
|
|
153
|
+
printf 'UNPROVEN %s setup-trial\n' "$(date -u +%FT%TZ)" > "$RECEIPT"; exit 2
|
|
154
|
+
fi
|
|
155
|
+
printf 'PASS %s no-code-changes\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
156
|
+
exit 0
|
|
157
|
+
}
|
package/hooks/ship-gate.sh
CHANGED
|
@@ -100,11 +100,7 @@ CHANGED=$(changed_files)
|
|
|
100
100
|
RECEIPT_KEY=$(receipt_key); RECEIPT="/tmp/claude-shipgate-$RECEIPT_KEY"
|
|
101
101
|
|
|
102
102
|
if [ "$MODE" = key ]; then printf '%s\n' "$RECEIPT_KEY"; exit 0; fi
|
|
103
|
-
|
|
104
|
-
printf 'FORCED %s\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
105
|
-
echo "ship-gate: FORCED — receipt written without running any check."
|
|
106
|
-
exit 0
|
|
107
|
-
fi
|
|
103
|
+
[ "$MODE" = force ] && gate_force
|
|
108
104
|
gate_report "$RECEIPT_KEY"
|
|
109
105
|
SOURCED=$(printf '%s\n' "$CHANGED" | grep -E "\.($GAUNTLET_SOURCE_EXT)$" \
|
|
110
106
|
| grep -vE "$TEST_RE|(^|/)\.stryker-tmp/" | while read -r f; do
|
|
@@ -134,7 +130,7 @@ if [ -n "$IGNORED" ]; then
|
|
|
134
130
|
fi
|
|
135
131
|
|
|
136
132
|
[ -n "$FILES" ] && echo "ship-gate: $(printf '%s\n' "$FILES" | wc -l) changed code file(s), base $(git rev-parse --short "$BASE")"
|
|
137
|
-
STATUS=0; structure_check ||
|
|
133
|
+
STATUS=0; structure_check || STATUS=1; [ "$STATUS" = 1 ] || setup_check; [ "$STATUS" = 1 ] && [ -z "$FILES" ] && structure_fail
|
|
138
134
|
if [ -z "$FILES" ]; then
|
|
139
135
|
# "I recognised nothing" is not "there is nothing", and the gate used to report
|
|
140
136
|
# both as a PASS. A Godot repo changed only .gd files, which no extension in
|
|
@@ -154,9 +150,7 @@ if [ -z "$FILES" ]; then
|
|
|
154
150
|
printf 'UNPROVEN %s unknown-extensions\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
155
151
|
exit 2
|
|
156
152
|
fi
|
|
157
|
-
|
|
158
|
-
printf 'PASS %s no-code-changes\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
159
|
-
exit 0
|
|
153
|
+
no_code_exit
|
|
160
154
|
fi
|
|
161
155
|
|
|
162
156
|
# ------------------------------------------------- check 1: file length (hard)
|
|
@@ -224,7 +218,7 @@ for owner in $OWNERS; do
|
|
|
224
218
|
# superset of the diff, slower than a range but never less than what changed.
|
|
225
219
|
case "$rel" in
|
|
226
220
|
*[][*?{}]*) FLAGS="$FLAGS,$(printf '%s' "$rel" | sed 's/[][*?{}]/?/g')"
|
|
227
|
-
GLOBBED="$GLOBBED$rel," ;;
|
|
221
|
+
GLOBBED="$GLOBBED$rel:$(echo $hunks)," ;;
|
|
228
222
|
*) for r in $hunks; do FLAGS="$FLAGS,$rel:$r"; done ;;
|
|
229
223
|
esac
|
|
230
224
|
done <<< "$OWNED"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const PROJECT = /^ {2}(\S.*?) (stryker|mutmut|config)(?: \((\d+)s\))? — (.*)$/;
|
|
5
|
+
const SURVIVOR = /^ {6}\[(Survived|NoCoverage)\] (\S+)\s+(\S+)$/;
|
|
6
|
+
const MUTMUT = /^ {6}(\S+__mutmut_\d+)$/;
|
|
7
|
+
const COUNTS = /^(\d+) mutants: (\d+) killed, (\d+) survived, (\d+) uncovered, (\d+) timed out, (\d+) errors$/;
|
|
8
|
+
const VERDICT = /^ship-gate: (PASS|FAIL|UNPROVEN|FORCED)(?: in (\d+)s)?/;
|
|
9
|
+
|
|
10
|
+
function projectFor(run, label, tool) {
|
|
11
|
+
let p = run.projects.find((x) => x.label === label && x.tool === tool);
|
|
12
|
+
if (!p) {
|
|
13
|
+
p = { label, tool, seconds: null, result: null, reused: [], mutated: [], counts: null, survivors: [] };
|
|
14
|
+
run.projects.push(p);
|
|
15
|
+
}
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function applyProject(p, seconds, text) {
|
|
20
|
+
if (seconds) p.seconds = Number(seconds);
|
|
21
|
+
const counts = text.match(COUNTS);
|
|
22
|
+
if (counts) {
|
|
23
|
+
const [, total, killed, survived, uncovered, timedOut, errors] = counts.map(Number);
|
|
24
|
+
p.counts = { total, killed, survived, uncovered, timedOut, errors };
|
|
25
|
+
} else if (text.startsWith("mutated:")) p.mutated = text.slice(8).trim().split(/\s+/);
|
|
26
|
+
else if (text.startsWith("ok, not re-run: passed and unchanged since:")) p.reused = text.split(":").pop().trim().split(/\s+/);
|
|
27
|
+
else if (text.startsWith("ok, not re-run")) p.result = "cached";
|
|
28
|
+
else if (text.startsWith("ok,")) p.result = "ok";
|
|
29
|
+
else if (text.startsWith("these lines can break")) p.result = "survivors";
|
|
30
|
+
else if (text.startsWith("the run failed")) p.result = "failed";
|
|
31
|
+
else if (text.startsWith("UNPROVEN")) p.result = "unproven";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readSection(run, line, state) {
|
|
35
|
+
if (line.startsWith("STRUCTURE — ")) { run.structure = line.slice(12); state.list = line.includes("outside") ? run.misplaced : null; return; }
|
|
36
|
+
if (line.startsWith("FILE LENGTH — ")) { run.length = line.slice(14); state.list = line.includes("over the limit") ? run.overLimit : null; return; }
|
|
37
|
+
if (line.startsWith("ship-gate: not gated")) { state.list = run.ignored; return; }
|
|
38
|
+
const scope = line.match(/^ship-gate:\s+(\d+) changed code file\(s\)/);
|
|
39
|
+
if (scope) run.scope = Number(scope[1]);
|
|
40
|
+
const verdict = line.match(VERDICT);
|
|
41
|
+
if (verdict) run.verdict = verdict[1];
|
|
42
|
+
if (line.startsWith("ship-gate: no changed code files")) run.verdict = "PASS";
|
|
43
|
+
if (!/^ {2}\S/.test(line) || line.includes("Move them") || line.includes("Splitting a file")) state.list = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readLine(run, line, state) {
|
|
47
|
+
const project = line.match(PROJECT);
|
|
48
|
+
if (project) {
|
|
49
|
+
state.project = projectFor(run, project[1], project[2]);
|
|
50
|
+
applyProject(state.project, project[3], project[4]);
|
|
51
|
+
state.list = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const survivor = line.match(SURVIVOR) ?? line.match(MUTMUT);
|
|
55
|
+
if (survivor && state.project) {
|
|
56
|
+
state.project.survivors.push(survivor.length > 2 ? { kind: survivor[1], mutator: survivor[2], at: survivor[3] } : { kind: "Survived", at: survivor[1] });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (state.list && /^ {2}\S/.test(line)) { state.list.push(line.trim()); return; }
|
|
60
|
+
readSection(run, line, state);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseReport(text) {
|
|
64
|
+
const run = { verdict: null, scope: 0, ignored: [], structure: null, misplaced: [], length: null, overLimit: [], projects: [] };
|
|
65
|
+
const state = { project: null, list: null };
|
|
66
|
+
for (const line of text.split("\n")) readLine(run, line, state);
|
|
67
|
+
// The verdict is the last line the gate prints; a run killed before it never earned one.
|
|
68
|
+
run.verdict ??= "INTERRUPTED";
|
|
69
|
+
return run;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function git(args) {
|
|
73
|
+
const r = spawnSync("git", args, { encoding: "utf8", maxBuffer: 1 << 26 });
|
|
74
|
+
return r.status === 0 ? r.stdout.trim() : "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function kindOf(path, testRe, sourceExt) {
|
|
78
|
+
if (testRe && new RegExp(testRe).test(path)) return "tests";
|
|
79
|
+
return sourceExt && new RegExp(`\\.(${sourceExt})$`).test(path) ? "code" : "other";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function untracked() {
|
|
83
|
+
return git(["ls-files", "--others", "--exclude-standard"]).split("\n").filter(Boolean).map((f) => {
|
|
84
|
+
let text = "";
|
|
85
|
+
try { text = readFileSync(f, "utf8"); } catch { /* unreadable: count the file, not its lines */ }
|
|
86
|
+
return [text ? text.split("\n").length - (text.endsWith("\n") ? 1 : 0) : 0, 0, f];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// What the PR will hold: the fork point against the working tree, plus files not yet added.
|
|
91
|
+
export function prSize(base, testRe, sourceExt) {
|
|
92
|
+
const fork = git(["merge-base", base, "HEAD"]).trim() || base;
|
|
93
|
+
const rows = git(["diff", "--numstat", "--no-renames", fork]).split("\n").filter(Boolean).map((l) => l.split("\t"));
|
|
94
|
+
const size = { files: 0, added: 0, removed: 0, code: { files: 0, added: 0, removed: 0 }, tests: { files: 0, added: 0, removed: 0 }, other: { files: 0, added: 0, removed: 0 } };
|
|
95
|
+
for (const [a, r, f] of [...rows, ...untracked()]) {
|
|
96
|
+
if (/(^|\/)\.claude\//.test(f)) continue;
|
|
97
|
+
const add = Number(a) || 0, rem = Number(r) || 0, bucket = size[kindOf(f, testRe, sourceExt)];
|
|
98
|
+
size.files++; size.added += add; size.removed += rem;
|
|
99
|
+
bucket.files++; bucket.added += add; bucket.removed += rem;
|
|
100
|
+
}
|
|
101
|
+
return size;
|
|
102
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
function records(dir) {
|
|
5
|
+
const read = (d) => (existsSync(d) ? readdirSync(d).filter((f) => f.endsWith(".json")).map((f) => join(d, f)) : []);
|
|
6
|
+
return [...read(dir), ...read(join(dir, "open"))].flatMap((f) => {
|
|
7
|
+
try {
|
|
8
|
+
const r = JSON.parse(readFileSync(f, "utf8"));
|
|
9
|
+
return r?.v === 1 && Array.isArray(r.gate) && Array.isArray(r.findings) ? [r] : [];
|
|
10
|
+
} catch { return []; }
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function tally(findings) {
|
|
15
|
+
const by = {};
|
|
16
|
+
for (const f of findings) {
|
|
17
|
+
const s = (by[f.by.split(":")[0]] ??= { found: 0, confirmed: 0, refuted: 0, unconfirmed: 0, fixed: 0 });
|
|
18
|
+
s.found++; s[f.verdict]++; if (f.fixed) s.fixed++;
|
|
19
|
+
}
|
|
20
|
+
return by;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findingCell(by = {}) {
|
|
24
|
+
const parts = Object.entries(by).map(([src, s]) => `${src} ${s.found}/${s.confirmed}/${s.fixed}`);
|
|
25
|
+
return parts.join(" · ") || "-";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function row(r) {
|
|
29
|
+
const s = r.summary ?? { minutes: null, gateRuns: r.gate.length, gateSeconds: r.gate.reduce((n, g) => n + (g.seconds ?? 0), 0), findings: tally(r.findings) };
|
|
30
|
+
const size = r.size ? `${r.size.files} +${r.size.added} -${r.size.removed}` : "-";
|
|
31
|
+
return [r.app, (r.finished ?? r.started).slice(0, 10), r.pr ? `#${r.pr}` : r.branch, r.status, s.minutes ?? "-", s.gateRuns, `${s.gateSeconds}s`, size, findingCell(s.findings)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function table(rows) {
|
|
35
|
+
const head = ["app", "date", "pr", "status", "min", "gate runs", "gate time", "files +/-", "findings found/confirmed/fixed"];
|
|
36
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i]).length)));
|
|
37
|
+
for (const r of [head, ...rows]) console.log(r.map((c, i) => String(c).padEnd(widths[i])).join(" ").trimEnd());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function printReport(dir) {
|
|
41
|
+
const all = records(dir).sort((a, b) => (a.finished ?? a.started).localeCompare(b.finished ?? b.started));
|
|
42
|
+
if (all.length === 0) {
|
|
43
|
+
console.log(`ship-log: no logs in ${dir}. Turn logging on with GAUNTLET_SHIP_LOG=1 in ~/.claude/gauntlet.conf.`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
table(all.map(row));
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { git, parseReport, prSize } from "./ship-log-parse.mjs";
|
|
6
|
+
import { printReport, tally } from "./ship-log-report.mjs";
|
|
7
|
+
|
|
8
|
+
const HERE = dirname(resolve(process.argv[1]));
|
|
9
|
+
const VERDICTS = ["confirmed", "refuted", "unconfirmed"];
|
|
10
|
+
const now = () => new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
11
|
+
|
|
12
|
+
// gate-config.sh owns the config files and their order, so the gate and this agree on "on".
|
|
13
|
+
function conf() {
|
|
14
|
+
const root = git(["rev-parse", "--show-toplevel"]) || process.cwd();
|
|
15
|
+
const script = 'cd "$2" && . "$1/gate-config.sh" || exit 1; load_gauntlet_conf .claude/gauntlet.conf "$1/../gauntlet.conf"; printf "\\nSHIP_LOG_ON=%s\\nSHIP_LOG_DIR=%s\\n" "$GAUNTLET_SHIP_LOG" "$GAUNTLET_SHIP_LOG_DIR"';
|
|
16
|
+
const out = spawnSync("bash", ["-c", script, "_", HERE, root], { encoding: "utf8" }).stdout;
|
|
17
|
+
const value = (key) => out.split("\n").findLast((l) => l.startsWith(`${key}=`))?.slice(key.length + 1) ?? "";
|
|
18
|
+
return { on: value("SHIP_LOG_ON") === "1", dir: value("SHIP_LOG_DIR") || join(process.env.HOME ?? "", ".claude", "ship-logs"), root };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function identity(root) {
|
|
22
|
+
const origin = git(["remote", "get-url", "origin"]);
|
|
23
|
+
const app = basename(origin || root).replace(/\.git$/, "");
|
|
24
|
+
return { app, branch: git(["branch", "--show-current"]) || "detached" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function openPath(dir, { app, branch }) {
|
|
28
|
+
return join(dir, "open", `${app}__${branch.replace(/[^\w.-]+/g, "-")}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function load(path, id) {
|
|
32
|
+
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
|
|
33
|
+
return { v: 1, ...id, started: now(), status: "open", pr: null, base: null, size: null, gate: [], steps: {}, findings: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function save(path, record) {
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
38
|
+
record.updated = now();
|
|
39
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function summarize(record) {
|
|
43
|
+
const by = tally(record.findings);
|
|
44
|
+
const minutes = (Date.parse(record.finished) - Date.parse(record.started)) / 60000;
|
|
45
|
+
return { minutes: Math.round(minutes * 10) / 10, gateRuns: record.gate.length, gateSeconds: record.gate.reduce((n, r) => n + (r.seconds ?? 0), 0), findings: by };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function finalize(dir, path, record, tag, status, reason) {
|
|
49
|
+
Object.assign(record, { status, reason: reason ?? null, finished: now() });
|
|
50
|
+
record.summary = summarize(record);
|
|
51
|
+
const stem = `${record.app}_${record.finished.slice(0, 10)}_${tag}`;
|
|
52
|
+
let out = join(dir, `${stem}.json`);
|
|
53
|
+
for (let i = 2; existsSync(out); i++) out = join(dir, `${stem}-${i}.json`);
|
|
54
|
+
save(path, record);
|
|
55
|
+
renameSync(path, out);
|
|
56
|
+
console.log(`ship-log: saved ${out}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A ship that never reached pr or end was abandoned; keep it, but never fold it into the next one.
|
|
60
|
+
function restart(dir, path, record, id) {
|
|
61
|
+
if (existsSync(path)) finalize(dir, path, record, id.branch.replace(/[^\w.-]+/g, "-"), "abandoned");
|
|
62
|
+
save(path, load(path, id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addGateRun(record, [report, exit, seconds, base]) {
|
|
66
|
+
const text = readFileSync(report, "utf8");
|
|
67
|
+
record.gate.push({ ts: now(), exit: Number(exit), seconds: Number(seconds), ...parseReport(text), report: text });
|
|
68
|
+
if (base) { record.base = base; record.size = prSize(base, process.env.SHIP_LOG_TEST_RE ?? "", process.env.SHIP_LOG_SOURCE_EXT ?? ""); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function addFinding(record, json) {
|
|
72
|
+
const f = JSON.parse(json ?? "");
|
|
73
|
+
if (typeof f.by !== "string" || typeof f.what !== "string" || !VERDICTS.includes(f.verdict) || typeof f.fixed !== "boolean") {
|
|
74
|
+
throw new Error(`a finding needs by, what, verdict (${VERDICTS.join("|")}) and fixed (true|false)`);
|
|
75
|
+
}
|
|
76
|
+
record.findings.push({ ts: now(), ...f });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function addStep(record, [name, edge]) {
|
|
80
|
+
if (!name || !["start", "end"].includes(edge)) throw new Error("usage: step <name> start|end");
|
|
81
|
+
const step = (record.steps[name] ??= {});
|
|
82
|
+
step[edge] = now();
|
|
83
|
+
if (step.start && step.end) step.seconds = Math.round((Date.parse(step.end) - Date.parse(step.start)) / 1000);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function run(mode, args) {
|
|
87
|
+
const c = conf();
|
|
88
|
+
if (mode === "report") return printReport(args[0] ?? c.dir);
|
|
89
|
+
if (mode === "on") return c.on ? 0 : 1;
|
|
90
|
+
if (!c.on) return 0;
|
|
91
|
+
const id = identity(c.root), path = openPath(c.dir, id), record = load(path, id);
|
|
92
|
+
if (mode === "start") return restart(c.dir, path, record, id), 0;
|
|
93
|
+
if (mode === "gate") addGateRun(record, args);
|
|
94
|
+
else if (mode === "finding") addFinding(record, args[0]);
|
|
95
|
+
else if (mode === "step") addStep(record, args);
|
|
96
|
+
else if (mode === "pr" && /^\d+$/.test(args[0] ?? "")) {
|
|
97
|
+
record.pr = Number(args[0]);
|
|
98
|
+
return finalize(c.dir, path, record, `pr${args[0]}`, "pr-opened"), 0;
|
|
99
|
+
}
|
|
100
|
+
else if (mode === "end") return finalize(c.dir, path, record, id.branch.replace(/[^\w.-]+/g, "-"), "stopped", args.join(" ") || null), 0;
|
|
101
|
+
else throw new Error("usage: ship-log.mjs on | start | gate <report> <exit> <seconds> <base> | step <name> start|end | finding '<json>' | pr <number> | end [reason] | report [dir]");
|
|
102
|
+
save(path, record);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
process.exit(run(process.argv[2], process.argv.slice(3)) ?? 0);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`ship-log: ${err.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
cd "${CLAUDE_PROJECT_DIR:-$PWD}" 2>/dev/null || exit 1
|
|
3
|
+
# The shared .git dir, so a worktree's mark is found from the main folder.
|
|
4
|
+
COMMON=$(cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd) || exit 1
|
|
5
|
+
BRANCH=${2:-$(git branch --show-current 2>/dev/null)}
|
|
6
|
+
[ -n "$BRANCH" ] || exit 1
|
|
7
|
+
MARK="/tmp/claude-ship-$(printf '%s %s' "$COMMON" "$BRANCH" | cksum | cut -d' ' -f1)"
|
|
8
|
+
|
|
9
|
+
# A mark lasts 12 hours, so a branch reused later (dev, for a release) needs /ship again.
|
|
10
|
+
case "${1:-}" in
|
|
11
|
+
begin) touch "$MARK" ;;
|
|
12
|
+
check) [ -n "$(find "$MARK" -mmin -720 2>/dev/null)" ] ;;
|
|
13
|
+
*) echo "usage: ship-mark.sh begin | check [branch]" >&2; exit 2 ;;
|
|
14
|
+
esac
|
package/lib/setup-claude-md.mjs
CHANGED
|
@@ -26,7 +26,7 @@ The hooks and skills in \`.claude/\` come from \`@spardutti/claude-skills\`. Fac
|
|
|
26
26
|
- **Your own skill:** write \`.claude/skills/<name>/SKILL.md\` with \`name\` and \`description\`, then add \`<name>\` to \`"custom"\` in \`.claude/.claude-skills.json\`. Never add it to \`"skills"\`: a sync deletes anything there that is not in the catalog.
|
|
27
27
|
- **Make it mandatory on some files:** put \`gate-paths: "src/**/*.tsx"\` under \`metadata:\`, quoted. A bare top-level \`paths:\` makes Claude Code drop the skill. Add \`tracks: react@19\` to fire only when the project depends on that package.
|
|
28
28
|
- **Hook settings** live in \`.claude/gauntlet.conf\` (shell syntax): \`GAUNTLET_TEST\`, \`GAUNTLET_TYPECHECK\`, \`GAUNTLET_BASE\` (the trunk branch, if not develop/dev/main), \`GAUNTLET_MAX_LINES\`, \`GAUNTLET_IGNORE_FILES\`, \`GAUNTLET_OFF=1\`.
|
|
29
|
-
- **
|
|
29
|
+
- **Shipping:** open, merge or release a PR only through the \`/ship\` command; load it every time, even when you remember the steps. Never run \`ship-gate.sh\` with \`--force\` or \`--baseline\` unless the user asks.
|
|
30
30
|
- **Never edit \`.claude/hooks/\`** — a sync overwrites it. Update with \`npx @spardutti/claude-skills --sync\`.`;
|
|
31
31
|
|
|
32
32
|
const BLOCKS = [
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -21,10 +21,13 @@ import { homedir } from "node:os";
|
|
|
21
21
|
// gate-config.sh sourced by ship-gate.sh — shared helpers
|
|
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
|
+
// ship-mark.sh written by /ship — no PR on a branch /ship never ran on
|
|
24
25
|
// version-check.sh SessionStart — a line when a newer catalog exists
|
|
25
26
|
// skill-flags-record.sh SubagentStop — logs each skills-audit verdict
|
|
26
27
|
// skill-report.mjs read by /skill-report — tallies that log
|
|
27
28
|
// diary-check.mjs run by /ship — says when /add-rule has repeats to look at
|
|
29
|
+
// ship-log.mjs the /ship log, when GAUNTLET_SHIP_LOG=1 — every gate run
|
|
30
|
+
// and finding, one file per ship, in one folder
|
|
28
31
|
// edit-check.sh PostToolUse — line-limit.sh, lint-ratchet.mjs and
|
|
29
32
|
// text-rules.mjs on each edited file
|
|
30
33
|
|
|
@@ -48,6 +51,10 @@ const TEXT_RULES_FILENAME = "text-rules.mjs";
|
|
|
48
51
|
const RATCHET_BASELINE_FILENAME = "ratchet-baseline.mjs";
|
|
49
52
|
const DIARY_CHECK_FILENAME = "diary-check.mjs";
|
|
50
53
|
const DIARY_LIB_FILENAME = "diary-lib.mjs";
|
|
54
|
+
const SHIP_LOG_FILENAME = "ship-log.mjs";
|
|
55
|
+
const SHIP_LOG_PARSE_FILENAME = "ship-log-parse.mjs";
|
|
56
|
+
const SHIP_LOG_REPORT_FILENAME = "ship-log-report.mjs";
|
|
57
|
+
const SHIP_MARK_FILENAME = "ship-mark.sh";
|
|
51
58
|
const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
|
|
52
59
|
const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
|
|
53
60
|
|
|
@@ -152,6 +159,10 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
152
159
|
RATCHET_BASELINE_FILENAME,
|
|
153
160
|
DIARY_CHECK_FILENAME,
|
|
154
161
|
DIARY_LIB_FILENAME,
|
|
162
|
+
SHIP_LOG_FILENAME,
|
|
163
|
+
SHIP_LOG_PARSE_FILENAME,
|
|
164
|
+
SHIP_LOG_REPORT_FILENAME,
|
|
165
|
+
SHIP_MARK_FILENAME,
|
|
155
166
|
]) {
|
|
156
167
|
const dest = join(hooksDir, name);
|
|
157
168
|
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.
|
|
3
|
+
"version": "2.54.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 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 hooks/",
|
|
15
15
|
"postpack": "git checkout -- README.md && rm -rf hooks"
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|