@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 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
@@ -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: 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.
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-$SESSION_ID-$SAFE" ] && continue
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-$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."}}
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
- 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"
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-$SESSION_ID-$SAFE_NAME"
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
- # Defer to the loading gate until it's been satisfied this session.
234
- 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
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-$SESSION_ID-*; do
329
+ for marker in /tmp/claude-skill-loaded-$KEY-*; do
286
330
  [ ! -f "$marker" ] && continue
287
- skill_name="\${marker##/tmp/claude-skill-loaded-$SESSION_ID-}"
288
- 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
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-$SESSION_ID-$skill"
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=""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.27.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": {