@slamb2k/mad-skills 2.0.6 → 2.0.8

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.
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env bash
2
+ # session-guard.sh — Claude Code SessionStart hook
3
+ # Validates Git repo, CLAUDE.md existence/freshness, Task List ID config,
4
+ # and checks for staleness.
5
+ #
6
+ # All user-facing questions instruct Claude to use the AskUserQuestion tool.
7
+ #
8
+ # Install globally in ~/.claude/settings.json:
9
+ # "command": "\"$HOME\"/.claude/hooks/session-guard.sh"
10
+ #
11
+ # Or per-project in .claude/settings.json:
12
+ # "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-guard.sh"
13
+ #
14
+ # TIMING NOTE: SessionStart hook output is silently injected and only surfaces
15
+ # on the user's first prompt. Pair with session-guard-prompt.sh on
16
+ # UserPromptSubmit for immediate feedback (see companion hook).
17
+
18
+ # NOTE: We intentionally avoid `set -e` here. Many commands (grep, git, find,
19
+ # jq, stat) return non-zero for perfectly normal reasons (no matches, not a
20
+ # repo, missing files). Letting them fail gracefully is simpler and more
21
+ # reliable than wrapping every line in `|| true`.
22
+ set -uo pipefail
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Config
26
+ # ---------------------------------------------------------------------------
27
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
28
+ CLAUDE_MD="$PROJECT_DIR/CLAUDE.md"
29
+ NOW=$(date +%s)
30
+ STALENESS_THRESHOLD=3 # Accumulated score >= this triggers user prompt
31
+ PENDING_DIR="${TMPDIR:-/tmp}/claude-session-guard"
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # State
35
+ # ---------------------------------------------------------------------------
36
+ EARLY_CONTEXT_PARTS=()
37
+ STALENESS_SIGNALS=()
38
+ STALENESS_SCORE=0
39
+
40
+ add_staleness() {
41
+ local msg="$1"
42
+ local weight="${2:-1}"
43
+ STALENESS_SIGNALS+=("⚠ $msg")
44
+ STALENESS_SCORE=$((STALENESS_SCORE + weight))
45
+ }
46
+
47
+ # Portable stat wrapper: returns mtime as epoch seconds
48
+ file_mtime() {
49
+ stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo "$NOW"
50
+ }
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Dedup: prevent double-firing if configured at both global and project level
54
+ # ---------------------------------------------------------------------------
55
+ mkdir -p "$PENDING_DIR"
56
+ GUARD_KEY=$(echo "$PROJECT_DIR" | md5sum 2>/dev/null | cut -d' ' -f1 || echo "default")
57
+ LOCK_FILE="$PENDING_DIR/$GUARD_KEY.lock"
58
+
59
+ if [[ -f "$LOCK_FILE" ]]; then
60
+ LOCK_AGE=$(( NOW - $(file_mtime "$LOCK_FILE") ))
61
+ if (( LOCK_AGE < 5 )); then
62
+ jq -n '{}'
63
+ exit 0
64
+ fi
65
+ fi
66
+ touch "$LOCK_FILE"
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # 0) Git repository check
70
+ # ---------------------------------------------------------------------------
71
+ GIT_ROOT=""
72
+
73
+ if command -v git &>/dev/null; then
74
+ GIT_ROOT=$(git -C "$PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null) || true
75
+
76
+ if [[ -z "$GIT_ROOT" ]]; then
77
+ EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ⚠️ This directory is NOT tracked by Git.")
78
+ EARLY_CONTEXT_PARTS+=("")
79
+ EARLY_CONTEXT_PARTS+=("Use AskUserQuestion to prompt:")
80
+ EARLY_CONTEXT_PARTS+=(" Question: \"This directory isn't inside a Git repository. What would you like to do?\"")
81
+ EARLY_CONTEXT_PARTS+=(" Type: single_select")
82
+ EARLY_CONTEXT_PARTS+=(" Options:")
83
+ EARLY_CONTEXT_PARTS+=(" 1. \"Initialise Git\" — run \`git init\` and suggest creating .gitignore")
84
+ EARLY_CONTEXT_PARTS+=(" 2. \"Skip\" — continue without version control")
85
+ EARLY_CONTEXT_PARTS+=("")
86
+
87
+ elif [[ "$GIT_ROOT" != "$PROJECT_DIR" ]]; then
88
+ DEPTH=0
89
+ CHECK_DIR="$PROJECT_DIR"
90
+ while [[ "$CHECK_DIR" != "$GIT_ROOT" && "$CHECK_DIR" != "/" ]]; do
91
+ CHECK_DIR=$(dirname "$CHECK_DIR")
92
+ DEPTH=$((DEPTH + 1))
93
+ done
94
+
95
+ MONOREPO_SIGNALS=()
96
+
97
+ if [[ -f "$GIT_ROOT/package.json" ]] && command -v jq &>/dev/null; then
98
+ jq -e '.workspaces // empty' "$GIT_ROOT/package.json" &>/dev/null && \
99
+ MONOREPO_SIGNALS+=("package.json has 'workspaces' field")
100
+ fi
101
+ [[ -f "$GIT_ROOT/pnpm-workspace.yaml" ]] && MONOREPO_SIGNALS+=("pnpm-workspace.yaml exists")
102
+ [[ -f "$GIT_ROOT/lerna.json" ]] && MONOREPO_SIGNALS+=("lerna.json exists")
103
+ [[ -f "$GIT_ROOT/nx.json" ]] && MONOREPO_SIGNALS+=("nx.json exists")
104
+ [[ -f "$GIT_ROOT/turbo.json" ]] && MONOREPO_SIGNALS+=("turbo.json exists")
105
+ [[ -f "$GIT_ROOT/rush.json" ]] && MONOREPO_SIGNALS+=("rush.json exists")
106
+
107
+ for d in packages apps services libs modules projects; do
108
+ [[ -d "$GIT_ROOT/$d" ]] && MONOREPO_SIGNALS+=("'$d/' directory exists at git root")
109
+ done
110
+
111
+ PKG_COUNT=$(find "$GIT_ROOT" -maxdepth 3 -name "package.json" -not -path "*/node_modules/*" 2>/dev/null | wc -l | tr -d ' ')
112
+ (( PKG_COUNT > 2 )) && MONOREPO_SIGNALS+=("${PKG_COUNT} package.json files found")
113
+
114
+ CLAUDE_MD_COUNT=$(find "$GIT_ROOT" -maxdepth 3 -name "CLAUDE.md" 2>/dev/null | wc -l | tr -d ' ')
115
+ (( CLAUDE_MD_COUNT > 1 )) && MONOREPO_SIGNALS+=("${CLAUDE_MD_COUNT} CLAUDE.md files found (per-package setup)")
116
+
117
+ RELATIVE_PATH="${PROJECT_DIR#"$GIT_ROOT/"}"
118
+
119
+ if (( ${#MONOREPO_SIGNALS[@]} >= 2 )); then
120
+ EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ℹ️ Git root is ${DEPTH} level(s) above CWD.")
121
+ EARLY_CONTEXT_PARTS+=(" Git root: $GIT_ROOT")
122
+ EARLY_CONTEXT_PARTS+=(" Working dir: $PROJECT_DIR")
123
+ EARLY_CONTEXT_PARTS+=(" Monorepo signals: ${MONOREPO_SIGNALS[*]}")
124
+ EARLY_CONTEXT_PARTS+=("")
125
+ EARLY_CONTEXT_PARTS+=("Use AskUserQuestion (low priority, don't block):")
126
+ EARLY_CONTEXT_PARTS+=(" Question: \"Git root is at \`$GIT_ROOT\` (monorepo). Working in \`${RELATIVE_PATH}\`. Correct context?\"")
127
+ EARLY_CONTEXT_PARTS+=(" Type: single_select")
128
+ EARLY_CONTEXT_PARTS+=(" Options:")
129
+ EARLY_CONTEXT_PARTS+=(" 1. \"Yes, correct package\"")
130
+ EARLY_CONTEXT_PARTS+=(" 2. \"No, switch to repo root\"")
131
+ EARLY_CONTEXT_PARTS+=("")
132
+
133
+ else
134
+ GIT_ROOT_FILE_COUNT=$(find "$GIT_ROOT" -maxdepth 1 -not -name '.*' 2>/dev/null | wc -l | tr -d ' ')
135
+
136
+ EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ⚠️ Git root is ${DEPTH} level(s) above CWD — does NOT look like a monorepo.")
137
+ EARLY_CONTEXT_PARTS+=(" Git root: $GIT_ROOT (${GIT_ROOT_FILE_COUNT} files)")
138
+ EARLY_CONTEXT_PARTS+=(" Working dir: $PROJECT_DIR")
139
+ if (( ${#MONOREPO_SIGNALS[@]} > 0 )); then
140
+ EARLY_CONTEXT_PARTS+=(" Weak signals: ${MONOREPO_SIGNALS[*]}")
141
+ fi
142
+ EARLY_CONTEXT_PARTS+=("")
143
+ EARLY_CONTEXT_PARTS+=("Use AskUserQuestion to resolve:")
144
+ EARLY_CONTEXT_PARTS+=(" Question: \"Git root is at \`$GIT_ROOT\` (${DEPTH} levels up), which doesn't look like a monorepo. May have been created accidentally.\"")
145
+ EARLY_CONTEXT_PARTS+=(" Type: single_select")
146
+ EARLY_CONTEXT_PARTS+=(" Options:")
147
+ EARLY_CONTEXT_PARTS+=(" 1. \"It's correct\" — continue normally")
148
+ EARLY_CONTEXT_PARTS+=(" 2. \"Initialise here instead\" — run \`git init\` here (warn ancestor .git still exists)")
149
+ EARLY_CONTEXT_PARTS+=(" 3. \"Investigate\" — list git root contents and recent commits")
150
+ EARLY_CONTEXT_PARTS+=("")
151
+ fi
152
+ fi
153
+ fi
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # 1) CLAUDE.md existence check
157
+ # ---------------------------------------------------------------------------
158
+ if [[ ! -f "$CLAUDE_MD" ]]; then
159
+ EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ⚠️ No CLAUDE.md found in project root.")
160
+ EARLY_CONTEXT_PARTS+=("")
161
+ EARLY_CONTEXT_PARTS+=("Use AskUserQuestion to prompt:")
162
+ EARLY_CONTEXT_PARTS+=(" Question: \"No CLAUDE.md found. Want me to set up this project for Claude Code?\"")
163
+ EARLY_CONTEXT_PARTS+=(" Type: single_select")
164
+ EARLY_CONTEXT_PARTS+=(" Options:")
165
+ EARLY_CONTEXT_PARTS+=(" 1. \"Initialise\" — run \`/init\` to scaffold CLAUDE.md")
166
+ EARLY_CONTEXT_PARTS+=(" 2. \"Skip\" — continue without one")
167
+ EARLY_CONTEXT_PARTS+=("")
168
+
169
+ # Emit and write pending flag, then exit early
170
+ CONTEXT=$(printf '%s\n' "${EARLY_CONTEXT_PARTS[@]}")
171
+ jq -n --arg ctx "$CONTEXT" '{
172
+ hookSpecificOutput: {
173
+ hookEventName: "SessionStart",
174
+ additionalContext: $ctx
175
+ }
176
+ }'
177
+ PENDING_FILE="$PENDING_DIR/$GUARD_KEY.pending"
178
+ echo "$CONTEXT" > "$PENDING_FILE"
179
+ exit 0
180
+ fi
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # 2) Task List ID check
184
+ # ---------------------------------------------------------------------------
185
+ TASK_LIST_CONFIGURED=false
186
+ SETTINGS_FILE="$PROJECT_DIR/.claude/settings.json"
187
+
188
+ [[ -n "${CLAUDE_CODE_TASK_LIST_ID:-}" ]] && TASK_LIST_CONFIGURED=true
189
+
190
+ if [[ "$TASK_LIST_CONFIGURED" == false ]] && command -v jq &>/dev/null; then
191
+ for CFG_FILE in "$SETTINGS_FILE" "$HOME/.claude/settings.json"; do
192
+ if [[ -f "$CFG_FILE" ]]; then
193
+ TASK_ID=$(jq -r '.env.CLAUDE_CODE_TASK_LIST_ID // empty' "$CFG_FILE" 2>/dev/null) || true
194
+ if [[ -n "$TASK_ID" ]]; then
195
+ TASK_LIST_CONFIGURED=true
196
+ break
197
+ fi
198
+ fi
199
+ done
200
+ fi
201
+
202
+ if [[ "$TASK_LIST_CONFIGURED" == false && -n "$GIT_ROOT" ]]; then
203
+ COMMIT_COUNT=$(git -C "$PROJECT_DIR" rev-list --count HEAD 2>/dev/null) || COMMIT_COUNT=0
204
+ FILE_COUNT=$(git -C "$PROJECT_DIR" ls-files 2>/dev/null | wc -l | tr -d ' ') || FILE_COUNT=0
205
+
206
+ if (( COMMIT_COUNT > 20 || FILE_COUNT > 30 )); then
207
+ REPO_NAME=$(basename "$GIT_ROOT")
208
+ EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ℹ️ No persistent Task List ID configured.")
209
+ EARLY_CONTEXT_PARTS+=(" Project: ${COMMIT_COUNT} commits, ${FILE_COUNT} tracked files.")
210
+ EARLY_CONTEXT_PARTS+=("")
211
+ EARLY_CONTEXT_PARTS+=("Use AskUserQuestion (low priority, don't block):")
212
+ EARLY_CONTEXT_PARTS+=(" Question: \"No persistent Task List ID configured. For a project this size, tasks won't survive across sessions. Add one?\"")
213
+ EARLY_CONTEXT_PARTS+=(" Type: single_select")
214
+ EARLY_CONTEXT_PARTS+=(" Options:")
215
+ EARLY_CONTEXT_PARTS+=(" 1. \"Yes\" — add {\"env\": {\"CLAUDE_CODE_TASK_LIST_ID\": \"${REPO_NAME}\"}} to .claude/settings.json")
216
+ EARLY_CONTEXT_PARTS+=(" 2. \"Skip\" — continue without persistent tasks")
217
+ EARLY_CONTEXT_PARTS+=("")
218
+ fi
219
+ fi
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # 3) Staleness evaluation
223
+ # ---------------------------------------------------------------------------
224
+ CLAUDE_MD_MTIME=$(file_mtime "$CLAUDE_MD")
225
+ CLAUDE_MD_AGE_DAYS=$(( (NOW - CLAUDE_MD_MTIME) / 86400 ))
226
+
227
+ # --- Age-based --------------------------------------------------------------
228
+ if (( CLAUDE_MD_AGE_DAYS > 14 )); then
229
+ add_staleness "CLAUDE.md last modified ${CLAUDE_MD_AGE_DAYS} days ago" 2
230
+ elif (( CLAUDE_MD_AGE_DAYS > 7 )); then
231
+ add_staleness "CLAUDE.md last modified ${CLAUDE_MD_AGE_DAYS} days ago" 1
232
+ fi
233
+
234
+ # --- Directory structure drift ----------------------------------------------
235
+ if command -v tree &>/dev/null; then
236
+ CURRENT_TREE=$(tree -L 2 -d -I 'node_modules|.git|__pycache__|.venv|venv|dist|build|.next|.nuxt|coverage|.claude' --noreport "$PROJECT_DIR" 2>/dev/null) || true
237
+ if [[ -n "$CURRENT_TREE" ]]; then
238
+ TREE_DIRS=$(echo "$CURRENT_TREE" | tail -n +2 \
239
+ | sed 's/[│├└─┬┤┼┐┘┌┏┗┓┛]//g; s/[|`]//g; s/--*//g' \
240
+ | sed 's/^[[:space:]]*//' | { grep -v '^$' || true; } | sort -u)
241
+ MISSING_DIRS=()
242
+ while IFS= read -r dir; do
243
+ [[ -z "$dir" ]] && continue
244
+ grep -qi "$dir" "$CLAUDE_MD" 2>/dev/null || MISSING_DIRS+=("$dir")
245
+ done <<< "$TREE_DIRS"
246
+ if (( ${#MISSING_DIRS[@]} > 2 )); then
247
+ add_staleness "Directories not in CLAUDE.md: ${MISSING_DIRS[*]}" 2
248
+ elif (( ${#MISSING_DIRS[@]} > 0 )); then
249
+ add_staleness "Directories not in CLAUDE.md: ${MISSING_DIRS[*]}" 1
250
+ fi
251
+ fi
252
+ fi
253
+
254
+ # --- package.json drift -----------------------------------------------------
255
+ if [[ -f "$PROJECT_DIR/package.json" ]]; then
256
+ PKG_MTIME=$(file_mtime "$PROJECT_DIR/package.json")
257
+
258
+ if (( PKG_MTIME > CLAUDE_MD_MTIME )); then
259
+ PKG_DELTA=$(( (PKG_MTIME - CLAUDE_MD_MTIME) / 86400 ))
260
+ add_staleness "package.json modified ${PKG_DELTA} day(s) after CLAUDE.md" 1
261
+ fi
262
+
263
+ if command -v jq &>/dev/null; then
264
+ DEP_COUNT=$(jq '[(.dependencies // {} | length), (.devDependencies // {} | length)] | add' "$PROJECT_DIR/package.json" 2>/dev/null) || DEP_COUNT="0"
265
+ DOCUMENTED_COUNT=$(grep -oiP '\d+\s*(dependencies|deps)' "$CLAUDE_MD" 2>/dev/null | head -1 | grep -oP '\d+') || true
266
+ if [[ -n "$DOCUMENTED_COUNT" && -n "$DEP_COUNT" ]]; then
267
+ DRIFT=$(( DEP_COUNT - DOCUMENTED_COUNT ))
268
+ DRIFT_ABS=${DRIFT#-}
269
+ if (( DRIFT_ABS > 5 )); then
270
+ add_staleness "Dep count drift: CLAUDE.md ~${DOCUMENTED_COUNT}, actual ${DEP_COUNT} (Δ${DRIFT})" 2
271
+ elif (( DRIFT_ABS > 0 )); then
272
+ add_staleness "Dep count drift: CLAUDE.md ~${DOCUMENTED_COUNT}, actual ${DEP_COUNT}" 1
273
+ fi
274
+ fi
275
+
276
+ KEY_DEPS=$(jq -r '(.dependencies // {}) | keys[]' "$PROJECT_DIR/package.json" 2>/dev/null) || true
277
+ UNDOCUMENTED=()
278
+ while IFS= read -r dep; do
279
+ [[ -z "$dep" ]] && continue
280
+ grep -qi "$dep" "$CLAUDE_MD" 2>/dev/null || UNDOCUMENTED+=("$dep")
281
+ done <<< "$KEY_DEPS"
282
+ if (( ${#UNDOCUMENTED[@]} > 5 )); then
283
+ add_staleness "${#UNDOCUMENTED[@]} production deps not in CLAUDE.md (e.g. ${UNDOCUMENTED[*]:0:5})" 2
284
+ fi
285
+ fi
286
+ fi
287
+
288
+ # --- Python dependency drift ------------------------------------------------
289
+ for PYFILE in "$PROJECT_DIR/pyproject.toml" "$PROJECT_DIR/requirements.txt" "$PROJECT_DIR/setup.py"; do
290
+ if [[ -f "$PYFILE" ]]; then
291
+ PY_MTIME=$(file_mtime "$PYFILE")
292
+ (( PY_MTIME > CLAUDE_MD_MTIME )) && add_staleness "$(basename "$PYFILE") modified after CLAUDE.md" 1
293
+ fi
294
+ done
295
+
296
+ # --- Key config files -------------------------------------------------------
297
+ for CFG in "$PROJECT_DIR/tsconfig.json" \
298
+ "$PROJECT_DIR/.env.example" \
299
+ "$PROJECT_DIR/docker-compose.yml" \
300
+ "$PROJECT_DIR/Dockerfile" \
301
+ "$PROJECT_DIR/Makefile" \
302
+ "$PROJECT_DIR/Cargo.toml" \
303
+ "$PROJECT_DIR/go.mod"; do
304
+ if [[ -f "$CFG" ]]; then
305
+ CFG_MTIME=$(file_mtime "$CFG")
306
+ (( CFG_MTIME > CLAUDE_MD_MTIME )) && add_staleness "$(basename "$CFG") modified after CLAUDE.md" 1
307
+ fi
308
+ done
309
+
310
+ # --- Git-based checks -------------------------------------------------------
311
+ if command -v git &>/dev/null && [[ -n "$GIT_ROOT" ]]; then
312
+ CLAUDE_MD_DATE=$(date -d "@$CLAUDE_MD_MTIME" --iso-8601=seconds 2>/dev/null) \
313
+ || CLAUDE_MD_DATE=$(date -r "$CLAUDE_MD_MTIME" +%Y-%m-%dT%H:%M:%S 2>/dev/null) \
314
+ || CLAUDE_MD_DATE=""
315
+
316
+ if [[ -n "$CLAUDE_MD_DATE" ]]; then
317
+ COMMITS_SINCE=$(git -C "$PROJECT_DIR" rev-list --count --since="$CLAUDE_MD_DATE" HEAD 2>/dev/null) || COMMITS_SINCE="0"
318
+ if (( COMMITS_SINCE > 50 )); then
319
+ add_staleness "${COMMITS_SINCE} commits since CLAUDE.md updated" 2
320
+ elif (( COMMITS_SINCE > 20 )); then
321
+ add_staleness "${COMMITS_SINCE} commits since CLAUDE.md updated" 1
322
+ fi
323
+
324
+ CHANGED_FILES=$(git -C "$PROJECT_DIR" diff --name-only --diff-filter=AD HEAD~20..HEAD 2>/dev/null | head -20) || true
325
+ NEW_TOP_LEVEL=$(echo "$CHANGED_FILES" | { grep -v '/' || true; } | { grep -v '^\.' || true; } | sort -u)
326
+ if [[ -n "$NEW_TOP_LEVEL" ]]; then
327
+ NEW_COUNT=$(echo "$NEW_TOP_LEVEL" | wc -l | tr -d ' ')
328
+ (( NEW_COUNT > 3 )) && add_staleness "${NEW_COUNT} top-level files added/removed recently" 1
329
+ fi
330
+ fi
331
+ fi
332
+
333
+ # --- Lock file drift --------------------------------------------------------
334
+ for LOCK in "$PROJECT_DIR/package-lock.json" \
335
+ "$PROJECT_DIR/yarn.lock" \
336
+ "$PROJECT_DIR/pnpm-lock.yaml" \
337
+ "$PROJECT_DIR/Cargo.lock" \
338
+ "$PROJECT_DIR/poetry.lock"; do
339
+ if [[ -f "$LOCK" ]]; then
340
+ LOCK_MTIME=$(file_mtime "$LOCK")
341
+ LOCK_DELTA=$(( (LOCK_MTIME - CLAUDE_MD_MTIME) / 86400 ))
342
+ if (( LOCK_DELTA > 7 )); then
343
+ add_staleness "$(basename "$LOCK") is ${LOCK_DELTA} days newer than CLAUDE.md" 1
344
+ break
345
+ fi
346
+ fi
347
+ done
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # 4) Produce output
351
+ # ---------------------------------------------------------------------------
352
+ OUTPUT_PARTS=()
353
+
354
+ # --- Welcome banner ---------------------------------------------------------
355
+ SKILL_COUNT=$(find "$PROJECT_DIR/skills" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l | tr -d ' ') || SKILL_COUNT=0
356
+ if (( SKILL_COUNT > 0 )); then
357
+ OUTPUT_PARTS+=("[MAD SKILLS] Active — ${SKILL_COUNT} skills loaded")
358
+ fi
359
+
360
+ for part in "${EARLY_CONTEXT_PARTS[@]+"${EARLY_CONTEXT_PARTS[@]}"}"; do
361
+ OUTPUT_PARTS+=("$part")
362
+ done
363
+
364
+ OUTPUT_PARTS+=("[SESSION GUARD] ✅ CLAUDE.md found in: $PROJECT_DIR")
365
+
366
+ if (( STALENESS_SCORE >= STALENESS_THRESHOLD )); then
367
+ OUTPUT_PARTS+=("")
368
+ OUTPUT_PARTS+=("[SESSION GUARD] ⚠️ CLAUDE.md appears STALE (score: ${STALENESS_SCORE}/${STALENESS_THRESHOLD})")
369
+ OUTPUT_PARTS+=("")
370
+ OUTPUT_PARTS+=("Signals:")
371
+ for sig in "${STALENESS_SIGNALS[@]}"; do
372
+ OUTPUT_PARTS+=(" $sig")
373
+ done
374
+ OUTPUT_PARTS+=("")
375
+ OUTPUT_PARTS+=("Use AskUserQuestion to prompt:")
376
+ OUTPUT_PARTS+=(" Question: \"CLAUDE.md appears out of date (${#STALENESS_SIGNALS[@]} signals detected). What would you like to do?\"")
377
+ OUTPUT_PARTS+=(" Type: single_select")
378
+ OUTPUT_PARTS+=(" Options:")
379
+ OUTPUT_PARTS+=(" 1. \"Update it\" — review project structure, deps, recent changes and update CLAUDE.md (preserve user-written notes)")
380
+ OUTPUT_PARTS+=(" 2. \"Show signals\" — list what's drifted before deciding")
381
+ OUTPUT_PARTS+=(" 3. \"Skip\" — continue with current CLAUDE.md")
382
+ OUTPUT_PARTS+=("")
383
+ elif (( ${#STALENESS_SIGNALS[@]} > 0 )); then
384
+ OUTPUT_PARTS+=("")
385
+ OUTPUT_PARTS+=("[SESSION GUARD] ℹ️ Minor drift (score: ${STALENESS_SCORE}/${STALENESS_THRESHOLD}) — not flagging:")
386
+ for sig in "${STALENESS_SIGNALS[@]}"; do
387
+ OUTPUT_PARTS+=(" $sig")
388
+ done
389
+ fi
390
+
391
+ CONTEXT=$(printf '%s\n' "${OUTPUT_PARTS[@]}")
392
+
393
+ jq -n --arg ctx "$CONTEXT" '{
394
+ hookSpecificOutput: {
395
+ hookEventName: "SessionStart",
396
+ additionalContext: $ctx
397
+ }
398
+ }'
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # 5) Write pending flag for UserPromptSubmit companion hook
402
+ # ---------------------------------------------------------------------------
403
+ PENDING_FILE="$PENDING_DIR/$GUARD_KEY.pending"
404
+ echo "$CONTEXT" > "$PENDING_FILE"
405
+
406
+ exit 0
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@slamb2k/mad-skills",
3
- "version": "2.0.6",
4
- "description": "Claude Code skills collection with CI/CD and npx installer",
3
+ "version": "2.0.8",
4
+ "description": "Claude Code skills collection planning, development and governance tools",
5
5
  "type": "module",
6
- "bin": {
7
- "claude-skills": "./src/cli.js"
8
- },
9
6
  "repository": {
10
7
  "type": "git",
11
8
  "url": "https://github.com/slamb2k/mad-skills"
12
9
  },
13
10
  "files": [
14
- "src/",
15
- "skills/"
11
+ "skills/",
12
+ "agents/",
13
+ "hooks/",
14
+ ".claude-plugin/"
16
15
  ],
17
16
  "scripts": {
18
17
  "validate": "node scripts/validate-skills.js",