@spardutti/claude-skills 2.26.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 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, and touch points before producing a short plan |
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 |
@@ -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 per-session marker file exists at /tmp/claude-skill-gate-<SESSION_ID>.
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
- # The session marker is checked at the very bottom now, not here. A skill the
34
- # edited file's own stack demands is not something an already-cleared gate
35
- # excuses, so the required-skill scan below runs first.
36
- MARKER="/tmp/claude-skill-gate-$SESSION_ID"
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
@@ -57,7 +75,9 @@ if [ "$TOOL" != "Bash" ]; then
57
75
  TARGET=$(printf '%s' "$INPUT" | grep -o '"file_path":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
58
76
  case "$TARGET" in
59
77
  # The settings file that configures the escape hatch cannot sit behind the
60
- # gate, or a denied ack has no way to be un-denied.
78
+ # gate, or a denied ack has no way to be un-denied. The bare relative form
79
+ # matters: a skill claiming **/* gated it, and nothing could re-open it.
80
+ .claude/settings.json|.claude/settings.local.json) exit 0 ;;
61
81
  */.claude/settings.json|*/.claude/settings.local.json) exit 0 ;;
62
82
  *.*) printf '%s' "$TARGET" | grep -qiE "\.($PROSE_EXT)$" && exit 0 ;;
63
83
  esac
@@ -90,8 +110,9 @@ if [ "$TOOL" = "Bash" ]; then
90
110
  *"<<"*|*python*" -c"*|*node*" -e"*|*perl*" -e"*|*ruby*" -e"*) WRITES=1 ;;
91
111
  esac
92
112
  [ -z "$WRITES" ] && exit 0
93
- # A Bash write names its files in the command itself.
94
- TARGETS=$(printf '%s' "$CMD" | grep -oiE "[A-Za-z0-9_./-]+\.($CODE_EXT)" 2>/dev/null)
113
+ # A Bash write names its files in the command itself. Dockerfile and
114
+ # .dockerignore carry no extension, so they are matched by name.
115
+ TARGETS=$(printf '%s' "$CMD" | grep -oiE "[A-Za-z0-9_./-]+\.($CODE_EXT)|[A-Za-z0-9_./-]*(Dockerfile[A-Za-z0-9_.-]*|\.dockerignore)" 2>/dev/null)
95
116
  fi
96
117
 
97
118
  # --- mandatory skills -------------------------------------------------------
@@ -106,18 +127,26 @@ fi
106
127
  # without both fields keep the old behaviour, so this tightens nothing that was
107
128
  # not deliberately declared.
108
129
  MISSING=""
130
+ MANIFESTS=""
109
131
  if [ -n "$TARGETS" ]; then
110
132
  MANIFESTS=$(find "$PROJECT_DIR" -maxdepth 4 \\( -name node_modules -o -name .git -o -name .venv -o -name dist \\) -prune -o \\( -name package.json -o -name pyproject.toml -o -name requirements.txt \\) -print 2>/dev/null)
111
133
  fi
112
- if [ -n "$TARGETS" ] && [ -n "$MANIFESTS" ]; then
134
+ if [ -n "$TARGETS" ]; then
113
135
  for SKILL_FILE in $(find "$PROJECT_DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null); do
114
- # paths: must be quoted in YAML — a scalar opening with * is an alias.
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.
115
145
  UNQUOTE='s/^["'"'"']//; s/["'"'"']$//'
116
- 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")
117
147
  STRACKS=$(sed -n 's/^tracks:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
118
148
  SNAME=$(sed -n 's/^name:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
119
149
  [ -z "$SPATHS" ] && continue
120
- [ -z "$STRACKS" ] && continue
121
150
  [ -z "$SNAME" ] && continue
122
151
 
123
152
  HIT=""
@@ -134,9 +163,14 @@ if [ -n "$TARGETS" ] && [ -n "$MANIFESTS" ]; then
134
163
 
135
164
  # A .tsx file is not proof of React — it could be Astro, Solid or Preact.
136
165
  # The packages the skill tracks are the proof, so the manifests decide.
166
+ #
167
+ # Some skills need no such proof: a .sql file is SQL and a Dockerfile is a
168
+ # Dockerfile, and no package declares either. A skill that names paths and
169
+ # no tracks is taken at its word.
137
170
  PY=""
138
171
  case "$STRACKS" in *pypi*) PY=1 ;; esac
139
172
  DEP=""
173
+ [ -z "$STRACKS" ] && DEP=1
140
174
  for P in $(printf '%s' "$STRACKS" | tr ',' ' '); do
141
175
  case "$P" in *@*) ;; *) continue ;; esac
