@slamb2k/mad-skills 2.0.12 → 2.0.14
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/plugin.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/lib/{banner.js → banner.cjs} +1 -1
- package/hooks/lib/{config.js → config.cjs} +18 -1
- package/hooks/lib/{git-checks.js → git-checks.cjs} +2 -2
- package/hooks/lib/{staleness.js → staleness.cjs} +2 -2
- package/hooks/lib/{task-checks.js → task-checks.cjs} +2 -2
- package/hooks/{session-guard.js → session-guard.cjs} +7 -7
- package/package.json +1 -1
- package/skills/manifest.json +1 -1
- package/hooks/session-guard-prompt.sh +0 -59
- package/hooks/session-guard.sh +0 -400
- /package/hooks/lib/{output.js → output.cjs} +0 -0
- /package/hooks/lib/{state.js → state.cjs} +0 -0
- /package/hooks/lib/{utils.js → utils.cjs} +0 -0
package/hooks/hooks.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"hooks": {
|
|
3
3
|
"SessionStart": [{
|
|
4
4
|
"matcher": "startup|clear|compact",
|
|
5
|
-
"hooks": [{ "type": "command", "command": "node
|
|
5
|
+
"hooks": [{ "type": "command", "command": "node ./hooks/session-guard.cjs check", "timeout": 30 }]
|
|
6
6
|
}],
|
|
7
7
|
"UserPromptSubmit": [{
|
|
8
|
-
"hooks": [{ "type": "command", "command": "node
|
|
8
|
+
"hooks": [{ "type": "command", "command": "node ./hooks/session-guard.cjs remind", "timeout": 10 }]
|
|
9
9
|
}]
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { readFileSync } = require('fs');
|
|
4
|
+
const { join, dirname } = require('path');
|
|
5
|
+
|
|
6
|
+
function getVersion() {
|
|
7
|
+
// Walk up from lib/ to find package.json (mad-skills plugin root)
|
|
8
|
+
const hooksDir = dirname(__dirname);
|
|
9
|
+
for (const candidate of [
|
|
10
|
+
join(hooksDir, '..', 'package.json'),
|
|
11
|
+
join(hooksDir, '..', '.claude-plugin', 'plugin.json'),
|
|
12
|
+
]) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(candidate, 'utf8')).version;
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
return '0.0.0-dev';
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
module.exports = {
|
|
4
|
-
version:
|
|
21
|
+
version: getVersion(),
|
|
5
22
|
|
|
6
23
|
staleness: {
|
|
7
24
|
threshold: 3,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { existsSync } = require('fs');
|
|
4
4
|
const { join, dirname, basename } = require('path');
|
|
5
|
-
const config = require('./config');
|
|
6
|
-
const { git, readJson, countFiles } = require('./utils');
|
|
5
|
+
const config = require('./config.cjs');
|
|
6
|
+
const { git, readJson, countFiles } = require('./utils.cjs');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Validate git repository state.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { existsSync } = require('fs');
|
|
4
4
|
const { join } = require('path');
|
|
5
|
-
const config = require('./config');
|
|
6
|
-
const { fileMtime, git, readJson, readText, getDirectories } = require('./utils');
|
|
5
|
+
const config = require('./config.cjs');
|
|
6
|
+
const { fileMtime, git, readJson, readText, getDirectories } = require('./utils.cjs');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Evaluate all staleness signals for CLAUDE.md.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
const { existsSync } = require('fs');
|
|
4
4
|
const { join, basename } = require('path');
|
|
5
5
|
const { homedir } = require('os');
|
|
6
|
-
const config = require('./config');
|
|
7
|
-
const { readJson, git } = require('./utils');
|
|
6
|
+
const config = require('./config.cjs');
|
|
7
|
+
const { readJson, git } = require('./utils.cjs');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Check if a persistent Task List ID is configured.
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
const { existsSync } = require('fs');
|
|
21
21
|
const { join } = require('path');
|
|
22
22
|
|
|
23
|
-
const config = require('./lib/config');
|
|
24
|
-
const state = require('./lib/state');
|
|
25
|
-
const { OutputBuilder } = require('./lib/output');
|
|
26
|
-
const { getBanner, BANNER_MARKER } = require('./lib/banner');
|
|
27
|
-
const { checkGit } = require('./lib/git-checks');
|
|
28
|
-
const { checkTaskList } = require('./lib/task-checks');
|
|
29
|
-
const { checkStaleness } = require('./lib/staleness');
|
|
23
|
+
const config = require('./lib/config.cjs');
|
|
24
|
+
const state = require('./lib/state.cjs');
|
|
25
|
+
const { OutputBuilder } = require('./lib/output.cjs');
|
|
26
|
+
const { getBanner, BANNER_MARKER } = require('./lib/banner.cjs');
|
|
27
|
+
const { checkGit } = require('./lib/git-checks.cjs');
|
|
28
|
+
const { checkTaskList } = require('./lib/task-checks.cjs');
|
|
29
|
+
const { checkStaleness } = require('./lib/staleness.cjs');
|
|
30
30
|
|
|
31
31
|
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
32
32
|
const CLAUDE_MD = join(PROJECT_DIR, 'CLAUDE.md');
|
package/package.json
CHANGED
package/skills/manifest.json
CHANGED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# session-guard-prompt.sh — UserPromptSubmit companion hook for session-guard
|
|
3
|
-
#
|
|
4
|
-
# Checks if session-guard.sh left a pending context file from SessionStart.
|
|
5
|
-
# If found, re-emits the context as additionalContext on the first user prompt,
|
|
6
|
-
# then deletes the flag file so it only fires once.
|
|
7
|
-
#
|
|
8
|
-
# This works around the known limitation where SessionStart hook output is
|
|
9
|
-
# silently injected and may not surface until after the first prompt is
|
|
10
|
-
# already processed (see anthropics/claude-code#10808).
|
|
11
|
-
#
|
|
12
|
-
# Install globally in ~/.claude/settings.json:
|
|
13
|
-
# "command": "\"$HOME\"/.claude/hooks/session-guard-prompt.sh"
|
|
14
|
-
#
|
|
15
|
-
# Or per-project in .claude/settings.json:
|
|
16
|
-
# "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-guard-prompt.sh"
|
|
17
|
-
|
|
18
|
-
set -uo pipefail
|
|
19
|
-
|
|
20
|
-
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
21
|
-
PENDING_DIR="${TMPDIR:-/tmp}/claude-session-guard"
|
|
22
|
-
GUARD_KEY=$(echo "$PROJECT_DIR" | md5sum 2>/dev/null | cut -d' ' -f1 || echo "default")
|
|
23
|
-
PENDING_FILE="$PENDING_DIR/$GUARD_KEY.pending"
|
|
24
|
-
|
|
25
|
-
# No pending file — fast exit
|
|
26
|
-
if [[ ! -f "$PENDING_FILE" ]]; then
|
|
27
|
-
jq -n '{}'
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
CONTEXT=$(cat "$PENDING_FILE")
|
|
32
|
-
rm -f "$PENDING_FILE"
|
|
33
|
-
|
|
34
|
-
# Also clean up the dedup lock from session-guard.sh
|
|
35
|
-
rm -f "$PENDING_DIR/$GUARD_KEY.lock"
|
|
36
|
-
|
|
37
|
-
# Skip re-emit if session-guard found no issues (just the ✅ line, no warnings)
|
|
38
|
-
if [[ $(echo "$CONTEXT" | grep -c '⚠️\|ℹ️') -eq 0 ]]; then
|
|
39
|
-
jq -n '{}'
|
|
40
|
-
exit 0
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
WRAPPED=$(cat <<EOF
|
|
44
|
-
[SESSION GUARD — FIRST PROMPT REMINDER]
|
|
45
|
-
The following was detected at session start. Act on these items NOW using
|
|
46
|
-
AskUserQuestion BEFORE proceeding with the user's request.
|
|
47
|
-
|
|
48
|
-
$CONTEXT
|
|
49
|
-
EOF
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
jq -n --arg ctx "$WRAPPED" '{
|
|
53
|
-
hookSpecificOutput: {
|
|
54
|
-
hookEventName: "UserPromptSubmit",
|
|
55
|
-
additionalContext: $ctx
|
|
56
|
-
}
|
|
57
|
-
}'
|
|
58
|
-
|
|
59
|
-
exit 0
|
package/hooks/session-guard.sh
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
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
|
-
for part in "${EARLY_CONTEXT_PARTS[@]+"${EARLY_CONTEXT_PARTS[@]}"}"; do
|
|
355
|
-
OUTPUT_PARTS+=("$part")
|
|
356
|
-
done
|
|
357
|
-
|
|
358
|
-
OUTPUT_PARTS+=("[SESSION GUARD] ✅ CLAUDE.md found in: $PROJECT_DIR")
|
|
359
|
-
|
|
360
|
-
if (( STALENESS_SCORE >= STALENESS_THRESHOLD )); then
|
|
361
|
-
OUTPUT_PARTS+=("")
|
|
362
|
-
OUTPUT_PARTS+=("[SESSION GUARD] ⚠️ CLAUDE.md appears STALE (score: ${STALENESS_SCORE}/${STALENESS_THRESHOLD})")
|
|
363
|
-
OUTPUT_PARTS+=("")
|
|
364
|
-
OUTPUT_PARTS+=("Signals:")
|
|
365
|
-
for sig in "${STALENESS_SIGNALS[@]}"; do
|
|
366
|
-
OUTPUT_PARTS+=(" $sig")
|
|
367
|
-
done
|
|
368
|
-
OUTPUT_PARTS+=("")
|
|
369
|
-
OUTPUT_PARTS+=("Use AskUserQuestion to prompt:")
|
|
370
|
-
OUTPUT_PARTS+=(" Question: \"CLAUDE.md appears out of date (${#STALENESS_SIGNALS[@]} signals detected). What would you like to do?\"")
|
|
371
|
-
OUTPUT_PARTS+=(" Type: single_select")
|
|
372
|
-
OUTPUT_PARTS+=(" Options:")
|
|
373
|
-
OUTPUT_PARTS+=(" 1. \"Update it\" — review project structure, deps, recent changes and update CLAUDE.md (preserve user-written notes)")
|
|
374
|
-
OUTPUT_PARTS+=(" 2. \"Show signals\" — list what's drifted before deciding")
|
|
375
|
-
OUTPUT_PARTS+=(" 3. \"Skip\" — continue with current CLAUDE.md")
|
|
376
|
-
OUTPUT_PARTS+=("")
|
|
377
|
-
elif (( ${#STALENESS_SIGNALS[@]} > 0 )); then
|
|
378
|
-
OUTPUT_PARTS+=("")
|
|
379
|
-
OUTPUT_PARTS+=("[SESSION GUARD] ℹ️ Minor drift (score: ${STALENESS_SCORE}/${STALENESS_THRESHOLD}) — not flagging:")
|
|
380
|
-
for sig in "${STALENESS_SIGNALS[@]}"; do
|
|
381
|
-
OUTPUT_PARTS+=(" $sig")
|
|
382
|
-
done
|
|
383
|
-
fi
|
|
384
|
-
|
|
385
|
-
CONTEXT=$(printf '%s\n' "${OUTPUT_PARTS[@]}")
|
|
386
|
-
|
|
387
|
-
jq -n --arg ctx "$CONTEXT" '{
|
|
388
|
-
hookSpecificOutput: {
|
|
389
|
-
hookEventName: "SessionStart",
|
|
390
|
-
additionalContext: $ctx
|
|
391
|
-
}
|
|
392
|
-
}'
|
|
393
|
-
|
|
394
|
-
# ---------------------------------------------------------------------------
|
|
395
|
-
# 5) Write pending flag for UserPromptSubmit companion hook
|
|
396
|
-
# ---------------------------------------------------------------------------
|
|
397
|
-
PENDING_FILE="$PENDING_DIR/$GUARD_KEY.pending"
|
|
398
|
-
echo "$CONTEXT" > "$PENDING_FILE"
|
|
399
|
-
|
|
400
|
-
exit 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|