@slamb2k/mad-skills 2.0.7 → 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.
- package/.claude-plugin/marketplace.json +11 -5
- package/.claude-plugin/plugin.json +9 -7
- package/README.md +101 -25
- package/hooks/hooks.json +18 -0
- package/hooks/session-guard-prompt.sh +59 -0
- package/hooks/session-guard.sh +155 -151
- package/package.json +2 -7
- package/skills/brace/SKILL.md +240 -14
- package/skills/brace/assets/gitignore-template +0 -3
- package/skills/brace/references/phase-prompts.md +37 -0
- package/skills/brace/references/report-template.md +5 -1
- package/skills/build/SKILL.md +292 -13
- package/skills/distil/SKILL.md +258 -7
- package/skills/manifest.json +26 -16
- package/skills/prime/SKILL.md +76 -8
- package/skills/rig/SKILL.md +165 -7
- package/skills/ship/SKILL.md +190 -19
- package/skills/speccy/SKILL.md +165 -0
- package/skills/speccy/references/interview-guide.md +96 -0
- package/skills/speccy/tests/evals.json +34 -0
- package/skills/sync/SKILL.md +175 -17
- package/commands/brace.md +0 -9
- package/commands/build.md +0 -9
- package/commands/distil.md +0 -9
- package/commands/prime.md +0 -9
- package/commands/rig.md +0 -9
- package/commands/ship.md +0 -9
- package/commands/sync.md +0 -9
- package/skills/brace/instructions.md +0 -229
- package/skills/build/instructions.md +0 -293
- package/skills/distil/instructions.md +0 -255
- package/skills/prime/instructions.md +0 -73
- package/skills/rig/instructions.md +0 -162
- package/skills/ship/instructions.md +0 -192
- package/skills/sync/instructions.md +0 -178
- package/src/cli.js +0 -482
package/hooks/session-guard.sh
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# session-guard.sh — Claude Code SessionStart hook
|
|
3
|
-
# Validates Git repo, CLAUDE.md existence/freshness,
|
|
3
|
+
# Validates Git repo, CLAUDE.md existence/freshness, Task List ID config,
|
|
4
|
+
# and checks for staleness.
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# }
|
|
17
|
-
# ]
|
|
18
|
-
# }
|
|
19
|
-
# }
|
|
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).
|
|
20
17
|
|
|
21
18
|
# NOTE: We intentionally avoid `set -e` here. Many commands (grep, git, find,
|
|
22
19
|
# jq, stat) return non-zero for perfectly normal reasons (no matches, not a
|
|
@@ -31,11 +28,12 @@ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
|
31
28
|
CLAUDE_MD="$PROJECT_DIR/CLAUDE.md"
|
|
32
29
|
NOW=$(date +%s)
|
|
33
30
|
STALENESS_THRESHOLD=3 # Accumulated score >= this triggers user prompt
|
|
31
|
+
PENDING_DIR="${TMPDIR:-/tmp}/claude-session-guard"
|
|
34
32
|
|
|
35
33
|
# ---------------------------------------------------------------------------
|
|
36
34
|
# State
|
|
37
35
|
# ---------------------------------------------------------------------------
|
|
38
|
-
EARLY_CONTEXT_PARTS=()
|
|
36
|
+
EARLY_CONTEXT_PARTS=()
|
|
39
37
|
STALENESS_SIGNALS=()
|
|
40
38
|
STALENESS_SCORE=0
|
|
41
39
|
|
|
@@ -51,6 +49,22 @@ file_mtime() {
|
|
|
51
49
|
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo "$NOW"
|
|
52
50
|
}
|
|
53
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
|
+
|
|
54
68
|
# ---------------------------------------------------------------------------
|
|
55
69
|
# 0) Git repository check
|
|
56
70
|
# ---------------------------------------------------------------------------
|
|
@@ -60,24 +74,17 @@ if command -v git &>/dev/null; then
|
|
|
60
74
|
GIT_ROOT=$(git -C "$PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null) || true
|
|
61
75
|
|
|
62
76
|
if [[ -z "$GIT_ROOT" ]]; then
|
|
63
|
-
# ---- Not inside any git repository ----------------------------------
|
|
64
77
|
EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ⚠️ This directory is NOT tracked by Git.")
|
|
65
78
|
EARLY_CONTEXT_PARTS+=("")
|
|
66
|
-
EARLY_CONTEXT_PARTS+=("
|
|
67
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
68
|
-
EARLY_CONTEXT_PARTS+=("
|
|
69
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
70
|
-
EARLY_CONTEXT_PARTS+=("1.
|
|
71
|
-
EARLY_CONTEXT_PARTS+=("2.
|
|
72
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
73
|
-
EARLY_CONTEXT_PARTS+=("What would you prefer?\"")
|
|
74
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
75
|
-
EARLY_CONTEXT_PARTS+=("If the user chooses to initialise, run \`git init\` in the project directory, then suggest creating a .gitignore if one doesn't exist.")
|
|
76
|
-
EARLY_CONTEXT_PARTS+=("If the user chooses to skip, continue normally.")
|
|
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")
|
|
77
85
|
EARLY_CONTEXT_PARTS+=("")
|
|
78
86
|
|
|
79
87
|
elif [[ "$GIT_ROOT" != "$PROJECT_DIR" ]]; then
|
|
80
|
-
# ---- Git root is an ancestor folder ---------------------------------
|
|
81
88
|
DEPTH=0
|
|
82
89
|
CHECK_DIR="$PROJECT_DIR"
|
|
83
90
|
while [[ "$CHECK_DIR" != "$GIT_ROOT" && "$CHECK_DIR" != "/" ]]; do
|
|
@@ -85,14 +92,11 @@ if command -v git &>/dev/null; then
|
|
|
85
92
|
DEPTH=$((DEPTH + 1))
|
|
86
93
|
done
|
|
87
94
|
|
|
88
|
-
# Gather monorepo signals at the git root
|
|
89
95
|
MONOREPO_SIGNALS=()
|
|
90
96
|
|
|
91
|
-
# Workspace configs
|
|
92
97
|
if [[ -f "$GIT_ROOT/package.json" ]] && command -v jq &>/dev/null; then
|
|
93
|
-
|
|
98
|
+
jq -e '.workspaces // empty' "$GIT_ROOT/package.json" &>/dev/null && \
|
|
94
99
|
MONOREPO_SIGNALS+=("package.json has 'workspaces' field")
|
|
95
|
-
fi
|
|
96
100
|
fi
|
|
97
101
|
[[ -f "$GIT_ROOT/pnpm-workspace.yaml" ]] && MONOREPO_SIGNALS+=("pnpm-workspace.yaml exists")
|
|
98
102
|
[[ -f "$GIT_ROOT/lerna.json" ]] && MONOREPO_SIGNALS+=("lerna.json exists")
|
|
@@ -100,98 +104,69 @@ if command -v git &>/dev/null; then
|
|
|
100
104
|
[[ -f "$GIT_ROOT/turbo.json" ]] && MONOREPO_SIGNALS+=("turbo.json exists")
|
|
101
105
|
[[ -f "$GIT_ROOT/rush.json" ]] && MONOREPO_SIGNALS+=("rush.json exists")
|
|
102
106
|
|
|
103
|
-
# Common monorepo directory patterns
|
|
104
107
|
for d in packages apps services libs modules projects; do
|
|
105
108
|
[[ -d "$GIT_ROOT/$d" ]] && MONOREPO_SIGNALS+=("'$d/' directory exists at git root")
|
|
106
109
|
done
|
|
107
110
|
|
|
108
|
-
# Multiple package.json files (strong monorepo indicator)
|
|
109
111
|
PKG_COUNT=$(find "$GIT_ROOT" -maxdepth 3 -name "package.json" -not -path "*/node_modules/*" 2>/dev/null | wc -l | tr -d ' ')
|
|
110
|
-
(( PKG_COUNT > 2 )) && MONOREPO_SIGNALS+=("${PKG_COUNT} package.json files found
|
|
112
|
+
(( PKG_COUNT > 2 )) && MONOREPO_SIGNALS+=("${PKG_COUNT} package.json files found")
|
|
111
113
|
|
|
112
|
-
# Multiple CLAUDE.md files (intentional per-package setup)
|
|
113
114
|
CLAUDE_MD_COUNT=$(find "$GIT_ROOT" -maxdepth 3 -name "CLAUDE.md" 2>/dev/null | wc -l | tr -d ' ')
|
|
114
|
-
(( CLAUDE_MD_COUNT > 1 )) && MONOREPO_SIGNALS+=("${CLAUDE_MD_COUNT} CLAUDE.md files found (
|
|
115
|
+
(( CLAUDE_MD_COUNT > 1 )) && MONOREPO_SIGNALS+=("${CLAUDE_MD_COUNT} CLAUDE.md files found (per-package setup)")
|
|
115
116
|
|
|
116
117
|
RELATIVE_PATH="${PROJECT_DIR#"$GIT_ROOT/"}"
|
|
117
118
|
|
|
118
119
|
if (( ${#MONOREPO_SIGNALS[@]} >= 2 )); then
|
|
119
|
-
|
|
120
|
-
EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ℹ️ Git root is ${DEPTH} level(s) above the current directory.")
|
|
120
|
+
EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ℹ️ Git root is ${DEPTH} level(s) above CWD.")
|
|
121
121
|
EARLY_CONTEXT_PARTS+=(" Git root: $GIT_ROOT")
|
|
122
122
|
EARLY_CONTEXT_PARTS+=(" Working dir: $PROJECT_DIR")
|
|
123
|
+
EARLY_CONTEXT_PARTS+=(" Monorepo signals: ${MONOREPO_SIGNALS[*]}")
|
|
123
124
|
EARLY_CONTEXT_PARTS+=("")
|
|
124
|
-
EARLY_CONTEXT_PARTS+=("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
129
|
-
EARLY_CONTEXT_PARTS+=("
|
|
130
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
131
|
-
EARLY_CONTEXT_PARTS+=("\"I notice the Git repository root is at \`$GIT_ROOT\`, which looks like a monorepo. I'm working in the \`${RELATIVE_PATH}\` package. Just confirming — is this the right context, or did you mean to open Claude Code at the repo root?\"")
|
|
132
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
133
|
-
EARLY_CONTEXT_PARTS+=("This is a low-priority confirmation — don't block on it. Continue with the session regardless.")
|
|
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\"")
|
|
134
131
|
EARLY_CONTEXT_PARTS+=("")
|
|
135
132
|
|
|
136
133
|
else
|
|
137
|
-
# ---- Ancestor may be incorrectly initialised ----------------------
|
|
138
134
|
GIT_ROOT_FILE_COUNT=$(find "$GIT_ROOT" -maxdepth 1 -not -name '.*' 2>/dev/null | wc -l | tr -d ' ')
|
|
139
135
|
|
|
140
|
-
EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ⚠️ Git root is ${DEPTH} level(s) above
|
|
141
|
-
EARLY_CONTEXT_PARTS+=(" Git root: $GIT_ROOT")
|
|
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)")
|
|
142
138
|
EARLY_CONTEXT_PARTS+=(" Working dir: $PROJECT_DIR")
|
|
143
|
-
EARLY_CONTEXT_PARTS+=(" Files at git root: ~${GIT_ROOT_FILE_COUNT}")
|
|
144
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
145
|
-
|
|
146
139
|
if (( ${#MONOREPO_SIGNALS[@]} > 0 )); then
|
|
147
|
-
EARLY_CONTEXT_PARTS+=("Weak signals
|
|
148
|
-
for sig in "${MONOREPO_SIGNALS[@]}"; do
|
|
149
|
-
EARLY_CONTEXT_PARTS+=(" • $sig")
|
|
150
|
-
done
|
|
151
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
140
|
+
EARLY_CONTEXT_PARTS+=(" Weak signals: ${MONOREPO_SIGNALS[*]}")
|
|
152
141
|
fi
|
|
153
|
-
|
|
154
|
-
EARLY_CONTEXT_PARTS+=("This may indicate that an ancestor directory was accidentally initialised with \`git init\`.")
|
|
155
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
156
|
-
EARLY_CONTEXT_PARTS+=("Please ask the user:")
|
|
157
142
|
EARLY_CONTEXT_PARTS+=("")
|
|
158
|
-
EARLY_CONTEXT_PARTS+=("
|
|
159
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
160
|
-
EARLY_CONTEXT_PARTS+=("
|
|
161
|
-
EARLY_CONTEXT_PARTS+=("
|
|
162
|
-
EARLY_CONTEXT_PARTS+=("
|
|
163
|
-
EARLY_CONTEXT_PARTS+=("
|
|
164
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
165
|
-
EARLY_CONTEXT_PARTS+=("What would you prefer?\"")
|
|
166
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
167
|
-
EARLY_CONTEXT_PARTS+=("If 'correct': continue normally.")
|
|
168
|
-
EARLY_CONTEXT_PARTS+=("If 'initialise here': run \`git init\` in the project directory. Warn the user that the ancestor .git will still exist and they may want to remove it later.")
|
|
169
|
-
EARLY_CONTEXT_PARTS+=("If 'investigate': list the git root contents and recent commits to help the user understand what's tracked.")
|
|
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")
|
|
170
150
|
EARLY_CONTEXT_PARTS+=("")
|
|
171
151
|
fi
|
|
172
152
|
fi
|
|
173
|
-
# else: GIT_ROOT == PROJECT_DIR — all good, no action needed
|
|
174
153
|
fi
|
|
175
154
|
|
|
176
155
|
# ---------------------------------------------------------------------------
|
|
177
156
|
# 1) CLAUDE.md existence check
|
|
178
157
|
# ---------------------------------------------------------------------------
|
|
179
158
|
if [[ ! -f "$CLAUDE_MD" ]]; then
|
|
180
|
-
EARLY_CONTEXT_PARTS+=("[SESSION GUARD] No CLAUDE.md
|
|
181
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
182
|
-
EARLY_CONTEXT_PARTS+=("This directory has not been initialised for Claude Code. Please ask the user:")
|
|
183
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
184
|
-
EARLY_CONTEXT_PARTS+=("\"I notice this directory doesn't have a CLAUDE.md file, so it isn't set up as a Claude Code project yet. Would you like me to:")
|
|
185
|
-
EARLY_CONTEXT_PARTS+=("")
|
|
186
|
-
EARLY_CONTEXT_PARTS+=("1. **Initialise it** — I'll run \`/init\` to scaffold a CLAUDE.md with project context")
|
|
187
|
-
EARLY_CONTEXT_PARTS+=("2. **Skip for now** — Continue without one (you may lose project-specific context)")
|
|
159
|
+
EARLY_CONTEXT_PARTS+=("[SESSION GUARD] ⚠️ No CLAUDE.md found in project root.")
|
|
188
160
|
EARLY_CONTEXT_PARTS+=("")
|
|
189
|
-
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")
|
|
190
167
|
EARLY_CONTEXT_PARTS+=("")
|
|
191
|
-
EARLY_CONTEXT_PARTS+=("If the user chooses to initialise, run the /init slash command.")
|
|
192
|
-
EARLY_CONTEXT_PARTS+=("If the user chooses to skip, continue normally.")
|
|
193
168
|
|
|
194
|
-
# Emit
|
|
169
|
+
# Emit and write pending flag, then exit early
|
|
195
170
|
CONTEXT=$(printf '%s\n' "${EARLY_CONTEXT_PARTS[@]}")
|
|
196
171
|
jq -n --arg ctx "$CONTEXT" '{
|
|
197
172
|
hookSpecificOutput: {
|
|
@@ -199,98 +174,126 @@ if [[ ! -f "$CLAUDE_MD" ]]; then
|
|
|
199
174
|
additionalContext: $ctx
|
|
200
175
|
}
|
|
201
176
|
}'
|
|
177
|
+
PENDING_FILE="$PENDING_DIR/$GUARD_KEY.pending"
|
|
178
|
+
echo "$CONTEXT" > "$PENDING_FILE"
|
|
202
179
|
exit 0
|
|
203
180
|
fi
|
|
204
181
|
|
|
205
182
|
# ---------------------------------------------------------------------------
|
|
206
|
-
# 2)
|
|
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
|
|
207
223
|
# ---------------------------------------------------------------------------
|
|
208
224
|
CLAUDE_MD_MTIME=$(file_mtime "$CLAUDE_MD")
|
|
209
225
|
CLAUDE_MD_AGE_DAYS=$(( (NOW - CLAUDE_MD_MTIME) / 86400 ))
|
|
210
|
-
CLAUDE_MD_CONTENT=$(cat "$CLAUDE_MD")
|
|
211
226
|
|
|
212
|
-
# ---
|
|
227
|
+
# --- Age-based --------------------------------------------------------------
|
|
213
228
|
if (( CLAUDE_MD_AGE_DAYS > 14 )); then
|
|
214
|
-
add_staleness "CLAUDE.md
|
|
229
|
+
add_staleness "CLAUDE.md last modified ${CLAUDE_MD_AGE_DAYS} days ago" 2
|
|
215
230
|
elif (( CLAUDE_MD_AGE_DAYS > 7 )); then
|
|
216
|
-
add_staleness "CLAUDE.md
|
|
231
|
+
add_staleness "CLAUDE.md last modified ${CLAUDE_MD_AGE_DAYS} days ago" 1
|
|
217
232
|
fi
|
|
218
233
|
|
|
219
|
-
# ---
|
|
234
|
+
# --- Directory structure drift ----------------------------------------------
|
|
220
235
|
if command -v tree &>/dev/null; then
|
|
221
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
|
|
222
237
|
if [[ -n "$CURRENT_TREE" ]]; then
|
|
223
|
-
# Strip box-drawing characters, pipes, dashes, whitespace to get clean dir names
|
|
224
238
|
TREE_DIRS=$(echo "$CURRENT_TREE" | tail -n +2 \
|
|
225
239
|
| sed 's/[│├└─┬┤┼┐┘┌┏┗┓┛]//g; s/[|`]//g; s/--*//g' \
|
|
226
240
|
| sed 's/^[[:space:]]*//' | { grep -v '^$' || true; } | sort -u)
|
|
227
241
|
MISSING_DIRS=()
|
|
228
242
|
while IFS= read -r dir; do
|
|
229
243
|
[[ -z "$dir" ]] && continue
|
|
230
|
-
|
|
231
|
-
MISSING_DIRS+=("$dir")
|
|
232
|
-
fi
|
|
244
|
+
grep -qi "$dir" "$CLAUDE_MD" 2>/dev/null || MISSING_DIRS+=("$dir")
|
|
233
245
|
done <<< "$TREE_DIRS"
|
|
234
246
|
if (( ${#MISSING_DIRS[@]} > 2 )); then
|
|
235
|
-
add_staleness "Directories not
|
|
247
|
+
add_staleness "Directories not in CLAUDE.md: ${MISSING_DIRS[*]}" 2
|
|
236
248
|
elif (( ${#MISSING_DIRS[@]} > 0 )); then
|
|
237
|
-
add_staleness "Directories not
|
|
249
|
+
add_staleness "Directories not in CLAUDE.md: ${MISSING_DIRS[*]}" 1
|
|
238
250
|
fi
|
|
239
251
|
fi
|
|
240
252
|
fi
|
|
241
253
|
|
|
242
|
-
# ---
|
|
254
|
+
# --- package.json drift -----------------------------------------------------
|
|
243
255
|
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
|
244
256
|
PKG_MTIME=$(file_mtime "$PROJECT_DIR/package.json")
|
|
245
257
|
|
|
246
|
-
# Check if package.json is newer than CLAUDE.md
|
|
247
258
|
if (( PKG_MTIME > CLAUDE_MD_MTIME )); then
|
|
248
259
|
PKG_DELTA=$(( (PKG_MTIME - CLAUDE_MD_MTIME) / 86400 ))
|
|
249
|
-
add_staleness "package.json
|
|
260
|
+
add_staleness "package.json modified ${PKG_DELTA} day(s) after CLAUDE.md" 1
|
|
250
261
|
fi
|
|
251
262
|
|
|
252
|
-
# Count dependencies vs what's documented
|
|
253
263
|
if command -v jq &>/dev/null; then
|
|
254
264
|
DEP_COUNT=$(jq '[(.dependencies // {} | length), (.devDependencies // {} | length)] | add' "$PROJECT_DIR/package.json" 2>/dev/null) || DEP_COUNT="0"
|
|
255
|
-
# Look for numbers near "dependenc" in CLAUDE.md (e.g. "23 dependencies")
|
|
256
265
|
DOCUMENTED_COUNT=$(grep -oiP '\d+\s*(dependencies|deps)' "$CLAUDE_MD" 2>/dev/null | head -1 | grep -oP '\d+') || true
|
|
257
266
|
if [[ -n "$DOCUMENTED_COUNT" && -n "$DEP_COUNT" ]]; then
|
|
258
267
|
DRIFT=$(( DEP_COUNT - DOCUMENTED_COUNT ))
|
|
259
268
|
DRIFT_ABS=${DRIFT#-}
|
|
260
269
|
if (( DRIFT_ABS > 5 )); then
|
|
261
|
-
add_staleness "
|
|
270
|
+
add_staleness "Dep count drift: CLAUDE.md ~${DOCUMENTED_COUNT}, actual ${DEP_COUNT} (Δ${DRIFT})" 2
|
|
262
271
|
elif (( DRIFT_ABS > 0 )); then
|
|
263
|
-
add_staleness "
|
|
272
|
+
add_staleness "Dep count drift: CLAUDE.md ~${DOCUMENTED_COUNT}, actual ${DEP_COUNT}" 1
|
|
264
273
|
fi
|
|
265
274
|
fi
|
|
266
275
|
|
|
267
|
-
# Check for key dependencies not documented
|
|
268
276
|
KEY_DEPS=$(jq -r '(.dependencies // {}) | keys[]' "$PROJECT_DIR/package.json" 2>/dev/null) || true
|
|
269
|
-
|
|
277
|
+
UNDOCUMENTED=()
|
|
270
278
|
while IFS= read -r dep; do
|
|
271
279
|
[[ -z "$dep" ]] && continue
|
|
272
|
-
|
|
273
|
-
UNDOCUMENTED_KEY_DEPS+=("$dep")
|
|
274
|
-
fi
|
|
280
|
+
grep -qi "$dep" "$CLAUDE_MD" 2>/dev/null || UNDOCUMENTED+=("$dep")
|
|
275
281
|
done <<< "$KEY_DEPS"
|
|
276
|
-
if (( ${#
|
|
277
|
-
|
|
278
|
-
add_staleness "Multiple production deps not in CLAUDE.md (${#UNDOCUMENTED_KEY_DEPS[@]} total, e.g. ${SAMPLE})" 2
|
|
282
|
+
if (( ${#UNDOCUMENTED[@]} > 5 )); then
|
|
283
|
+
add_staleness "${#UNDOCUMENTED[@]} production deps not in CLAUDE.md (e.g. ${UNDOCUMENTED[*]:0:5})" 2
|
|
279
284
|
fi
|
|
280
285
|
fi
|
|
281
286
|
fi
|
|
282
287
|
|
|
283
|
-
# ---
|
|
288
|
+
# --- Python dependency drift ------------------------------------------------
|
|
284
289
|
for PYFILE in "$PROJECT_DIR/pyproject.toml" "$PROJECT_DIR/requirements.txt" "$PROJECT_DIR/setup.py"; do
|
|
285
290
|
if [[ -f "$PYFILE" ]]; then
|
|
286
291
|
PY_MTIME=$(file_mtime "$PYFILE")
|
|
287
|
-
|
|
288
|
-
add_staleness "$(basename "$PYFILE") was modified after CLAUDE.md" 1
|
|
289
|
-
fi
|
|
292
|
+
(( PY_MTIME > CLAUDE_MD_MTIME )) && add_staleness "$(basename "$PYFILE") modified after CLAUDE.md" 1
|
|
290
293
|
fi
|
|
291
294
|
done
|
|
292
295
|
|
|
293
|
-
# ---
|
|
296
|
+
# --- Key config files -------------------------------------------------------
|
|
294
297
|
for CFG in "$PROJECT_DIR/tsconfig.json" \
|
|
295
298
|
"$PROJECT_DIR/.env.example" \
|
|
296
299
|
"$PROJECT_DIR/docker-compose.yml" \
|
|
@@ -300,15 +303,12 @@ for CFG in "$PROJECT_DIR/tsconfig.json" \
|
|
|
300
303
|
"$PROJECT_DIR/go.mod"; do
|
|
301
304
|
if [[ -f "$CFG" ]]; then
|
|
302
305
|
CFG_MTIME=$(file_mtime "$CFG")
|
|
303
|
-
|
|
304
|
-
add_staleness "$(basename "$CFG") modified after CLAUDE.md" 1
|
|
305
|
-
fi
|
|
306
|
+
(( CFG_MTIME > CLAUDE_MD_MTIME )) && add_staleness "$(basename "$CFG") modified after CLAUDE.md" 1
|
|
306
307
|
fi
|
|
307
308
|
done
|
|
308
309
|
|
|
309
|
-
# ---
|
|
310
|
+
# --- Git-based checks -------------------------------------------------------
|
|
310
311
|
if command -v git &>/dev/null && [[ -n "$GIT_ROOT" ]]; then
|
|
311
|
-
# Convert CLAUDE.md mtime to ISO date for git
|
|
312
312
|
CLAUDE_MD_DATE=$(date -d "@$CLAUDE_MD_MTIME" --iso-8601=seconds 2>/dev/null) \
|
|
313
313
|
|| CLAUDE_MD_DATE=$(date -r "$CLAUDE_MD_MTIME" +%Y-%m-%dT%H:%M:%S 2>/dev/null) \
|
|
314
314
|
|| CLAUDE_MD_DATE=""
|
|
@@ -316,24 +316,21 @@ if command -v git &>/dev/null && [[ -n "$GIT_ROOT" ]]; then
|
|
|
316
316
|
if [[ -n "$CLAUDE_MD_DATE" ]]; then
|
|
317
317
|
COMMITS_SINCE=$(git -C "$PROJECT_DIR" rev-list --count --since="$CLAUDE_MD_DATE" HEAD 2>/dev/null) || COMMITS_SINCE="0"
|
|
318
318
|
if (( COMMITS_SINCE > 50 )); then
|
|
319
|
-
add_staleness "${COMMITS_SINCE} commits since CLAUDE.md
|
|
319
|
+
add_staleness "${COMMITS_SINCE} commits since CLAUDE.md updated" 2
|
|
320
320
|
elif (( COMMITS_SINCE > 20 )); then
|
|
321
|
-
add_staleness "${COMMITS_SINCE} commits since CLAUDE.md
|
|
321
|
+
add_staleness "${COMMITS_SINCE} commits since CLAUDE.md updated" 1
|
|
322
322
|
fi
|
|
323
323
|
|
|
324
|
-
# Check for structural file additions/deletions
|
|
325
324
|
CHANGED_FILES=$(git -C "$PROJECT_DIR" diff --name-only --diff-filter=AD HEAD~20..HEAD 2>/dev/null | head -20) || true
|
|
326
325
|
NEW_TOP_LEVEL=$(echo "$CHANGED_FILES" | { grep -v '/' || true; } | { grep -v '^\.' || true; } | sort -u)
|
|
327
326
|
if [[ -n "$NEW_TOP_LEVEL" ]]; then
|
|
328
327
|
NEW_COUNT=$(echo "$NEW_TOP_LEVEL" | wc -l | tr -d ' ')
|
|
329
|
-
|
|
330
|
-
add_staleness "Multiple top-level files added/removed since last update (${NEW_COUNT} files)" 1
|
|
331
|
-
fi
|
|
328
|
+
(( NEW_COUNT > 3 )) && add_staleness "${NEW_COUNT} top-level files added/removed recently" 1
|
|
332
329
|
fi
|
|
333
330
|
fi
|
|
334
331
|
fi
|
|
335
332
|
|
|
336
|
-
# ---
|
|
333
|
+
# --- Lock file drift --------------------------------------------------------
|
|
337
334
|
for LOCK in "$PROJECT_DIR/package-lock.json" \
|
|
338
335
|
"$PROJECT_DIR/yarn.lock" \
|
|
339
336
|
"$PROJECT_DIR/pnpm-lock.yaml" \
|
|
@@ -344,7 +341,7 @@ for LOCK in "$PROJECT_DIR/package-lock.json" \
|
|
|
344
341
|
LOCK_DELTA=$(( (LOCK_MTIME - CLAUDE_MD_MTIME) / 86400 ))
|
|
345
342
|
if (( LOCK_DELTA > 7 )); then
|
|
346
343
|
add_staleness "$(basename "$LOCK") is ${LOCK_DELTA} days newer than CLAUDE.md" 1
|
|
347
|
-
break
|
|
344
|
+
break
|
|
348
345
|
fi
|
|
349
346
|
fi
|
|
350
347
|
done
|
|
@@ -354,42 +351,43 @@ done
|
|
|
354
351
|
# ---------------------------------------------------------------------------
|
|
355
352
|
OUTPUT_PARTS=()
|
|
356
353
|
|
|
357
|
-
#
|
|
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
|
+
|
|
358
360
|
for part in "${EARLY_CONTEXT_PARTS[@]+"${EARLY_CONTEXT_PARTS[@]}"}"; do
|
|
359
361
|
OUTPUT_PARTS+=("$part")
|
|
360
362
|
done
|
|
361
363
|
|
|
362
|
-
# Confirm CLAUDE.md found
|
|
363
364
|
OUTPUT_PARTS+=("[SESSION GUARD] ✅ CLAUDE.md found in: $PROJECT_DIR")
|
|
364
365
|
|
|
365
366
|
if (( STALENESS_SCORE >= STALENESS_THRESHOLD )); then
|
|
366
367
|
OUTPUT_PARTS+=("")
|
|
367
|
-
OUTPUT_PARTS+=("[SESSION GUARD] ⚠️ CLAUDE.md appears
|
|
368
|
+
OUTPUT_PARTS+=("[SESSION GUARD] ⚠️ CLAUDE.md appears STALE (score: ${STALENESS_SCORE}/${STALENESS_THRESHOLD})")
|
|
368
369
|
OUTPUT_PARTS+=("")
|
|
369
|
-
OUTPUT_PARTS+=("
|
|
370
|
+
OUTPUT_PARTS+=("Signals:")
|
|
370
371
|
for sig in "${STALENESS_SIGNALS[@]}"; do
|
|
371
372
|
OUTPUT_PARTS+=(" $sig")
|
|
372
373
|
done
|
|
373
374
|
OUTPUT_PARTS+=("")
|
|
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")
|
|
375
382
|
OUTPUT_PARTS+=("")
|
|
376
|
-
OUTPUT_PARTS+=("\"I've detected that your CLAUDE.md may be out of date based on these signals:")
|
|
377
|
-
for sig in "${STALENESS_SIGNALS[@]}"; do
|
|
378
|
-
OUTPUT_PARTS+=("- ${sig#⚠ }")
|
|
379
|
-
done
|
|
380
|
-
OUTPUT_PARTS+=("")
|
|
381
|
-
OUTPUT_PARTS+=("Would you like me to review and update CLAUDE.md to reflect the current state of the project?\"")
|
|
382
|
-
OUTPUT_PARTS+=("")
|
|
383
|
-
OUTPUT_PARTS+=("If the user agrees, read the current CLAUDE.md, analyse the project structure, dependencies, and recent changes, then update CLAUDE.md accordingly. Preserve any user-written notes or conventions that are still accurate.")
|
|
384
383
|
elif (( ${#STALENESS_SIGNALS[@]} > 0 )); then
|
|
385
384
|
OUTPUT_PARTS+=("")
|
|
386
|
-
OUTPUT_PARTS+=("[SESSION GUARD] ℹ️ Minor drift
|
|
385
|
+
OUTPUT_PARTS+=("[SESSION GUARD] ℹ️ Minor drift (score: ${STALENESS_SCORE}/${STALENESS_THRESHOLD}) — not flagging:")
|
|
387
386
|
for sig in "${STALENESS_SIGNALS[@]}"; do
|
|
388
387
|
OUTPUT_PARTS+=(" $sig")
|
|
389
388
|
done
|
|
390
389
|
fi
|
|
391
390
|
|
|
392
|
-
# Join and emit
|
|
393
391
|
CONTEXT=$(printf '%s\n' "${OUTPUT_PARTS[@]}")
|
|
394
392
|
|
|
395
393
|
jq -n --arg ctx "$CONTEXT" '{
|
|
@@ -399,4 +397,10 @@ jq -n --arg ctx "$CONTEXT" '{
|
|
|
399
397
|
}
|
|
400
398
|
}'
|
|
401
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
|
+
|
|
402
406
|
exit 0
|
package/package.json
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slamb2k/mad-skills",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Claude Code skills collection
|
|
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
11
|
"skills/",
|
|
16
|
-
"commands/",
|
|
17
12
|
"agents/",
|
|
18
13
|
"hooks/",
|
|
19
14
|
".claude-plugin/"
|