142
176
  PKG=$(printf '%s' "$P" | sed 's/@[^@]*$//')
@@ -152,7 +186,7 @@ if [ -n "$TARGETS" ] && [ -n "$MANIFESTS" ]; then
152
186
  [ -z "$DEP" ] && continue
153
187
 
154
188
  SAFE=$(printf '%s' "$SNAME" | tr -cd 'A-Za-z0-9_-')
155
- [ -f "/tmp/claude-skill-loaded-$SESSION_ID-$SAFE" ] && continue
189
+ [ -f "/tmp/claude-skill-loaded-$KEY-$SAFE" ] && continue
156
190
  case " $MISSING " in *" $SNAME "*) ;; *) MISSING="$MISSING $SNAME" ;; esac
157
191
  done
158
192
  fi
@@ -172,7 +206,7 @@ fi
172
206
 
173
207
 
174
208
  cat <<EOF
175
- {"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-$SESSION_ID\\n\\nStep 3 — only after Step 2 completes, retry the file edit."}}
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."}}
176
210
  EOF
177
211
  exit 0
178
212
  `;
@@ -191,14 +225,24 @@ if [ -z "$SESSION_ID" ]; then
191
225
  exit 0
192
226
  fi
193
227
 
194
- touch "/tmp/claude-skill-gate-$SESSION_ID"
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"
195
239
 
196
240
  SKILL_NAME=$(printf '%s' "$INPUT" | grep -o '"skill":"[^"]*"' | head -1 | sed 's/"skill":"//; s/"$//')
197
241
  if [ -n "$SKILL_NAME" ]; then
198
242
  # Sanitize: only allow [A-Za-z0-9_-] in the marker filename.
199
243
  SAFE_NAME=$(printf '%s' "$SKILL_NAME" | tr -cd 'A-Za-z0-9_-')
200
244
  if [ -n "$SAFE_NAME" ]; then
201
- touch "/tmp/claude-skill-loaded-$SESSION_ID-$SAFE_NAME"
245
+ touch "/tmp/claude-skill-loaded-$KEY-$SAFE_NAME"
202
246
  fi
203
247
  fi
204
248
 
@@ -222,8 +266,16 @@ if [ -z "$SESSION_ID" ]; then
222
266
  exit 0
223
267
  fi
224
268
 
225
- # Defer to the loading gate until it's been satisfied this session.
226
- if [ ! -f "/tmp/claude-skill-gate-$SESSION_ID" ]; then
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
227
279
  exit 0
228
280
  fi
229
281
 
@@ -274,10 +326,10 @@ fi
274
326
 
275
327
  # Collect every loaded-but-unacked skill so a single ack clears them all.
276
328
  UNACKED=""
277
- for marker in /tmp/claude-skill-loaded-$SESSION_ID-*; do
329
+ for marker in /tmp/claude-skill-loaded-$KEY-*; do
278
330
  [ ! -f "$marker" ] && continue
279
- skill_name="\${marker##/tmp/claude-skill-loaded-$SESSION_ID-}"
280
- if [ ! -f "/tmp/claude-skill-acked-$SESSION_ID-$skill_name" ]; then
331
+ skill_name="\${marker##/tmp/claude-skill-loaded-$KEY-}"
332
+ if [ ! -f "/tmp/claude-skill-acked-$KEY-$skill_name" ]; then
281
333
  UNACKED="$UNACKED $skill_name"
282
334
  fi
283
335
  done
@@ -293,7 +345,7 @@ ACK_CMD="touch"
293
345
  NAMES=""
294
346
  RULES_BLOCKS=""
295
347
  for skill in $UNACKED; do
296
- ACK_CMD="$ACK_CMD /tmp/claude-skill-acked-$SESSION_ID-$skill"
348
+ ACK_CMD="$ACK_CMD /tmp/claude-skill-acked-$KEY-$skill"
297
349
  NAMES="$NAMES, '$skill'"
298
350
  SKILL_MD="$PROJECT_DIR/.claude/skills/$skill/SKILL.md"
299
351
  RULES=""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.26.0",
3
+ "version": "2.28.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {