@spardutti/claude-skills 2.27.0 → 2.28.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/lib/setup-hook.mjs +61 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
|
|
|
133
133
|
|---------|--------------|
|
|
134
134
|
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **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 |
|
|
135
135
|
| `/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` |
|
|
136
|
-
| `/plan-feature` | Integration-first feature planning — 3 parallel subagents scan for reusable code, patterns
|
|
136
|
+
| `/plan-feature` | Integration-first feature planning, then building — 3 parallel subagents scan for reusable code, patterns and touch points, and the plan lands with a contract and a per-worker file split; on your go it dispatches parallel implementation workers that each load their own skills and report back as one diff |
|
|
137
137
|
| `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
|
|
138
138
|
| `/deep-review` | Multi-agent deep code review — 5 parallel subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
|
|
139
139
|
| `/test-review` | Write-then-verify test review — scopes to the diff, runs red-green + mutation gates, then an isolated read-only subagent proves each test would catch a regression instead of rubber-stamping it |
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -4,7 +4,10 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
// PreToolUse gate on Write|Edit|MultiEdit. Blocks the tool call unless
|
|
7
|
-
// a
|
|
7
|
+
// a marker file exists at /tmp/claude-skill-gate-<KEY>, where KEY is the
|
|
8
|
+
// subagent's agent_id when the call comes from one, and the session_id
|
|
9
|
+
// otherwise — a subagent reports the parent's session_id, so keying on that
|
|
10
|
+
// alone let a worker inherit the main thread's cleared gate.
|
|
8
11
|
// Per-session (not per-prompt) so simple confirmations like "yes" don't
|
|
9
12
|
// re-lock the gate after evaluation has already happened in the session.
|
|
10
13
|
// The PostToolUse hook on Skill creates the marker automatically; for
|
|
@@ -30,10 +33,25 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
30
33
|
exit 0
|
|
31
34
|
fi
|
|
32
35
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
36
|
+
# A subagent reports the PARENT's session_id — measured, not assumed: a probe
|
|
37
|
+
# hook dumped its own stdin from the main thread and from inside a subagent and
|
|
38
|
+
# the two ids were identical. So a worker spawned to implement part of a plan
|
|
39
|
+
# inherited the main thread's cleared markers and edited with none of the
|
|
40
|
+
# skills loaded in its own context.
|
|
41
|
+
#
|
|
42
|
+
# agent_id is present only inside a subagent, and holds steady across that
|
|
43
|
+
# subagent's turns, so keying on it gives each worker its own namespace and
|
|
44
|
+
# costs it one Skill call per skill, not one per turn.
|
|
45
|
+
AGENT_ID=$(printf '%s' "$INPUT" | grep -o '"agent_id":"[^"]*"' | head -1 | sed 's/"agent_id":"//; s/"$//')
|
|
46
|
+
KEY="$SESSION_ID"
|
|
47
|
+
if [ -n "$AGENT_ID" ]; then
|
|
48
|
+
KEY=$(printf '%s' "$AGENT_ID" | tr -cd 'A-Za-z0-9_-')
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# The marker is checked at the very bottom now, not here. A skill the edited
|
|
52
|
+
# file's own stack demands is not something an already-cleared gate excuses,
|
|
53
|
+
# so the required-skill scan below runs first.
|
|
54
|
+
MARKER="/tmp/claude-skill-gate-$KEY"
|
|
37
55
|
|
|
38
56
|
# Write|Edit|MultiEdit is not the only way to change a file. A session that edits
|
|
39
57
|
# through \`python3 - <<'PY'\` in Bash walked past this gate entirely — twelve
|
|
@@ -115,9 +133,17 @@ if [ -n "$TARGETS" ]; then
|
|
|
115
133
|
fi
|
|
116
134
|
if [ -n "$TARGETS" ]; then
|
|
117
135
|
for SKILL_FILE in $(find "$PROJECT_DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null); do
|
|
118
|
-
# paths
|
|
136
|
+
# Read gate-paths from under metadata:, never a bare paths: key. A bare
|
|
137
|
+
# paths: made Claude Code drop the skill from its registry outright —
|
|
138
|
+
# every Skill(name) call returned "Unknown skill" and, with no SKIP for a
|
|
139
|
+
# mandatory skill, the session could not edit code at all. metadata is
|
|
140
|
+
# documented as free-form and ignored by Claude Code, and the docs say in
|
|
141
|
+
# so many words not to reuse paths as a key inside it.
|
|
142
|
+
#
|
|
143
|
+
# The value must be quoted in YAML either way: a scalar opening with * is
|
|
144
|
+
# an alias indicator.
|
|
119
145
|
UNQUOTE='s/^["'"'"']//; s/["'"'"']$//'
|
|
120
|
-
SPATHS=$(sed -n 's/^paths:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
146
|
+
SPATHS=$(sed -n 's/^[[:space:]]*gate-paths:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
121
147
|
STRACKS=$(sed -n 's/^tracks:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
122
148
|
SNAME=$(sed -n 's/^name:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
123
149
|
[ -z "$SPATHS" ] && continue
|
|
@@ -160,7 +186,7 @@ if [ -n "$TARGETS" ]; then
|
|
|
160
186
|
[ -z "$DEP" ] && continue
|
|
161
187
|
|
|
162
188
|
SAFE=$(printf '%s' "$SNAME" | tr -cd 'A-Za-z0-9_-')
|
|
163
|
-
[ -f "/tmp/claude-skill-loaded-$
|
|
189
|
+
[ -f "/tmp/claude-skill-loaded-$KEY-$SAFE" ] && continue
|
|
164
190
|
case " $MISSING " in *" $SNAME "*) ;; *) MISSING="$MISSING $SNAME" ;; esac
|
|
165
191
|
done
|
|
166
192
|
fi
|
|
@@ -180,7 +206,7 @@ fi
|
|
|
180
206
|
|
|
181
207
|
|
|
182
208
|
cat <<EOF
|
|
183
|
-
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: skill evaluation required before file edits in this session.\\n\\nStep 1 — evaluate every available skill as ACTIVATE or SKIP with a one-line reason.\\n\\nStep 2 — you MUST take EXACTLY ONE of these tool actions to clear the gate. Listing skills in text is NOT enough; retrying the edit without doing one of these will be denied again:\\n (a) If any skill is ACTIVATE → call Skill(name) for it. This auto-clears the gate.\\n (b) If ALL skills are SKIP → run this Bash tool call: touch /tmp/claude-skill-gate-$
|
|
209
|
+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: skill evaluation required before file edits in this session.\\n\\nStep 1 — evaluate every available skill as ACTIVATE or SKIP with a one-line reason.\\n\\nStep 2 — you MUST take EXACTLY ONE of these tool actions to clear the gate. Listing skills in text is NOT enough; retrying the edit without doing one of these will be denied again:\\n (a) If any skill is ACTIVATE → call Skill(name) for it. This auto-clears the gate.\\n (b) If ALL skills are SKIP → run this Bash tool call: touch /tmp/claude-skill-gate-$KEY\\n\\nStep 3 — only after Step 2 completes, retry the file edit."}}
|
|
184
210
|
EOF
|
|
185
211
|
exit 0
|
|
186
212
|
`;
|
|
@@ -199,14 +225,24 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
199
225
|
exit 0
|
|
200
226
|
fi
|
|
201
227
|
|
|
202
|
-
|
|
228
|
+
# Keyed exactly as the gate keys it. A subagent carries the parent's
|
|
229
|
+
# session_id, so both scripts fall back to agent_id when one is present — and
|
|
230
|
+
# they must agree, or a loaded skill writes a marker the gate never looks for
|
|
231
|
+
# and every edit is denied forever.
|
|
232
|
+
AGENT_ID=$(printf '%s' "$INPUT" | grep -o '"agent_id":"[^"]*"' | head -1 | sed 's/"agent_id":"//; s/"$//')
|
|
233
|
+
KEY="$SESSION_ID"
|
|
234
|
+
if [ -n "$AGENT_ID" ]; then
|
|
235
|
+
KEY=$(printf '%s' "$AGENT_ID" | tr -cd 'A-Za-z0-9_-')
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
touch "/tmp/claude-skill-gate-$KEY"
|
|
203
239
|
|
|
204
240
|
SKILL_NAME=$(printf '%s' "$INPUT" | grep -o '"skill":"[^"]*"' | head -1 | sed 's/"skill":"//; s/"$//')
|
|
205
241
|
if [ -n "$SKILL_NAME" ]; then
|
|
206
242
|
# Sanitize: only allow [A-Za-z0-9_-] in the marker filename.
|
|
207
243
|
SAFE_NAME=$(printf '%s' "$SKILL_NAME" | tr -cd 'A-Za-z0-9_-')
|
|
208
244
|
if [ -n "$SAFE_NAME" ]; then
|
|
209
|
-
touch "/tmp/claude-skill-loaded-$
|
|
245
|
+
touch "/tmp/claude-skill-loaded-$KEY-$SAFE_NAME"
|
|
210
246
|
fi
|
|
211
247
|
fi
|
|
212
248
|
|
|
@@ -230,8 +266,16 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
230
266
|
exit 0
|
|
231
267
|
fi
|
|
232
268
|
|
|
233
|
-
#
|
|
234
|
-
|
|
269
|
+
# Keyed exactly as the loading gate and the automark key it — see those two.
|
|
270
|
+
# A subagent carries the parent's session_id, so all three must agree.
|
|
271
|
+
AGENT_ID=$(printf '%s' "$INPUT" | grep -o '"agent_id":"[^"]*"' | head -1 | sed 's/"agent_id":"//; s/"$//')
|
|
272
|
+
KEY="$SESSION_ID"
|
|
273
|
+
if [ -n "$AGENT_ID" ]; then
|
|
274
|
+
KEY=$(printf '%s' "$AGENT_ID" | tr -cd 'A-Za-z0-9_-')
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# Defer to the loading gate until it's been satisfied for this key.
|
|
278
|
+
if [ ! -f "/tmp/claude-skill-gate-$KEY" ]; then
|
|
235
279
|
exit 0
|
|
236
280
|
fi
|
|
237
281
|
|
|
@@ -282,10 +326,10 @@ fi
|
|
|
282
326
|
|
|
283
327
|
# Collect every loaded-but-unacked skill so a single ack clears them all.
|
|
284
328
|
UNACKED=""
|
|
285
|
-
for marker in /tmp/claude-skill-loaded-$
|
|
329
|
+
for marker in /tmp/claude-skill-loaded-$KEY-*; do
|
|
286
330
|
[ ! -f "$marker" ] && continue
|
|
287
|
-
skill_name="\${marker##/tmp/claude-skill-loaded-$
|
|
288
|
-
if [ ! -f "/tmp/claude-skill-acked-$
|
|
331
|
+
skill_name="\${marker##/tmp/claude-skill-loaded-$KEY-}"
|
|
332
|
+
if [ ! -f "/tmp/claude-skill-acked-$KEY-$skill_name" ]; then
|
|
289
333
|
UNACKED="$UNACKED $skill_name"
|
|
290
334
|
fi
|
|
291
335
|
done
|
|
@@ -301,7 +345,7 @@ ACK_CMD="touch"
|
|
|
301
345
|
NAMES=""
|
|
302
346
|
RULES_BLOCKS=""
|
|
303
347
|
for skill in $UNACKED; do
|
|
304
|
-
ACK_CMD="$ACK_CMD /tmp/claude-skill-acked-$
|
|
348
|
+
ACK_CMD="$ACK_CMD /tmp/claude-skill-acked-$KEY-$skill"
|
|
305
349
|
NAMES="$NAMES, '$skill'"
|
|
306
350
|
SKILL_MD="$PROJECT_DIR/.claude/skills/$skill/SKILL.md"
|
|
307
351
|
RULES=""
|